From 2febfc17e678a3e32e03557708042b2026b77ca8 Mon Sep 17 00:00:00 2001 From: zbtrs2 Date: Mon, 29 Jun 2026 01:50:09 +0800 Subject: [PATCH 1/9] ollama-kvcache: add cache-key, prefix index and cost arbiter Introduce the algorithmic core of the Ollama KVCache Bus, an integration that lets independent Ollama/llama.cpp processes share prompt KV through the Mooncake Store. cachekey derives a content-addressed key from a model fingerprint (model digest, tokenizer/RoPE hashes parsed from GGUF metadata, KV dtype/layout, context length, block size) and a forward-chained per-block hash of the prompt tokens, where h_i = H(h_{i-1} || block_i). Chaining makes longest-prefix matching exact: two prompts agree on h_i iff they share every token of blocks 0..i. KV produced under different model parameters lands in a disjoint key space and is never reused. prefixindex maintains an in-memory radix tree over those chained hashes, the cross-process analogue of a RadixAttention tree whose nodes reference remote KV snapshots. It answers longest-prefix lookups in O(blocks) and tracks per-prefix fan-out (hotness) and recency for replication and eviction decisions. arbiter decides restore-vs-recompute per request. A cached prefix only helps when the store can deliver its KV faster than the GPU regenerates it; the arbiter estimates both from rates it learns online (EWMA, seeded on first observation) per model, and restores only when it is cheaper. This keeps KV sharing loss-free across hardware regimes. Includes unit tests for hash determinism, prefix consistency, model separation, longest-prefix matching, eviction ordering and the arbiter's adapt-to-recompute behaviour. --- mooncake-integration/ollama/bridge/go.mod | 21 ++ mooncake-integration/ollama/bridge/go.sum | 24 ++ .../ollama/bridge/internal/arbiter/arbiter.go | 213 ++++++++++++ .../bridge/internal/arbiter/arbiter_test.go | 57 ++++ .../bridge/internal/cachekey/gguf_meta.go | 320 ++++++++++++++++++ .../ollama/bridge/internal/cachekey/key.go | 122 +++++++ .../bridge/internal/cachekey/key_test.go | 91 +++++ .../bridge/internal/prefixindex/radix.go | 221 ++++++++++++ .../bridge/internal/prefixindex/radix_test.go | 65 ++++ 9 files changed, 1134 insertions(+) create mode 100644 mooncake-integration/ollama/bridge/go.mod create mode 100644 mooncake-integration/ollama/bridge/go.sum create mode 100644 mooncake-integration/ollama/bridge/internal/arbiter/arbiter.go create mode 100644 mooncake-integration/ollama/bridge/internal/arbiter/arbiter_test.go create mode 100644 mooncake-integration/ollama/bridge/internal/cachekey/gguf_meta.go create mode 100644 mooncake-integration/ollama/bridge/internal/cachekey/key.go create mode 100644 mooncake-integration/ollama/bridge/internal/cachekey/key_test.go create mode 100644 mooncake-integration/ollama/bridge/internal/prefixindex/radix.go create mode 100644 mooncake-integration/ollama/bridge/internal/prefixindex/radix_test.go diff --git a/mooncake-integration/ollama/bridge/go.mod b/mooncake-integration/ollama/bridge/go.mod new file mode 100644 index 00000000..e62898cb --- /dev/null +++ b/mooncake-integration/ollama/bridge/go.mod @@ -0,0 +1,21 @@ +module github.com/mooncake-ai/ollama-mooncake-bridge + +go 1.23 + +require ( + github.com/prometheus/client_golang v1.19.1 + github.com/prometheus/client_model v0.5.0 + google.golang.org/grpc v1.64.1 + google.golang.org/protobuf v1.34.2 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect +) diff --git a/mooncake-integration/ollama/bridge/go.sum b/mooncake-integration/ollama/bridge/go.sum new file mode 100644 index 00000000..a2c5717e --- /dev/null +++ b/mooncake-integration/ollama/bridge/go.sum @@ -0,0 +1,24 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/mooncake-integration/ollama/bridge/internal/arbiter/arbiter.go b/mooncake-integration/ollama/bridge/internal/arbiter/arbiter.go new file mode 100644 index 00000000..74f26528 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/arbiter/arbiter.go @@ -0,0 +1,213 @@ +// Package arbiter decides, per request, whether reusing a cached KV prefix is +// actually worth it — i.e. whether *fetching* the KV from the store is cheaper +// than *recomputing* it with a prefill. +// +// Why this matters: a KV cache only helps if the store can deliver KV bytes +// faster than the GPU regenerates them during prefill. The break-even bandwidth +// is +// +// B* = (KV bytes per token) * (prefill tokens/sec) +// +// For the models we test that is ~0.6–0.85 GB/s. Over slow TCP loopback we +// measured ~0.5 GB/s (so recompute wins); over RDMA ~30–40 GB/s (so restore +// wins by ~50x). A naive "always restore on hit" policy would *lose* on slow +// transports. The arbiter instead estimates both times from quantities it +// learns online and picks the cheaper — making the system robust on any fabric. +// +// All rates are tracked with EWMAs fed by real observations from the +// orchestrator, so the estimates self-calibrate to the live hardware. +package arbiter + +import ( + "fmt" + "sync" +) + +// ewma is a thread-safe exponentially-weighted moving average. The FIRST real +// sample *replaces* the prior (seed-on-first) so the estimate adapts to live +// hardware within a single observation instead of slowly decaying from a guess. +type ewma struct { + mu sync.Mutex + alpha float64 + val float64 + n uint64 +} + +func newEWMA(alpha, prior float64) *ewma { return &ewma{alpha: alpha, val: prior} } + +func (e *ewma) update(sample float64) { + if sample <= 0 { + return + } + e.mu.Lock() + if e.n == 0 { + e.val = sample // seed on first real observation + } else { + e.val = e.alpha*sample + (1-e.alpha)*e.val + } + e.n++ + e.mu.Unlock() +} + +func (e *ewma) get() float64 { + e.mu.Lock() + defer e.mu.Unlock() + return e.val +} + +// Config holds priors and policy knobs. +type Config struct { + // Priors used before any observation. + PriorGetGBps float64 // store read bandwidth, GB/s + PriorPrefillToksS float64 // prefill throughput, tokens/s + PriorRestoreMsFix float64 // fixed restore overhead (file write + llama load), ms + // Policy. + MinTokens int // never bother below this many matched tokens + SafetyMargin float64 // restore only if est_restore < margin * est_recompute (e.g. 0.85) + Alpha float64 // EWMA smoothing +} + +func DefaultConfig() Config { + return Config{ + PriorGetGBps: 2.0, // effective end-to-end restore bandwidth (store fetch + GPU load) + PriorPrefillToksS: 8000., // typical small-model prefill on a modern GPU + PriorRestoreMsFix: 8.0, + MinTokens: 256, + SafetyMargin: 0.9, + Alpha: 0.3, + } +} + +// modelState holds the learned rates for one (model, kv-type). Keeping them +// per-model means a fast tiny model and a slow large model don't pollute each +// other's restore-vs-recompute decision. +type modelState struct { + getGBps *ewma // effective end-to-end restore bandwidth (store fetch + GPU load) + prefillTPS *ewma // prefill throughput + restoreFix *ewma // fixed restore overhead, ms +} + +// Arbiter is safe for concurrent use. +type Arbiter struct { + cfg Config + mu sync.Mutex + states map[string]*modelState +} + +func New(cfg Config) *Arbiter { + if cfg.Alpha == 0 { + cfg = DefaultConfig() + } + return &Arbiter{cfg: cfg, states: map[string]*modelState{}} +} + +func (a *Arbiter) state(key string) *modelState { + a.mu.Lock() + defer a.mu.Unlock() + s := a.states[key] + if s == nil { + s = &modelState{ + getGBps: newEWMA(a.cfg.Alpha, a.cfg.PriorGetGBps), + prefillTPS: newEWMA(a.cfg.Alpha, a.cfg.PriorPrefillToksS), + restoreFix: newEWMA(a.cfg.Alpha, a.cfg.PriorRestoreMsFix), + } + a.states[key] = s + } + return s +} + +// Decision is the arbiter's verdict for one candidate prefix. +type Decision struct { + Restore bool + EstRestoreMs float64 + EstRecomputeMs float64 + Reason string +} + +// Decide weighs restoring a `matchedBytes`/`matchedTokens` prefix for model +// `key` against recomputing those tokens. +func (a *Arbiter) Decide(key string, matchedTokens int, matchedBytes uint64) Decision { + if matchedTokens < a.cfg.MinTokens { + return Decision{Restore: false, Reason: fmt.Sprintf("matched %d toks < min %d", matchedTokens, a.cfg.MinTokens)} + } + s := a.state(key) + gbps := s.getGBps.get() + tps := s.prefillTPS.get() + fix := s.restoreFix.get() + + estRestore := float64(matchedBytes)/(gbps*1e9)*1e3 + fix // ms + estRecompute := float64(matchedTokens) / tps * 1e3 // ms + + if estRestore < a.cfg.SafetyMargin*estRecompute { + return Decision{ + Restore: true, + EstRestoreMs: estRestore, + EstRecomputeMs: estRecompute, + Reason: fmt.Sprintf("restore %.1fms < %.0f%%*recompute %.1fms (restore-path %.1fGB/s, prefill %.0f tok/s)", + estRestore, a.cfg.SafetyMargin*100, estRecompute, gbps, tps), + } + } + return Decision{ + Restore: false, + EstRestoreMs: estRestore, + EstRecomputeMs: estRecompute, + Reason: fmt.Sprintf("recompute %.1fms <= restore %.1fms (restore-path %.1fGB/s too slow to beat prefill for %s)", + estRecompute, estRestore, gbps, humanBytes(matchedBytes)), + } +} + +// --- online feedback from the orchestrator --- + +// ObserveGet feeds a real end-to-end restore: `bytes` moved in `ms` ms. +func (a *Arbiter) ObserveGet(key string, bytes uint64, ms float64) { + if ms > 0 && bytes > 0 { + a.state(key).getGBps.update(float64(bytes) / (ms / 1e3) / 1e9) + } +} + +// ObservePrefill feeds a real prefill: `tokens` computed in `ms` ms. +func (a *Arbiter) ObservePrefill(key string, tokens int, ms float64) { + if ms > 0 && tokens > 0 { + a.state(key).prefillTPS.update(float64(tokens) / (ms / 1e3)) + } +} + +// ObserveRestoreFixed feeds the fixed (size-independent) restore overhead. +func (a *Arbiter) ObserveRestoreFixed(key string, ms float64) { a.state(key).restoreFix.update(ms) } + +// Snapshot exposes the average learned estimates across models (for metrics). +type Snapshot struct { + GetGBps float64 + PrefillToksS float64 + RestoreFixMs float64 + Models int +} + +func (a *Arbiter) Snapshot() Snapshot { + a.mu.Lock() + defer a.mu.Unlock() + if len(a.states) == 0 { + return Snapshot{GetGBps: a.cfg.PriorGetGBps, PrefillToksS: a.cfg.PriorPrefillToksS, RestoreFixMs: a.cfg.PriorRestoreMsFix} + } + var g, t, f float64 + for _, s := range a.states { + g += s.getGBps.get() + t += s.prefillTPS.get() + f += s.restoreFix.get() + } + n := float64(len(a.states)) + return Snapshot{GetGBps: g / n, PrefillToksS: t / n, RestoreFixMs: f / n, Models: len(a.states)} +} + +func humanBytes(b uint64) string { + const u = 1024 + if b < u { + return fmt.Sprintf("%dB", b) + } + div, exp := uint64(u), 0 + for n := b / u; n >= u; n /= u { + div *= u + exp++ + } + return fmt.Sprintf("%.1f%ciB", float64(b)/float64(div), "KMGTPE"[exp]) +} diff --git a/mooncake-integration/ollama/bridge/internal/arbiter/arbiter_test.go b/mooncake-integration/ollama/bridge/internal/arbiter/arbiter_test.go new file mode 100644 index 00000000..808c65c1 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/arbiter/arbiter_test.go @@ -0,0 +1,57 @@ +package arbiter + +import "testing" + +func cfg() Config { + c := DefaultConfig() + c.MinTokens = 100 + return c +} + +func TestDeclineBelowMinTokens(t *testing.T) { + a := New(cfg()) + if a.Decide("m", 50, 1<<20).Restore { + t.Fatal("should not restore below MinTokens") + } +} + +// On a fast model, after one real (slow) restore observation, the arbiter must +// flip to recompute — the loss-free property. +func TestAdaptsToRecompute(t *testing.T) { + a := New(cfg()) + // fast prefill: 20000 tok/s ; a 8000-token, 220MB prefix + a.ObservePrefill("fast", 8000, 8000.0/20000*1000) // 400ms + // first decision uses the optimistic prior -> restore + d0 := a.Decide("fast", 8000, 220<<20) + if !d0.Restore { + t.Fatalf("first decision should try restore, got %q", d0.Reason) + } + // observe that the *full* restore actually took 600ms (slower than prefill) + a.ObserveGet("fast", 220<<20, 600) + d1 := a.Decide("fast", 8000, 220<<20) + if d1.Restore { + t.Fatalf("after learning slow restore, should recompute; got %q", d1.Reason) + } +} + +// On a slow model (large, slow prefill), restore should keep winning. +func TestRestoreWinsWhenWorthIt(t *testing.T) { + a := New(cfg()) + a.ObservePrefill("big", 30000, 30000.0/8000*1000) // 3750ms to prefill 30k toks + a.ObserveGet("big", 880<<20, 1800) // 880MB restore in 1.8s + d := a.Decide("big", 30000, 880<<20) + if !d.Restore { + t.Fatalf("restore should win for slow-prefill model; got %q", d.Reason) + } +} + +// Per-model isolation: a slow model's observations must not poison a fast one. +func TestPerModelIsolation(t *testing.T) { + a := New(cfg()) + a.ObserveGet("slowstore", 100<<20, 5000) // terrible bandwidth for model A + a.ObservePrefill("fastgpu", 8000, 200) // model B prefills fast + // model B has its own (prior) restore estimate, unaffected by A + if a.Snapshot().Models != 2 { + t.Fatalf("want 2 model states, got %d", a.Snapshot().Models) + } +} diff --git a/mooncake-integration/ollama/bridge/internal/cachekey/gguf_meta.go b/mooncake-integration/ollama/bridge/internal/cachekey/gguf_meta.go new file mode 100644 index 00000000..c3df2b2a --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/cachekey/gguf_meta.go @@ -0,0 +1,320 @@ +package cachekey + +import ( + "bufio" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "fmt" + "io" + "math" + "os" + "sort" + "strings" +) + +// GGUF metadata value types (see ggml-org/llama.cpp gguf spec). +const ( + ggufUint8 uint32 = iota + ggufInt8 + ggufUint16 + ggufInt16 + ggufUint32 + ggufInt32 + ggufFloat32 + ggufBool + ggufString + ggufArray + ggufUint64 + ggufInt64 + ggufFloat64 +) + +// GGUFMeta is the subset of GGUF metadata we parse. We deliberately read only +// the header KV block (not tensor data), so this is fast even for multi-GB +// files. +type GGUFMeta struct { + Path string + Version uint32 + Arch string + KV map[string]any // raw metadata kv (scalars + small arrays summarized) + metaHash string // sha256 of the raw metadata block bytes +} + +// ReadGGUFMeta parses just enough of a .gguf file to build a ModelFingerprint. +func ReadGGUFMeta(path string) (*GGUFMeta, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + br := bufio.NewReaderSize(f, 1<<20) + + // We hash every byte we consume from the metadata block to derive a stable + // model digest without hashing the (huge) tensor payload. + hsh := sha256.New() + r := io.TeeReader(br, hsh) + + var magic [4]byte + if _, err := io.ReadFull(r, magic[:]); err != nil { + return nil, err + } + if string(magic[:]) != "GGUF" { + return nil, fmt.Errorf("not a GGUF file (magic=%q)", magic[:]) + } + m := &GGUFMeta{Path: path, KV: map[string]any{}} + rd := &leReader{r: r} + m.Version = rd.u32() + _ = rd.u64() // tensor_count (we don't need tensors) + kvCount := rd.u64() + if rd.err != nil { + return nil, rd.err + } + for i := uint64(0); i < kvCount && rd.err == nil; i++ { + key := rd.str() + val := rd.value() + if key != "" { + m.KV[key] = val + } + } + if rd.err != nil && rd.err != io.EOF { + return nil, fmt.Errorf("gguf parse: %w", rd.err) + } + if a, ok := m.KV["general.architecture"].(string); ok { + m.Arch = a + } + m.metaHash = hex.EncodeToString(hsh.Sum(nil)) + return m, nil +} + +// Fingerprint derives a ModelFingerprint from parsed GGUF metadata. kvType is +// the runtime --cache-type-k/v (defaults to f16); swa marks sliding-window +// attention. modelDigest, if non-empty (e.g. an Ollama blob digest), overrides +// the metadata-hash fallback. +func (m *GGUFMeta) Fingerprint(modelDigest, kvType string, swa bool, blockSize int) ModelFingerprint { + if modelDigest == "" { + modelDigest = "ggufmeta-" + m.metaHash[:24] + } + if kvType == "" { + kvType = "f16" + } + layout := "dense" + if swa { + layout = "swa" + } + fp := ModelFingerprint{ + ModelDigest: modelDigest, + Arch: orUnknown(m.Arch), + TokenizerHash: m.tokenizerHash(), + RopeHash: m.ropeHash(), + KVType: kvType, + KVLayout: layout, + NCtxTrain: m.ctxLen(), + BlockSize: blockSize, + } + return fp +} + +func (m *GGUFMeta) archKey(suffix string) string { return m.Arch + "." + suffix } + +func (m *GGUFMeta) ctxLen() int { + if v, ok := toInt(m.KV[m.archKey("context_length")]); ok { + return v + } + return 0 +} + +// tokenizerHash folds the tokenizer model id and a compact signature of the +// vocabulary (size + a sample fingerprint) so different tokenizers never share +// keys, without serializing the whole vocab. +func (m *GGUFMeta) tokenizerHash() string { + h := sha256.New() + write := func(k string) { + if v, ok := m.KV[k]; ok { + fmt.Fprintf(h, "%s=%v;", k, summarize(v)) + } + } + write("tokenizer.ggml.model") + write("tokenizer.ggml.pre") + write("tokenizer.ggml.bos_token_id") + write("tokenizer.ggml.eos_token_id") + write("tokenizer.ggml.add_bos_token") + write("tokenizer.chat_template") + return hex.EncodeToString(h.Sum(nil)[:12]) +} + +func (m *GGUFMeta) ropeHash() string { + h := sha256.New() + for _, k := range []string{ + m.archKey("rope.dimension_count"), + m.archKey("rope.freq_base"), + m.archKey("rope.scaling.type"), + m.archKey("rope.scaling.factor"), + m.archKey("rope.scaling.original_context_length"), + m.archKey("embedding_length"), + m.archKey("attention.head_count"), + m.archKey("attention.head_count_kv"), + m.archKey("block_count"), + } { + if v, ok := m.KV[k]; ok { + fmt.Fprintf(h, "%s=%v;", k, summarize(v)) + } + } + return hex.EncodeToString(h.Sum(nil)[:12]) +} + +// ---- low-level little-endian GGUF reader ---- + +type leReader struct { + r io.Reader + err error + b8 [8]byte +} + +func (z *leReader) read(n int) []byte { + if z.err != nil { + return nil + } + buf := make([]byte, n) + if _, err := io.ReadFull(z.r, buf); err != nil { + z.err = err + return nil + } + return buf +} +func (z *leReader) u32() uint32 { + if z.err != nil { + return 0 + } + if _, err := io.ReadFull(z.r, z.b8[:4]); err != nil { + z.err = err + return 0 + } + return binary.LittleEndian.Uint32(z.b8[:4]) +} +func (z *leReader) u64() uint64 { + if z.err != nil { + return 0 + } + if _, err := io.ReadFull(z.r, z.b8[:8]); err != nil { + z.err = err + return 0 + } + return binary.LittleEndian.Uint64(z.b8[:8]) +} +func (z *leReader) str() string { + n := z.u64() + if z.err != nil || n > (1<<28) { // guard against corrupt lengths + if n > (1 << 28) { + z.err = fmt.Errorf("gguf string too long: %d", n) + } + return "" + } + return string(z.read(int(n))) +} + +// value reads a single typed metadata value. Arrays are summarized (we keep +// length + first few elements) to avoid materializing huge vocab arrays. +func (z *leReader) value() any { + t := z.u32() + return z.valueOfType(t) +} + +func (z *leReader) valueOfType(t uint32) any { + switch t { + case ggufUint8: + return uint64(z.read(1)[0]) + case ggufInt8: + return int64(int8(z.read(1)[0])) + case ggufUint16: + b := z.read(2) + if b == nil { + return nil + } + return uint64(binary.LittleEndian.Uint16(b)) + case ggufInt16: + b := z.read(2) + if b == nil { + return nil + } + return int64(int16(binary.LittleEndian.Uint16(b))) + case ggufUint32: + return uint64(z.u32()) + case ggufInt32: + return int64(int32(z.u32())) + case ggufFloat32: + return float64(math.Float32frombits(z.u32())) + case ggufBool: + b := z.read(1) + if b == nil { + return nil + } + return b[0] != 0 + case ggufString: + return z.str() + case ggufUint64: + return z.u64() + case ggufInt64: + return int64(z.u64()) + case ggufFloat64: + return math.Float64frombits(z.u64()) + case ggufArray: + et := z.u32() + n := z.u64() + if z.err != nil { + return nil + } + // Summarize: keep length + up to 8 sample elements so tokenizer arrays + // don't blow up memory but still contribute to the signature. + const sample = 8 + arr := arraySummary{ElemType: et, Len: n} + for i := uint64(0); i < n && z.err == nil; i++ { + v := z.valueOfType(et) + if i < sample { + arr.Head = append(arr.Head, v) + } + } + return arr + default: + z.err = fmt.Errorf("gguf: unknown value type %d", t) + return nil + } +} + +type arraySummary struct { + ElemType uint32 + Len uint64 + Head []any +} + +func summarize(v any) string { + if a, ok := v.(arraySummary); ok { + parts := make([]string, 0, len(a.Head)) + for _, e := range a.Head { + parts = append(parts, fmt.Sprintf("%v", e)) + } + sort.Strings(parts) + return fmt.Sprintf("arr(t=%d,n=%d)[%s]", a.ElemType, a.Len, strings.Join(parts, ",")) + } + return fmt.Sprintf("%v", v) +} + +func toInt(v any) (int, bool) { + switch x := v.(type) { + case uint64: + return int(x), true + case int64: + return int(x), true + case float64: + return int(x), true + default: + return 0, false + } +} + +func orUnknown(s string) string { + if s == "" { + return "unknown" + } + return s +} diff --git a/mooncake-integration/ollama/bridge/internal/cachekey/key.go b/mooncake-integration/ollama/bridge/internal/cachekey/key.go new file mode 100644 index 00000000..23d092f3 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/cachekey/key.go @@ -0,0 +1,122 @@ +// Package cachekey builds the cache namespace and per-block chained hashes that +// uniquely (and *safely*) identify a reusable KV-cache prefix. +// +// The cardinal rule: KV produced by a different model / tokenizer / RoPE / KV +// dtype / KV layout MUST NEVER be reused. A wrong reuse silently poisons +// generation and is far worse than a miss. Therefore every field that can change +// the numerical content of the KV cache is folded into the key. We would rather +// miss than mis-hit. +package cachekey + +import ( + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "fmt" + "strings" +) + +// ModelFingerprint captures every attribute that affects the *bytes* of a KV +// cache entry. Two requests may share a cached prefix only if their +// fingerprints are byte-identical. +type ModelFingerprint struct { + ModelDigest string // ollama blob digest, or a stable hash of the GGUF metadata block + Arch string // general.architecture (llama, qwen2, ...) + TokenizerHash string // hash over tokenizer model + vocab signature + RopeHash string // hash over rope dims / freq base / scaling + KVType string // f16 | q8_0 | q4_0 | bf16 ... (--cache-type-k/v) + KVLayout string // dense | swa (sliding-window attention changes layout) + NCtxTrain int // training context length + BlockSize int // token granularity of a cache block +} + +// Valid reports whether the fingerprint is populated enough to be trusted. +// An under-specified fingerprint must force a miss, never a risky hit. +func (m ModelFingerprint) Valid() bool { + return m.ModelDigest != "" && m.Arch != "" && m.KVType != "" && m.BlockSize > 0 +} + +// Prefix is the stable, human-readable namespace shared by every block key that +// belongs to this fingerprint + user namespace. BlockKey appends the trailing +// {block_index}:{prefix_block_hash}. +// +// ollama:{model_digest}:{tokenizer_hash}:{rope_hash}:{kv_dtype}:{kv_layout}:{n_ctx_train}:{block_size}:{user_ns} +func (m ModelFingerprint) Prefix(userNamespace string) string { + if userNamespace == "" { + userNamespace = "_" + } + // sanitize: ':' is our delimiter, so it may not appear inside a field. + clean := func(s string) string { return strings.ReplaceAll(s, ":", "_") } + return strings.Join([]string{ + "ollama", + clean(m.ModelDigest), + clean(emptyTo(m.TokenizerHash, "-")), + clean(emptyTo(m.RopeHash, "-")), + clean(m.KVType), + clean(emptyTo(m.KVLayout, "dense")), + fmt.Sprintf("%d", m.NCtxTrain), + fmt.Sprintf("%d", m.BlockSize), + clean(userNamespace), + }, ":") +} + +// BlockKey is the store object key for the prefix that ends at block boundary +// blockIndex (i.e. it covers blocks 0..blockIndex-1, = blockIndex*BlockSize +// tokens). chainHashHex is the chained hash returned by ChainBlockHashes for +// that boundary. +func (m ModelFingerprint) BlockKey(userNamespace string, blockIndex int, chainHashHex string) string { + return fmt.Sprintf("%s:%d:%s", m.Prefix(userNamespace), blockIndex, chainHashHex) +} + +// Chain holds, for a token sequence, the chained hash at every *full* block +// boundary. Chain.Hex[i] is the hash covering tokens[0 : (i+1)*BlockSize], i.e. +// the key material for a prefix of (i+1) blocks. A trailing partial block (if +// any) is intentionally NOT hashed: only block-aligned prefixes are reusable, +// which keeps keys deterministic across requests that share a long head. +type Chain struct { + BlockSize int + NumTokens int + FullBlocks int // len(Hex) + Hex []string // 16-byte (128-bit) truncated sha256, hex-encoded +} + +// ChainBlockHashes computes the chained, content-addressed hash of each full +// block of tokens, seeded by `seed` (the fingerprint Prefix), so that the same +// tokens under a different model produce different hashes. +// +// h_0 = H( seed_bytes || block_0_bytes ) +// h_i = H( h_{i-1} || block_i_bytes ) +// +// Chaining guarantees prefix consistency: two sequences agree on h_i iff they +// agree on every token of blocks 0..i. This is what makes longest-prefix +// matching correct. +func ChainBlockHashes(seed string, tokens []int32, blockSize int) Chain { + if blockSize <= 0 { + blockSize = 256 + } + full := len(tokens) / blockSize + out := Chain{BlockSize: blockSize, NumTokens: len(tokens), FullBlocks: full, Hex: make([]string, 0, full)} + + prev := sha256.Sum256([]byte("omb-kvcache-v1\x00" + seed)) + buf := make([]byte, blockSize*4) // 4 bytes per int32 token + for i := 0; i < full; i++ { + blk := tokens[i*blockSize : (i+1)*blockSize] + for j, t := range blk { + binary.LittleEndian.PutUint32(buf[j*4:], uint32(t)) + } + h := sha256.New() + h.Write(prev[:]) + h.Write(buf) + sum := h.Sum(nil) + copy(prev[:], sum) // chain forward (full 32 bytes carried, 16 emitted) + out.Hex = append(out.Hex, hex.EncodeToString(sum[:16])) + } + return out +} + +func emptyTo(s, d string) string { + if s == "" { + return d + } + return s +} diff --git a/mooncake-integration/ollama/bridge/internal/cachekey/key_test.go b/mooncake-integration/ollama/bridge/internal/cachekey/key_test.go new file mode 100644 index 00000000..f38676d2 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/cachekey/key_test.go @@ -0,0 +1,91 @@ +package cachekey + +import ( + "os" + "testing" +) + +func TestChainBlockHashes_Determinism(t *testing.T) { + toks := make([]int32, 1000) + for i := range toks { + toks[i] = int32(i * 7 % 50000) + } + a := ChainBlockHashes("seed", toks, 256) + b := ChainBlockHashes("seed", toks, 256) + if a.FullBlocks != 1000/256 { + t.Fatalf("FullBlocks = %d, want %d", a.FullBlocks, 1000/256) + } + for i := range a.Hex { + if a.Hex[i] != b.Hex[i] { + t.Fatalf("nondeterministic at %d", i) + } + } +} + +func TestChainBlockHashes_PrefixConsistency(t *testing.T) { + // Two sequences sharing the first 3 blocks must agree on the first 3 chain + // hashes and (almost surely) differ afterwards. + base := make([]int32, 4*256) + for i := range base { + base[i] = int32(i) + } + x := append([]int32{}, base...) + y := append([]int32{}, base...) + // diverge in block index 3 (token 3*256) + y[3*256] = 999999 + cx := ChainBlockHashes("s", x, 256) + cy := ChainBlockHashes("s", y, 256) + for i := 0; i < 3; i++ { + if cx.Hex[i] != cy.Hex[i] { + t.Fatalf("shared block %d hashes differ", i) + } + } + if cx.Hex[3] == cy.Hex[3] { + t.Fatalf("divergent block 3 hashes collided") + } +} + +func TestChainBlockHashes_SeedSeparation(t *testing.T) { + toks := make([]int32, 512) + a := ChainBlockHashes("modelA", toks, 256) + b := ChainBlockHashes("modelB", toks, 256) + if a.Hex[0] == b.Hex[0] { + t.Fatalf("different seeds (models) must not share hashes") + } +} + +func TestFingerprintPrefixStable(t *testing.T) { + fp := ModelFingerprint{ModelDigest: "d", Arch: "qwen2", KVType: "f16", BlockSize: 256} + if !fp.Valid() { + t.Fatal("expected valid fingerprint") + } + if got := fp.Prefix("ns"); got == "" { + t.Fatal("empty prefix") + } + // ':' inside a field must be sanitized so it can't break the delimiter. + fp2 := fp + fp2.ModelDigest = "a:b:c" + if p := fp2.Prefix("ns"); p == fp.Prefix("ns") { + t.Fatal("digest with colons should differ") + } +} + +// Parses the real model if OMB_TEST_GGUF points at a .gguf file. +func TestReadGGUFMeta_Real(t *testing.T) { + path := os.Getenv("OMB_TEST_GGUF") + if path == "" { + t.Skip("set OMB_TEST_GGUF to a .gguf to run") + } + m, err := ReadGGUFMeta(path) + if err != nil { + t.Fatalf("ReadGGUFMeta: %v", err) + } + if m.Arch == "" { + t.Fatalf("arch empty") + } + fp := m.Fingerprint("", "f16", false, 256) + if !fp.Valid() { + t.Fatalf("incomplete fingerprint: %+v", fp) + } + t.Logf("arch=%s ctx=%d tok=%s rope=%s digest=%s", fp.Arch, fp.NCtxTrain, fp.TokenizerHash, fp.RopeHash, fp.ModelDigest) +} diff --git a/mooncake-integration/ollama/bridge/internal/prefixindex/radix.go b/mooncake-integration/ollama/bridge/internal/prefixindex/radix.go new file mode 100644 index 00000000..f6af43e9 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/prefixindex/radix.go @@ -0,0 +1,221 @@ +// Package prefixindex maintains an in-memory radix (prefix) tree over the +// *chained block hashes* of cached KV prefixes. It is the cross-process / +// cross-node analogue of SGLang's RadixAttention tree, but the nodes reference +// KV snapshots living in the Mooncake Store rather than GPU memory. +// +// Because a chained block hash h_i already encodes blocks 0..i (see +// cachekey.ChainBlockHashes), two sequences that share the first j blocks share +// the identical path h_0..h_{j-1} in the tree and then diverge. Walking a +// request's chain down the tree therefore yields, in O(blocks), the longest +// stored prefix that is a true prefix of the request — and, as a free +// by-product, the *fan-out* at each node tells us how "hot" (widely shared) a +// prefix is, which drives replication / soft-pin decisions. +// +// The tree is a per-node hint: the authoritative existence check is the store. +// We use the tree for (a) a fast local hit path, (b) blob-size lookup so the +// cost arbiter can size a transfer without a round-trip, and (c) LRU eviction. +package prefixindex + +import ( + "sort" + "sync" + "time" +) + +type node struct { + children map[string]*node // next-block chain hash -> child + parent *node + edge string // the chain hash on the edge from parent to this node + + hasSnapshot bool // a KV snapshot exists for the prefix ending here + storeKey string // store object key for that snapshot + blocks int // depth in blocks (== prefix length / blockSize) + tokens int // exact token count of the snapshot + bytes uint64 // snapshot size + hits uint64 // times this snapshot was reused + lastUsed time.Time +} + +func newNode(parent *node, edge string, depth int) *node { + return &node{children: map[string]*node{}, parent: parent, edge: edge, blocks: depth} +} + +// Match is the result of a longest-prefix lookup. +type Match struct { + Found bool + Blocks int + Tokens int + Bytes uint64 + StoreKey string + Hits uint64 + // Fanout is how many distinct continuations diverge right after the matched + // prefix — a proxy for how widely shared (hot) this prefix is. + Fanout int +} + +// Index is a forest: one radix tree per root key (fingerprint+namespace). +type Index struct { + mu sync.RWMutex + roots map[string]*node + nSnap int + totalB uint64 +} + +func New() *Index { return &Index{roots: map[string]*node{}} } + +func (ix *Index) rootFor(rootKey string, create bool) *node { + r := ix.roots[rootKey] + if r == nil && create { + r = newNode(nil, "", 0) + ix.roots[rootKey] = r + } + return r +} + +// Insert records that a KV snapshot covering `blocks` blocks (chain[:blocks]) +// is stored under storeKey. chain must have at least `blocks` entries. +func (ix *Index) Insert(rootKey string, chain []string, blocks, tokens int, storeKey string, bytes uint64) { + if blocks <= 0 || blocks > len(chain) { + return + } + ix.mu.Lock() + defer ix.mu.Unlock() + cur := ix.rootFor(rootKey, true) + for i := 0; i < blocks; i++ { + h := chain[i] + ch := cur.children[h] + if ch == nil { + ch = newNode(cur, h, i+1) + cur.children[h] = ch + } + cur = ch + } + if !cur.hasSnapshot { + ix.nSnap++ + ix.totalB += bytes + } else { + ix.totalB += bytes - cur.bytes + } + cur.hasSnapshot = true + cur.storeKey = storeKey + cur.tokens = tokens + cur.bytes = bytes + cur.lastUsed = time.Now() +} + +// LongestMatch walks the request chain and returns the deepest node that holds +// a snapshot. touch=true bumps hit counters/recency for the matched node. +func (ix *Index) LongestMatch(rootKey string, chain []string, touch bool) Match { + ix.mu.Lock() + defer ix.mu.Unlock() + r := ix.rootFor(rootKey, false) + if r == nil { + return Match{} + } + cur := r + var best *node + for i := 0; i < len(chain); i++ { + ch := cur.children[chain[i]] + if ch == nil { + break + } + cur = ch + if cur.hasSnapshot { + best = cur + } + } + if best == nil { + return Match{} + } + if touch { + best.hits++ + best.lastUsed = time.Now() + } + return Match{ + Found: true, Blocks: best.blocks, Tokens: best.tokens, Bytes: best.bytes, + StoreKey: best.storeKey, Hits: best.hits, Fanout: len(best.children), + } +} + +// Forget drops the snapshot mark at the given depth (after a store eviction). +func (ix *Index) Forget(rootKey string, chain []string, blocks int) { + ix.mu.Lock() + defer ix.mu.Unlock() + r := ix.rootFor(rootKey, false) + if r == nil { + return + } + cur := r + for i := 0; i < blocks && cur != nil; i++ { + cur = cur.children[chain[i]] + } + if cur != nil && cur.hasSnapshot { + cur.hasSnapshot = false + ix.nSnap-- + ix.totalB -= cur.bytes + ix.prune(rootKey, cur) + } +} + +// prune removes now-empty leaf chains to bound memory. +func (ix *Index) prune(rootKey string, n *node) { + for n != nil && n.parent != nil && len(n.children) == 0 && !n.hasSnapshot { + p := n.parent + delete(p.children, n.edge) + n = p + } + if r := ix.roots[rootKey]; r != nil && len(r.children) == 0 { + delete(ix.roots, rootKey) + } +} + +// EvictionCandidate is a snapshot worth removing under memory pressure. +type EvictionCandidate struct { + RootKey string + StoreKey string + Bytes uint64 + Hits uint64 + LastUsed time.Time + Blocks int +} + +// ColdestSnapshots returns up to n least-recently-used snapshots (LRU), so the +// caller can evict them from the store and the tree. +func (ix *Index) ColdestSnapshots(n int) []EvictionCandidate { + ix.mu.RLock() + defer ix.mu.RUnlock() + var all []EvictionCandidate + for rk, root := range ix.roots { + var walk func(*node) + walk = func(nd *node) { + if nd.hasSnapshot { + all = append(all, EvictionCandidate{ + RootKey: rk, StoreKey: nd.storeKey, Bytes: nd.bytes, + Hits: nd.hits, LastUsed: nd.lastUsed, Blocks: nd.blocks, + }) + } + for _, c := range nd.children { + walk(c) + } + } + walk(root) + } + sort.Slice(all, func(i, j int) bool { return all[i].LastUsed.Before(all[j].LastUsed) }) + if len(all) > n { + all = all[:n] + } + return all +} + +// Stats snapshots index-wide counters. +type Stats struct { + Snapshots int + Bytes uint64 + Roots int +} + +func (ix *Index) Stats() Stats { + ix.mu.RLock() + defer ix.mu.RUnlock() + return Stats{Snapshots: ix.nSnap, Bytes: ix.totalB, Roots: len(ix.roots)} +} diff --git a/mooncake-integration/ollama/bridge/internal/prefixindex/radix_test.go b/mooncake-integration/ollama/bridge/internal/prefixindex/radix_test.go new file mode 100644 index 00000000..3520c6d1 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/prefixindex/radix_test.go @@ -0,0 +1,65 @@ +package prefixindex + +import "testing" + +func chain(n int) []string { + c := make([]string, n) + for i := range c { + c[i] = string(rune('a'+i%26)) + string(rune('0'+i/26)) + } + return c +} + +func TestLongestMatch(t *testing.T) { + ix := New() + c := chain(10) + // store a 4-block and a 7-block snapshot on the same path + ix.Insert("m", c, 4, 4*256, "key4", 1000) + ix.Insert("m", c, 7, 7*256, "key7", 2000) + + // a request sharing all 10 blocks should match the deepest stored (7) + m := ix.LongestMatch("m", c, false) + if !m.Found || m.Blocks != 7 || m.StoreKey != "key7" || m.Bytes != 2000 { + t.Fatalf("want 7-block key7, got %+v", m) + } + + // a request that diverges at block 5 should match the 4-block snapshot + d := append([]string{}, c...) + d[5] = "ZZ" + m = ix.LongestMatch("m", d, false) + if !m.Found || m.Blocks != 4 || m.StoreKey != "key4" { + t.Fatalf("want 4-block key4 after divergence, got %+v", m) + } + + // unknown root => no match + if ix.LongestMatch("other", c, false).Found { + t.Fatal("unexpected match in empty root") + } +} + +func TestForgetAndStats(t *testing.T) { + ix := New() + c := chain(6) + ix.Insert("m", c, 3, 3*256, "k", 500) + if s := ix.Stats(); s.Snapshots != 1 || s.Bytes != 500 { + t.Fatalf("stats after insert: %+v", s) + } + ix.Forget("m", c, 3) + if ix.LongestMatch("m", c, false).Found { + t.Fatal("match after forget") + } + if s := ix.Stats(); s.Snapshots != 0 { + t.Fatalf("snapshots after forget: %d", s.Snapshots) + } +} + +func TestColdestEviction(t *testing.T) { + ix := New() + c := chain(8) + ix.Insert("m", c, 2, 512, "old", 100) + ix.Insert("m", c, 5, 1280, "new", 200) + got := ix.ColdestSnapshots(1) + if len(got) != 1 || got[0].StoreKey != "old" { + t.Fatalf("want oldest 'old', got %+v", got) + } +} -- 2.34.1 From 4927b5a694a83ed16e87b5466daa524177d08c69 Mon Sep 17 00:00:00 2001 From: zbtrs2 Date: Mon, 29 Jun 2026 01:50:25 +0800 Subject: [PATCH 2/9] ollama-kvcache: add gRPC contracts and store backends Define the two service contracts and the object-store abstraction the orchestrator builds on. bridge.proto is the agent-facing KVCacheBus: Lookup (read-only longest prefix match plus arbiter decision), Prepare (Lookup plus restore of the matched KV into a llama.cpp slot) and Commit (save the slot KV back to the store). storeproxy.proto is the sidecar-to-proxy contract; KV blobs are addressed by file path so a multi-GiB snapshot is copied at most once and never streams through the Go process. The store package exposes a small Backend interface with two implementations: a Mooncake backend that forwards file-path Put/Get and batched existence checks to the Python store proxy over gRPC, and a local filesystem backend used as the cross-process baseline and a dependency-free fallback. Generated stubs are checked in so the module builds without protoc. --- .../ollama/bridge/api/bridge.proto | 150 ++ .../bridge/internal/bridge/pb/bridge.pb.go | 1651 +++++++++++++++++ .../internal/bridge/pb/bridge_grpc.pb.go | 278 +++ .../ollama/bridge/internal/store/local.go | 118 ++ .../ollama/bridge/internal/store/mooncake.go | 110 ++ .../ollama/bridge/internal/store/store.go | 49 + .../internal/storeproxy/pb/storeproxy.pb.go | 1479 +++++++++++++++ .../storeproxy/pb/storeproxy_grpc.pb.go | 396 ++++ .../ollama/proto/storeproxy.proto | 97 + 9 files changed, 4328 insertions(+) create mode 100644 mooncake-integration/ollama/bridge/api/bridge.proto create mode 100644 mooncake-integration/ollama/bridge/internal/bridge/pb/bridge.pb.go create mode 100644 mooncake-integration/ollama/bridge/internal/bridge/pb/bridge_grpc.pb.go create mode 100644 mooncake-integration/ollama/bridge/internal/store/local.go create mode 100644 mooncake-integration/ollama/bridge/internal/store/mooncake.go create mode 100644 mooncake-integration/ollama/bridge/internal/store/store.go create mode 100644 mooncake-integration/ollama/bridge/internal/storeproxy/pb/storeproxy.pb.go create mode 100644 mooncake-integration/ollama/bridge/internal/storeproxy/pb/storeproxy_grpc.pb.go create mode 100644 mooncake-integration/ollama/proto/storeproxy.proto diff --git a/mooncake-integration/ollama/bridge/api/bridge.proto b/mooncake-integration/ollama/bridge/api/bridge.proto new file mode 100644 index 00000000..26d55f08 --- /dev/null +++ b/mooncake-integration/ollama/bridge/api/bridge.proto @@ -0,0 +1,150 @@ +syntax = "proto3"; + +// KVCacheBus is the contract between a caller (a coding Agent, the bench +// harness, or patched Ollama) and the Go sidecar. It implements the three-stage +// reuse flow: Lookup (longest-prefix match) -> Prepare (Load/restore the matched +// KV into a llama.cpp slot) -> Commit (Save the produced KV back to the Mooncake +// Store). +package bridge.v1; + +option go_package = "github.com/mooncake-ai/ollama-mooncake-bridge/internal/bridge/pb;bridgepb"; + +service KVCacheBus { + rpc Health(HealthRequest) returns (HealthReply); + // Lookup is read-only: longest block-prefix match + arbiter decision, no I/O + // into llama.cpp. Useful for inspection / benchmarking. + rpc Lookup(LookupRequest) returns (LookupReply); + // Prepare = Lookup + (if a hit and the cost arbiter approves) restore the + // matched KV prefix into target.slot so llama.cpp only prefills the tail. + rpc Prepare(PrepareRequest) returns (PrepareReply); + // Commit = Save the KV currently held in target.slot and Put it to the store. + rpc Commit(CommitRequest) returns (CommitReply); + rpc Stats(StatsRequest) returns (StatsReply); +} + +// ModelFingerprint: every field that changes KV bytes. A mismatch => miss. +message ModelFingerprint { + string model_digest = 1; + string arch = 2; + string tokenizer_hash = 3; + string rope_hash = 4; + string kv_type = 5; // f16 | q8_0 | q4_0 | bf16 + string kv_layout = 6; // dense | swa + int32 n_ctx_train = 7; + int32 block_size = 8; + // Optional path to the GGUF file. If set, the sidecar parses it once to fill + // any empty arch/tokenizer/rope/ctx fields (and a content-addressed digest), + // so callers only need to know the model file. + string model_path = 9; +} + +// CachePolicy mirrors the user-visible options.mooncake.* request extension. +message CachePolicy { + bool enable = 1; + string namespace = 2; // e.g. "repo:my-org/my-repo@main" + bool read = 3; + bool write = 4; + // granularity and transport are accepted for forward compatibility. Block + // granularity is the implemented mode; the store transport (tcp|rdma) is + // chosen once by the store-proxy at startup rather than per request. + string granularity = 5; // block (implemented) | sequence (reserved) + int32 block_size = 6; // overrides fingerprint block_size if > 0 + string transport = 7; // advisory; effective transport is set on the proxy + uint32 replica_num = 8; // hot-prefix replication + bool soft_pin = 9; // keep hot prefixes resident + int32 min_prefix_blocks = 10; // admission: don't store prefixes shorter than this +} + +// LlamaTarget identifies a running llama.cpp server slot to restore into / read +// from. base_url like "http://127.0.0.1:52070"; slot_id is the /slots id. +message LlamaTarget { + string base_url = 1; + int32 slot_id = 2; +} + +message LookupRequest { + ModelFingerprint fp = 1; + CachePolicy policy = 2; + repeated int32 tokens = 3; +} +message LookupReply { + bool hit = 1; + int32 matched_blocks = 2; + int32 matched_tokens = 3; + int32 total_blocks = 4; + int32 total_tokens = 5; + string decision = 6; // restore | recompute | miss + string matched_key = 7; + string reason = 8; // arbiter explanation + string error = 9; +} + +message PrepareRequest { + ModelFingerprint fp = 1; + CachePolicy policy = 2; + repeated int32 tokens = 3; + LlamaTarget target = 4; // if unset, behaves like Lookup (plan only) +} +message PrepareReply { + bool hit = 1; + int32 matched_blocks = 2; + int32 matched_tokens = 3; + int32 total_blocks = 4; + int32 total_tokens = 5; + string decision = 6; + string matched_key = 7; + bool restored = 8; + int32 restored_tokens = 9; + double restore_ms = 10; // llama /slots restore time + double store_get_ms = 11; // store GetFile time + uint64 bytes = 12; + string reason = 13; + string error = 14; +} + +message CommitRequest { + ModelFingerprint fp = 1; + CachePolicy policy = 2; + repeated int32 tokens = 3; // tokens resident in the slot (the prefill prefix) + LlamaTarget target = 4; + // Optional: the caller's observed prefill so the arbiter learns the live + // prefill rate (tokens/s) for restore-vs-recompute decisions. + int32 prefill_n = 5; + double prefill_ms = 6; +} +message CommitReply { + bool ok = 1; + bool stored = 2; // false if skipped (already present / below threshold) + int32 stored_blocks = 3; + int32 stored_tokens = 4; + uint64 bytes = 5; + double save_ms = 6; // llama /slots save time + double store_put_ms = 7; // store PutFile time + string key = 8; + string reason = 9; + string error = 10; +} + +message HealthRequest {} +message HealthReply { + bool ok = 1; + string version = 2; + string store_backend = 3; + bool store_ok = 4; + string protocol = 5; + string detail = 6; +} + +message StatsRequest {} +message StatsReply { + uint64 prepare_total = 1; + uint64 hits = 2; + uint64 misses = 3; + uint64 hit_blocks = 4; + uint64 miss_blocks = 5; + uint64 saved_prefill_tokens = 6; // primary effectiveness metric + uint64 restore_count = 7; + uint64 commit_count = 8; + uint64 bytes_get = 9; + uint64 bytes_put = 10; +} diff --git a/mooncake-integration/ollama/bridge/internal/bridge/pb/bridge.pb.go b/mooncake-integration/ollama/bridge/internal/bridge/pb/bridge.pb.go new file mode 100644 index 00000000..4fc18839 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/bridge/pb/bridge.pb.go @@ -0,0 +1,1651 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v3.12.4 +// source: bridge.proto + +// KVCacheBus is the contract between a caller (a coding Agent, the bench +// harness, or patched Ollama) and the Go sidecar. It implements the three-stage +// reuse flow: Lookup (longest-prefix match) -> Prepare (Load/restore the matched +// KV into a llama.cpp slot) -> Commit (Save the produced KV back to the Mooncake +// Store). + +package bridgepb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ModelFingerprint: every field that changes KV bytes. A mismatch => miss. +type ModelFingerprint struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ModelDigest string `protobuf:"bytes,1,opt,name=model_digest,json=modelDigest,proto3" json:"model_digest,omitempty"` + Arch string `protobuf:"bytes,2,opt,name=arch,proto3" json:"arch,omitempty"` + TokenizerHash string `protobuf:"bytes,3,opt,name=tokenizer_hash,json=tokenizerHash,proto3" json:"tokenizer_hash,omitempty"` + RopeHash string `protobuf:"bytes,4,opt,name=rope_hash,json=ropeHash,proto3" json:"rope_hash,omitempty"` + KvType string `protobuf:"bytes,5,opt,name=kv_type,json=kvType,proto3" json:"kv_type,omitempty"` // f16 | q8_0 | q4_0 | bf16 + KvLayout string `protobuf:"bytes,6,opt,name=kv_layout,json=kvLayout,proto3" json:"kv_layout,omitempty"` // dense | swa + NCtxTrain int32 `protobuf:"varint,7,opt,name=n_ctx_train,json=nCtxTrain,proto3" json:"n_ctx_train,omitempty"` + BlockSize int32 `protobuf:"varint,8,opt,name=block_size,json=blockSize,proto3" json:"block_size,omitempty"` + // Optional path to the GGUF file. If set, the sidecar parses it once to fill + // any empty arch/tokenizer/rope/ctx fields (and a content-addressed digest), + // so callers only need to know the model file. + ModelPath string `protobuf:"bytes,9,opt,name=model_path,json=modelPath,proto3" json:"model_path,omitempty"` +} + +func (x *ModelFingerprint) Reset() { + *x = ModelFingerprint{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ModelFingerprint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelFingerprint) ProtoMessage() {} + +func (x *ModelFingerprint) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModelFingerprint.ProtoReflect.Descriptor instead. +func (*ModelFingerprint) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{0} +} + +func (x *ModelFingerprint) GetModelDigest() string { + if x != nil { + return x.ModelDigest + } + return "" +} + +func (x *ModelFingerprint) GetArch() string { + if x != nil { + return x.Arch + } + return "" +} + +func (x *ModelFingerprint) GetTokenizerHash() string { + if x != nil { + return x.TokenizerHash + } + return "" +} + +func (x *ModelFingerprint) GetRopeHash() string { + if x != nil { + return x.RopeHash + } + return "" +} + +func (x *ModelFingerprint) GetKvType() string { + if x != nil { + return x.KvType + } + return "" +} + +func (x *ModelFingerprint) GetKvLayout() string { + if x != nil { + return x.KvLayout + } + return "" +} + +func (x *ModelFingerprint) GetNCtxTrain() int32 { + if x != nil { + return x.NCtxTrain + } + return 0 +} + +func (x *ModelFingerprint) GetBlockSize() int32 { + if x != nil { + return x.BlockSize + } + return 0 +} + +func (x *ModelFingerprint) GetModelPath() string { + if x != nil { + return x.ModelPath + } + return "" +} + +// CachePolicy mirrors the user-visible options.mooncake.* request extension. +type CachePolicy struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enable bool `protobuf:"varint,1,opt,name=enable,proto3" json:"enable,omitempty"` + Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` // e.g. "repo:my-org/my-repo@main" + Read bool `protobuf:"varint,3,opt,name=read,proto3" json:"read,omitempty"` + Write bool `protobuf:"varint,4,opt,name=write,proto3" json:"write,omitempty"` + // granularity and transport are accepted for forward compatibility. Block + // granularity is the implemented mode; the store transport (tcp|rdma) is + // chosen once by the store-proxy at startup rather than per request. + Granularity string `protobuf:"bytes,5,opt,name=granularity,proto3" json:"granularity,omitempty"` // block (implemented) | sequence (reserved) + BlockSize int32 `protobuf:"varint,6,opt,name=block_size,json=blockSize,proto3" json:"block_size,omitempty"` // overrides fingerprint block_size if > 0 + Transport string `protobuf:"bytes,7,opt,name=transport,proto3" json:"transport,omitempty"` // advisory; effective transport is set on the proxy + ReplicaNum uint32 `protobuf:"varint,8,opt,name=replica_num,json=replicaNum,proto3" json:"replica_num,omitempty"` // hot-prefix replication + SoftPin bool `protobuf:"varint,9,opt,name=soft_pin,json=softPin,proto3" json:"soft_pin,omitempty"` // keep hot prefixes resident + MinPrefixBlocks int32 `protobuf:"varint,10,opt,name=min_prefix_blocks,json=minPrefixBlocks,proto3" json:"min_prefix_blocks,omitempty"` // admission: don't store prefixes shorter than this +} + +func (x *CachePolicy) Reset() { + *x = CachePolicy{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CachePolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CachePolicy) ProtoMessage() {} + +func (x *CachePolicy) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CachePolicy.ProtoReflect.Descriptor instead. +func (*CachePolicy) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{1} +} + +func (x *CachePolicy) GetEnable() bool { + if x != nil { + return x.Enable + } + return false +} + +func (x *CachePolicy) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *CachePolicy) GetRead() bool { + if x != nil { + return x.Read + } + return false +} + +func (x *CachePolicy) GetWrite() bool { + if x != nil { + return x.Write + } + return false +} + +func (x *CachePolicy) GetGranularity() string { + if x != nil { + return x.Granularity + } + return "" +} + +func (x *CachePolicy) GetBlockSize() int32 { + if x != nil { + return x.BlockSize + } + return 0 +} + +func (x *CachePolicy) GetTransport() string { + if x != nil { + return x.Transport + } + return "" +} + +func (x *CachePolicy) GetReplicaNum() uint32 { + if x != nil { + return x.ReplicaNum + } + return 0 +} + +func (x *CachePolicy) GetSoftPin() bool { + if x != nil { + return x.SoftPin + } + return false +} + +func (x *CachePolicy) GetMinPrefixBlocks() int32 { + if x != nil { + return x.MinPrefixBlocks + } + return 0 +} + +// LlamaTarget identifies a running llama.cpp server slot to restore into / read +// from. base_url like "http://127.0.0.1:52070"; slot_id is the /slots id. +type LlamaTarget struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + BaseUrl string `protobuf:"bytes,1,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` + SlotId int32 `protobuf:"varint,2,opt,name=slot_id,json=slotId,proto3" json:"slot_id,omitempty"` +} + +func (x *LlamaTarget) Reset() { + *x = LlamaTarget{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LlamaTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LlamaTarget) ProtoMessage() {} + +func (x *LlamaTarget) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LlamaTarget.ProtoReflect.Descriptor instead. +func (*LlamaTarget) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{2} +} + +func (x *LlamaTarget) GetBaseUrl() string { + if x != nil { + return x.BaseUrl + } + return "" +} + +func (x *LlamaTarget) GetSlotId() int32 { + if x != nil { + return x.SlotId + } + return 0 +} + +type LookupRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Fp *ModelFingerprint `protobuf:"bytes,1,opt,name=fp,proto3" json:"fp,omitempty"` + Policy *CachePolicy `protobuf:"bytes,2,opt,name=policy,proto3" json:"policy,omitempty"` + Tokens []int32 `protobuf:"varint,3,rep,packed,name=tokens,proto3" json:"tokens,omitempty"` +} + +func (x *LookupRequest) Reset() { + *x = LookupRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LookupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LookupRequest) ProtoMessage() {} + +func (x *LookupRequest) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LookupRequest.ProtoReflect.Descriptor instead. +func (*LookupRequest) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{3} +} + +func (x *LookupRequest) GetFp() *ModelFingerprint { + if x != nil { + return x.Fp + } + return nil +} + +func (x *LookupRequest) GetPolicy() *CachePolicy { + if x != nil { + return x.Policy + } + return nil +} + +func (x *LookupRequest) GetTokens() []int32 { + if x != nil { + return x.Tokens + } + return nil +} + +type LookupReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Hit bool `protobuf:"varint,1,opt,name=hit,proto3" json:"hit,omitempty"` + MatchedBlocks int32 `protobuf:"varint,2,opt,name=matched_blocks,json=matchedBlocks,proto3" json:"matched_blocks,omitempty"` + MatchedTokens int32 `protobuf:"varint,3,opt,name=matched_tokens,json=matchedTokens,proto3" json:"matched_tokens,omitempty"` + TotalBlocks int32 `protobuf:"varint,4,opt,name=total_blocks,json=totalBlocks,proto3" json:"total_blocks,omitempty"` + TotalTokens int32 `protobuf:"varint,5,opt,name=total_tokens,json=totalTokens,proto3" json:"total_tokens,omitempty"` + Decision string `protobuf:"bytes,6,opt,name=decision,proto3" json:"decision,omitempty"` // restore | recompute | miss + MatchedKey string `protobuf:"bytes,7,opt,name=matched_key,json=matchedKey,proto3" json:"matched_key,omitempty"` + Reason string `protobuf:"bytes,8,opt,name=reason,proto3" json:"reason,omitempty"` // arbiter explanation + Error string `protobuf:"bytes,9,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *LookupReply) Reset() { + *x = LookupReply{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LookupReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LookupReply) ProtoMessage() {} + +func (x *LookupReply) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LookupReply.ProtoReflect.Descriptor instead. +func (*LookupReply) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{4} +} + +func (x *LookupReply) GetHit() bool { + if x != nil { + return x.Hit + } + return false +} + +func (x *LookupReply) GetMatchedBlocks() int32 { + if x != nil { + return x.MatchedBlocks + } + return 0 +} + +func (x *LookupReply) GetMatchedTokens() int32 { + if x != nil { + return x.MatchedTokens + } + return 0 +} + +func (x *LookupReply) GetTotalBlocks() int32 { + if x != nil { + return x.TotalBlocks + } + return 0 +} + +func (x *LookupReply) GetTotalTokens() int32 { + if x != nil { + return x.TotalTokens + } + return 0 +} + +func (x *LookupReply) GetDecision() string { + if x != nil { + return x.Decision + } + return "" +} + +func (x *LookupReply) GetMatchedKey() string { + if x != nil { + return x.MatchedKey + } + return "" +} + +func (x *LookupReply) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *LookupReply) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type PrepareRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Fp *ModelFingerprint `protobuf:"bytes,1,opt,name=fp,proto3" json:"fp,omitempty"` + Policy *CachePolicy `protobuf:"bytes,2,opt,name=policy,proto3" json:"policy,omitempty"` + Tokens []int32 `protobuf:"varint,3,rep,packed,name=tokens,proto3" json:"tokens,omitempty"` + Target *LlamaTarget `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` // if unset, behaves like Lookup (plan only) +} + +func (x *PrepareRequest) Reset() { + *x = PrepareRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PrepareRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrepareRequest) ProtoMessage() {} + +func (x *PrepareRequest) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrepareRequest.ProtoReflect.Descriptor instead. +func (*PrepareRequest) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{5} +} + +func (x *PrepareRequest) GetFp() *ModelFingerprint { + if x != nil { + return x.Fp + } + return nil +} + +func (x *PrepareRequest) GetPolicy() *CachePolicy { + if x != nil { + return x.Policy + } + return nil +} + +func (x *PrepareRequest) GetTokens() []int32 { + if x != nil { + return x.Tokens + } + return nil +} + +func (x *PrepareRequest) GetTarget() *LlamaTarget { + if x != nil { + return x.Target + } + return nil +} + +type PrepareReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Hit bool `protobuf:"varint,1,opt,name=hit,proto3" json:"hit,omitempty"` + MatchedBlocks int32 `protobuf:"varint,2,opt,name=matched_blocks,json=matchedBlocks,proto3" json:"matched_blocks,omitempty"` + MatchedTokens int32 `protobuf:"varint,3,opt,name=matched_tokens,json=matchedTokens,proto3" json:"matched_tokens,omitempty"` + TotalBlocks int32 `protobuf:"varint,4,opt,name=total_blocks,json=totalBlocks,proto3" json:"total_blocks,omitempty"` + TotalTokens int32 `protobuf:"varint,5,opt,name=total_tokens,json=totalTokens,proto3" json:"total_tokens,omitempty"` + Decision string `protobuf:"bytes,6,opt,name=decision,proto3" json:"decision,omitempty"` + MatchedKey string `protobuf:"bytes,7,opt,name=matched_key,json=matchedKey,proto3" json:"matched_key,omitempty"` + Restored bool `protobuf:"varint,8,opt,name=restored,proto3" json:"restored,omitempty"` + RestoredTokens int32 `protobuf:"varint,9,opt,name=restored_tokens,json=restoredTokens,proto3" json:"restored_tokens,omitempty"` + RestoreMs float64 `protobuf:"fixed64,10,opt,name=restore_ms,json=restoreMs,proto3" json:"restore_ms,omitempty"` // llama /slots restore time + StoreGetMs float64 `protobuf:"fixed64,11,opt,name=store_get_ms,json=storeGetMs,proto3" json:"store_get_ms,omitempty"` // store GetFile time + Bytes uint64 `protobuf:"varint,12,opt,name=bytes,proto3" json:"bytes,omitempty"` + Reason string `protobuf:"bytes,13,opt,name=reason,proto3" json:"reason,omitempty"` + Error string `protobuf:"bytes,14,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *PrepareReply) Reset() { + *x = PrepareReply{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PrepareReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrepareReply) ProtoMessage() {} + +func (x *PrepareReply) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrepareReply.ProtoReflect.Descriptor instead. +func (*PrepareReply) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{6} +} + +func (x *PrepareReply) GetHit() bool { + if x != nil { + return x.Hit + } + return false +} + +func (x *PrepareReply) GetMatchedBlocks() int32 { + if x != nil { + return x.MatchedBlocks + } + return 0 +} + +func (x *PrepareReply) GetMatchedTokens() int32 { + if x != nil { + return x.MatchedTokens + } + return 0 +} + +func (x *PrepareReply) GetTotalBlocks() int32 { + if x != nil { + return x.TotalBlocks + } + return 0 +} + +func (x *PrepareReply) GetTotalTokens() int32 { + if x != nil { + return x.TotalTokens + } + return 0 +} + +func (x *PrepareReply) GetDecision() string { + if x != nil { + return x.Decision + } + return "" +} + +func (x *PrepareReply) GetMatchedKey() string { + if x != nil { + return x.MatchedKey + } + return "" +} + +func (x *PrepareReply) GetRestored() bool { + if x != nil { + return x.Restored + } + return false +} + +func (x *PrepareReply) GetRestoredTokens() int32 { + if x != nil { + return x.RestoredTokens + } + return 0 +} + +func (x *PrepareReply) GetRestoreMs() float64 { + if x != nil { + return x.RestoreMs + } + return 0 +} + +func (x *PrepareReply) GetStoreGetMs() float64 { + if x != nil { + return x.StoreGetMs + } + return 0 +} + +func (x *PrepareReply) GetBytes() uint64 { + if x != nil { + return x.Bytes + } + return 0 +} + +func (x *PrepareReply) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *PrepareReply) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type CommitRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Fp *ModelFingerprint `protobuf:"bytes,1,opt,name=fp,proto3" json:"fp,omitempty"` + Policy *CachePolicy `protobuf:"bytes,2,opt,name=policy,proto3" json:"policy,omitempty"` + Tokens []int32 `protobuf:"varint,3,rep,packed,name=tokens,proto3" json:"tokens,omitempty"` // tokens resident in the slot (the prefill prefix) + Target *LlamaTarget `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` + // Optional: the caller's observed prefill so the arbiter learns the live + // prefill rate (tokens/s) for restore-vs-recompute decisions. + PrefillN int32 `protobuf:"varint,5,opt,name=prefill_n,json=prefillN,proto3" json:"prefill_n,omitempty"` + PrefillMs float64 `protobuf:"fixed64,6,opt,name=prefill_ms,json=prefillMs,proto3" json:"prefill_ms,omitempty"` +} + +func (x *CommitRequest) Reset() { + *x = CommitRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CommitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitRequest) ProtoMessage() {} + +func (x *CommitRequest) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommitRequest.ProtoReflect.Descriptor instead. +func (*CommitRequest) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{7} +} + +func (x *CommitRequest) GetFp() *ModelFingerprint { + if x != nil { + return x.Fp + } + return nil +} + +func (x *CommitRequest) GetPolicy() *CachePolicy { + if x != nil { + return x.Policy + } + return nil +} + +func (x *CommitRequest) GetTokens() []int32 { + if x != nil { + return x.Tokens + } + return nil +} + +func (x *CommitRequest) GetTarget() *LlamaTarget { + if x != nil { + return x.Target + } + return nil +} + +func (x *CommitRequest) GetPrefillN() int32 { + if x != nil { + return x.PrefillN + } + return 0 +} + +func (x *CommitRequest) GetPrefillMs() float64 { + if x != nil { + return x.PrefillMs + } + return 0 +} + +type CommitReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Stored bool `protobuf:"varint,2,opt,name=stored,proto3" json:"stored,omitempty"` // false if skipped (already present / below threshold) + StoredBlocks int32 `protobuf:"varint,3,opt,name=stored_blocks,json=storedBlocks,proto3" json:"stored_blocks,omitempty"` + StoredTokens int32 `protobuf:"varint,4,opt,name=stored_tokens,json=storedTokens,proto3" json:"stored_tokens,omitempty"` + Bytes uint64 `protobuf:"varint,5,opt,name=bytes,proto3" json:"bytes,omitempty"` + SaveMs float64 `protobuf:"fixed64,6,opt,name=save_ms,json=saveMs,proto3" json:"save_ms,omitempty"` // llama /slots save time + StorePutMs float64 `protobuf:"fixed64,7,opt,name=store_put_ms,json=storePutMs,proto3" json:"store_put_ms,omitempty"` // store PutFile time + Key string `protobuf:"bytes,8,opt,name=key,proto3" json:"key,omitempty"` + Reason string `protobuf:"bytes,9,opt,name=reason,proto3" json:"reason,omitempty"` + Error string `protobuf:"bytes,10,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *CommitReply) Reset() { + *x = CommitReply{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CommitReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitReply) ProtoMessage() {} + +func (x *CommitReply) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommitReply.ProtoReflect.Descriptor instead. +func (*CommitReply) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{8} +} + +func (x *CommitReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *CommitReply) GetStored() bool { + if x != nil { + return x.Stored + } + return false +} + +func (x *CommitReply) GetStoredBlocks() int32 { + if x != nil { + return x.StoredBlocks + } + return 0 +} + +func (x *CommitReply) GetStoredTokens() int32 { + if x != nil { + return x.StoredTokens + } + return 0 +} + +func (x *CommitReply) GetBytes() uint64 { + if x != nil { + return x.Bytes + } + return 0 +} + +func (x *CommitReply) GetSaveMs() float64 { + if x != nil { + return x.SaveMs + } + return 0 +} + +func (x *CommitReply) GetStorePutMs() float64 { + if x != nil { + return x.StorePutMs + } + return 0 +} + +func (x *CommitReply) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *CommitReply) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *CommitReply) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type HealthRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *HealthRequest) Reset() { + *x = HealthRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthRequest) ProtoMessage() {} + +func (x *HealthRequest) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthRequest.ProtoReflect.Descriptor instead. +func (*HealthRequest) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{9} +} + +type HealthReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + StoreBackend string `protobuf:"bytes,3,opt,name=store_backend,json=storeBackend,proto3" json:"store_backend,omitempty"` + StoreOk bool `protobuf:"varint,4,opt,name=store_ok,json=storeOk,proto3" json:"store_ok,omitempty"` + Protocol string `protobuf:"bytes,5,opt,name=protocol,proto3" json:"protocol,omitempty"` + Detail string `protobuf:"bytes,6,opt,name=detail,proto3" json:"detail,omitempty"` +} + +func (x *HealthReply) Reset() { + *x = HealthReply{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthReply) ProtoMessage() {} + +func (x *HealthReply) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthReply.ProtoReflect.Descriptor instead. +func (*HealthReply) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{10} +} + +func (x *HealthReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *HealthReply) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *HealthReply) GetStoreBackend() string { + if x != nil { + return x.StoreBackend + } + return "" +} + +func (x *HealthReply) GetStoreOk() bool { + if x != nil { + return x.StoreOk + } + return false +} + +func (x *HealthReply) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *HealthReply) GetDetail() string { + if x != nil { + return x.Detail + } + return "" +} + +type StatsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *StatsRequest) Reset() { + *x = StatsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatsRequest) ProtoMessage() {} + +func (x *StatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatsRequest.ProtoReflect.Descriptor instead. +func (*StatsRequest) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{11} +} + +type StatsReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PrepareTotal uint64 `protobuf:"varint,1,opt,name=prepare_total,json=prepareTotal,proto3" json:"prepare_total,omitempty"` + Hits uint64 `protobuf:"varint,2,opt,name=hits,proto3" json:"hits,omitempty"` + Misses uint64 `protobuf:"varint,3,opt,name=misses,proto3" json:"misses,omitempty"` + HitBlocks uint64 `protobuf:"varint,4,opt,name=hit_blocks,json=hitBlocks,proto3" json:"hit_blocks,omitempty"` + MissBlocks uint64 `protobuf:"varint,5,opt,name=miss_blocks,json=missBlocks,proto3" json:"miss_blocks,omitempty"` + SavedPrefillTokens uint64 `protobuf:"varint,6,opt,name=saved_prefill_tokens,json=savedPrefillTokens,proto3" json:"saved_prefill_tokens,omitempty"` // primary effectiveness metric + RestoreCount uint64 `protobuf:"varint,7,opt,name=restore_count,json=restoreCount,proto3" json:"restore_count,omitempty"` + CommitCount uint64 `protobuf:"varint,8,opt,name=commit_count,json=commitCount,proto3" json:"commit_count,omitempty"` + BytesGet uint64 `protobuf:"varint,9,opt,name=bytes_get,json=bytesGet,proto3" json:"bytes_get,omitempty"` + BytesPut uint64 `protobuf:"varint,10,opt,name=bytes_put,json=bytesPut,proto3" json:"bytes_put,omitempty"` +} + +func (x *StatsReply) Reset() { + *x = StatsReply{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatsReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatsReply) ProtoMessage() {} + +func (x *StatsReply) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatsReply.ProtoReflect.Descriptor instead. +func (*StatsReply) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{12} +} + +func (x *StatsReply) GetPrepareTotal() uint64 { + if x != nil { + return x.PrepareTotal + } + return 0 +} + +func (x *StatsReply) GetHits() uint64 { + if x != nil { + return x.Hits + } + return 0 +} + +func (x *StatsReply) GetMisses() uint64 { + if x != nil { + return x.Misses + } + return 0 +} + +func (x *StatsReply) GetHitBlocks() uint64 { + if x != nil { + return x.HitBlocks + } + return 0 +} + +func (x *StatsReply) GetMissBlocks() uint64 { + if x != nil { + return x.MissBlocks + } + return 0 +} + +func (x *StatsReply) GetSavedPrefillTokens() uint64 { + if x != nil { + return x.SavedPrefillTokens + } + return 0 +} + +func (x *StatsReply) GetRestoreCount() uint64 { + if x != nil { + return x.RestoreCount + } + return 0 +} + +func (x *StatsReply) GetCommitCount() uint64 { + if x != nil { + return x.CommitCount + } + return 0 +} + +func (x *StatsReply) GetBytesGet() uint64 { + if x != nil { + return x.BytesGet + } + return 0 +} + +func (x *StatsReply) GetBytesPut() uint64 { + if x != nil { + return x.BytesPut + } + return 0 +} + +var File_bridge_proto protoreflect.FileDescriptor + +var file_bridge_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, + 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x22, 0xa1, 0x02, 0x0a, 0x10, 0x4d, 0x6f, + 0x64, 0x65, 0x6c, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x21, + 0x0a, 0x0c, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x44, 0x69, 0x67, 0x65, 0x73, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x63, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x61, 0x72, 0x63, 0x68, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x7a, + 0x65, 0x72, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x69, 0x7a, 0x65, 0x72, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, + 0x72, 0x6f, 0x70, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x72, 0x6f, 0x70, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x76, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x76, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6b, 0x76, 0x5f, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6b, 0x76, 0x4c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x12, + 0x1e, 0x0a, 0x0b, 0x6e, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x6e, 0x43, 0x74, 0x78, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x12, + 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1d, + 0x0a, 0x0a, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x22, 0xb4, 0x02, + 0x0a, 0x0b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, + 0x06, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x61, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x04, 0x72, 0x65, 0x61, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x77, 0x72, 0x69, 0x74, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x77, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x0a, + 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x12, + 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1c, + 0x0a, 0x09, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1f, 0x0a, 0x0b, + 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x4e, 0x75, 0x6d, 0x12, 0x19, 0x0a, + 0x08, 0x73, 0x6f, 0x66, 0x74, 0x5f, 0x70, 0x69, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x07, 0x73, 0x6f, 0x66, 0x74, 0x50, 0x69, 0x6e, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x69, 0x6e, 0x5f, + 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0f, 0x6d, 0x69, 0x6e, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x73, 0x22, 0x41, 0x0a, 0x0b, 0x4c, 0x6c, 0x61, 0x6d, 0x61, 0x54, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x17, + 0x0a, 0x07, 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x06, 0x73, 0x6c, 0x6f, 0x74, 0x49, 0x64, 0x22, 0x84, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x6f, 0x6b, + 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x02, 0x66, 0x70, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, + 0x6e, 0x74, 0x52, 0x02, 0x66, 0x70, 0x12, 0x2e, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x05, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x9e, + 0x02, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x68, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x68, 0x69, 0x74, + 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x62, 0x6c, 0x6f, 0x63, + 0x6b, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, + 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x65, 0x64, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x0d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x21, + 0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x6c, 0x6f, 0x63, 0x6b, + 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x4b, 0x65, + 0x79, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, + 0xb5, 0x01, 0x0a, 0x0e, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x02, 0x66, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x6c, + 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x52, 0x02, 0x66, 0x70, 0x12, + 0x2e, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x16, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x63, 0x68, + 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, + 0x16, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x05, 0x52, + 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6c, 0x61, 0x6d, 0x61, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, + 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0xbb, 0x03, 0x0a, 0x0c, 0x50, 0x72, 0x65, 0x70, + 0x61, 0x72, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x69, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x68, 0x69, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x61, + 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, + 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x65, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1a, + 0x0a, 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, + 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, + 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, + 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x64, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0e, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x4d, 0x73, 0x12, + 0x20, 0x0a, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x73, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0a, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x47, 0x65, 0x74, 0x4d, + 0x73, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, + 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xf0, 0x01, 0x0a, 0x0d, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x02, 0x66, 0x70, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, + 0x52, 0x02, 0x66, 0x70, 0x12, 0x2e, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x05, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x62, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6c, 0x61, 0x6d, 0x61, 0x54, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x70, 0x72, 0x65, 0x66, 0x69, 0x6c, 0x6c, 0x5f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x08, 0x70, 0x72, 0x65, 0x66, 0x69, 0x6c, 0x6c, 0x4e, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x65, + 0x66, 0x69, 0x6c, 0x6c, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x6c, 0x6c, 0x4d, 0x73, 0x22, 0x90, 0x02, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, + 0x6d, 0x69, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, + 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x42, + 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x17, 0x0a, 0x07, 0x73, 0x61, 0x76, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x01, 0x52, 0x06, 0x73, 0x61, 0x76, 0x65, 0x4d, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x5f, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, + 0x0a, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x50, 0x75, 0x74, 0x4d, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, + 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x0f, 0x0a, 0x0d, 0x48, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xab, 0x01, 0x0a, + 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x0e, 0x0a, 0x02, + 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, + 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x6f, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x4f, 0x6b, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xd1, 0x02, 0x0a, 0x0a, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x65, + 0x70, 0x61, 0x72, 0x65, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x0c, 0x70, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x68, 0x69, + 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x69, 0x73, 0x73, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x06, 0x6d, 0x69, 0x73, 0x73, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x68, 0x69, + 0x74, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, + 0x68, 0x69, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x69, 0x73, + 0x73, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, + 0x6d, 0x69, 0x73, 0x73, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x61, + 0x76, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x6c, 0x6c, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x73, 0x61, 0x76, 0x65, 0x64, 0x50, + 0x72, 0x65, 0x66, 0x69, 0x6c, 0x6c, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, + 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x67, 0x65, + 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x62, 0x79, 0x74, 0x65, 0x73, 0x47, 0x65, + 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x70, 0x75, 0x74, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x62, 0x79, 0x74, 0x65, 0x73, 0x50, 0x75, 0x74, 0x32, 0xb8, + 0x02, 0x0a, 0x0a, 0x4b, 0x56, 0x43, 0x61, 0x63, 0x68, 0x65, 0x42, 0x75, 0x73, 0x12, 0x3a, 0x0a, + 0x06, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x18, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x3a, 0x0a, 0x06, 0x4c, 0x6f, 0x6f, + 0x6b, 0x75, 0x70, 0x12, 0x18, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, + 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x3d, 0x0a, 0x07, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, + 0x12, 0x19, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, + 0x70, 0x61, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x62, 0x72, + 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x52, + 0x65, 0x70, 0x6c, 0x79, 0x12, 0x3a, 0x0a, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x18, + 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x12, 0x37, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x17, 0x2e, 0x62, 0x72, 0x69, 0x64, + 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x42, 0x4b, 0x5a, 0x49, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x6f, 0x6f, 0x6e, 0x63, 0x61, 0x6b, 0x65, + 0x2d, 0x61, 0x69, 0x2f, 0x6f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2d, 0x6d, 0x6f, 0x6f, 0x6e, 0x63, + 0x61, 0x6b, 0x65, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x70, 0x62, 0x3b, 0x62, 0x72, + 0x69, 0x64, 0x67, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_bridge_proto_rawDescOnce sync.Once + file_bridge_proto_rawDescData = file_bridge_proto_rawDesc +) + +func file_bridge_proto_rawDescGZIP() []byte { + file_bridge_proto_rawDescOnce.Do(func() { + file_bridge_proto_rawDescData = protoimpl.X.CompressGZIP(file_bridge_proto_rawDescData) + }) + return file_bridge_proto_rawDescData +} + +var file_bridge_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_bridge_proto_goTypes = []any{ + (*ModelFingerprint)(nil), // 0: bridge.v1.ModelFingerprint + (*CachePolicy)(nil), // 1: bridge.v1.CachePolicy + (*LlamaTarget)(nil), // 2: bridge.v1.LlamaTarget + (*LookupRequest)(nil), // 3: bridge.v1.LookupRequest + (*LookupReply)(nil), // 4: bridge.v1.LookupReply + (*PrepareRequest)(nil), // 5: bridge.v1.PrepareRequest + (*PrepareReply)(nil), // 6: bridge.v1.PrepareReply + (*CommitRequest)(nil), // 7: bridge.v1.CommitRequest + (*CommitReply)(nil), // 8: bridge.v1.CommitReply + (*HealthRequest)(nil), // 9: bridge.v1.HealthRequest + (*HealthReply)(nil), // 10: bridge.v1.HealthReply + (*StatsRequest)(nil), // 11: bridge.v1.StatsRequest + (*StatsReply)(nil), // 12: bridge.v1.StatsReply +} +var file_bridge_proto_depIdxs = []int32{ + 0, // 0: bridge.v1.LookupRequest.fp:type_name -> bridge.v1.ModelFingerprint + 1, // 1: bridge.v1.LookupRequest.policy:type_name -> bridge.v1.CachePolicy + 0, // 2: bridge.v1.PrepareRequest.fp:type_name -> bridge.v1.ModelFingerprint + 1, // 3: bridge.v1.PrepareRequest.policy:type_name -> bridge.v1.CachePolicy + 2, // 4: bridge.v1.PrepareRequest.target:type_name -> bridge.v1.LlamaTarget + 0, // 5: bridge.v1.CommitRequest.fp:type_name -> bridge.v1.ModelFingerprint + 1, // 6: bridge.v1.CommitRequest.policy:type_name -> bridge.v1.CachePolicy + 2, // 7: bridge.v1.CommitRequest.target:type_name -> bridge.v1.LlamaTarget + 9, // 8: bridge.v1.KVCacheBus.Health:input_type -> bridge.v1.HealthRequest + 3, // 9: bridge.v1.KVCacheBus.Lookup:input_type -> bridge.v1.LookupRequest + 5, // 10: bridge.v1.KVCacheBus.Prepare:input_type -> bridge.v1.PrepareRequest + 7, // 11: bridge.v1.KVCacheBus.Commit:input_type -> bridge.v1.CommitRequest + 11, // 12: bridge.v1.KVCacheBus.Stats:input_type -> bridge.v1.StatsRequest + 10, // 13: bridge.v1.KVCacheBus.Health:output_type -> bridge.v1.HealthReply + 4, // 14: bridge.v1.KVCacheBus.Lookup:output_type -> bridge.v1.LookupReply + 6, // 15: bridge.v1.KVCacheBus.Prepare:output_type -> bridge.v1.PrepareReply + 8, // 16: bridge.v1.KVCacheBus.Commit:output_type -> bridge.v1.CommitReply + 12, // 17: bridge.v1.KVCacheBus.Stats:output_type -> bridge.v1.StatsReply + 13, // [13:18] is the sub-list for method output_type + 8, // [8:13] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_bridge_proto_init() } +func file_bridge_proto_init() { + if File_bridge_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_bridge_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*ModelFingerprint); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*CachePolicy); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*LlamaTarget); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*LookupRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*LookupReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*PrepareRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*PrepareReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*CommitRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*CommitReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[9].Exporter = func(v any, i int) any { + switch v := v.(*HealthRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[10].Exporter = func(v any, i int) any { + switch v := v.(*HealthReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[11].Exporter = func(v any, i int) any { + switch v := v.(*StatsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[12].Exporter = func(v any, i int) any { + switch v := v.(*StatsReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_bridge_proto_rawDesc, + NumEnums: 0, + NumMessages: 13, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_bridge_proto_goTypes, + DependencyIndexes: file_bridge_proto_depIdxs, + MessageInfos: file_bridge_proto_msgTypes, + }.Build() + File_bridge_proto = out.File + file_bridge_proto_rawDesc = nil + file_bridge_proto_goTypes = nil + file_bridge_proto_depIdxs = nil +} diff --git a/mooncake-integration/ollama/bridge/internal/bridge/pb/bridge_grpc.pb.go b/mooncake-integration/ollama/bridge/internal/bridge/pb/bridge_grpc.pb.go new file mode 100644 index 00000000..41662985 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/bridge/pb/bridge_grpc.pb.go @@ -0,0 +1,278 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.4.0 +// - protoc v3.12.4 +// source: bridge.proto + +// KVCacheBus is the contract between a caller (a coding Agent, the bench +// harness, or patched Ollama) and the Go sidecar. It implements the three-stage +// reuse flow: Lookup (longest-prefix match) -> Prepare (Load/restore the matched +// KV into a llama.cpp slot) -> Commit (Save the produced KV back to the Mooncake +// Store). + +package bridgepb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.62.0 or later. +const _ = grpc.SupportPackageIsVersion8 + +const ( + KVCacheBus_Health_FullMethodName = "/bridge.v1.KVCacheBus/Health" + KVCacheBus_Lookup_FullMethodName = "/bridge.v1.KVCacheBus/Lookup" + KVCacheBus_Prepare_FullMethodName = "/bridge.v1.KVCacheBus/Prepare" + KVCacheBus_Commit_FullMethodName = "/bridge.v1.KVCacheBus/Commit" + KVCacheBus_Stats_FullMethodName = "/bridge.v1.KVCacheBus/Stats" +) + +// KVCacheBusClient is the client API for KVCacheBus service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type KVCacheBusClient interface { + Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthReply, error) + // Lookup is read-only: longest block-prefix match + arbiter decision, no I/O + // into llama.cpp. Useful for inspection / benchmarking. + Lookup(ctx context.Context, in *LookupRequest, opts ...grpc.CallOption) (*LookupReply, error) + // Prepare = Lookup + (if a hit and the cost arbiter approves) restore the + // matched KV prefix into target.slot so llama.cpp only prefills the tail. + Prepare(ctx context.Context, in *PrepareRequest, opts ...grpc.CallOption) (*PrepareReply, error) + // Commit = Save the KV currently held in target.slot and Put it to the store. + Commit(ctx context.Context, in *CommitRequest, opts ...grpc.CallOption) (*CommitReply, error) + Stats(ctx context.Context, in *StatsRequest, opts ...grpc.CallOption) (*StatsReply, error) +} + +type kVCacheBusClient struct { + cc grpc.ClientConnInterface +} + +func NewKVCacheBusClient(cc grpc.ClientConnInterface) KVCacheBusClient { + return &kVCacheBusClient{cc} +} + +func (c *kVCacheBusClient) Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HealthReply) + err := c.cc.Invoke(ctx, KVCacheBus_Health_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *kVCacheBusClient) Lookup(ctx context.Context, in *LookupRequest, opts ...grpc.CallOption) (*LookupReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LookupReply) + err := c.cc.Invoke(ctx, KVCacheBus_Lookup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *kVCacheBusClient) Prepare(ctx context.Context, in *PrepareRequest, opts ...grpc.CallOption) (*PrepareReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PrepareReply) + err := c.cc.Invoke(ctx, KVCacheBus_Prepare_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *kVCacheBusClient) Commit(ctx context.Context, in *CommitRequest, opts ...grpc.CallOption) (*CommitReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CommitReply) + err := c.cc.Invoke(ctx, KVCacheBus_Commit_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *kVCacheBusClient) Stats(ctx context.Context, in *StatsRequest, opts ...grpc.CallOption) (*StatsReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StatsReply) + err := c.cc.Invoke(ctx, KVCacheBus_Stats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// KVCacheBusServer is the server API for KVCacheBus service. +// All implementations must embed UnimplementedKVCacheBusServer +// for forward compatibility +type KVCacheBusServer interface { + Health(context.Context, *HealthRequest) (*HealthReply, error) + // Lookup is read-only: longest block-prefix match + arbiter decision, no I/O + // into llama.cpp. Useful for inspection / benchmarking. + Lookup(context.Context, *LookupRequest) (*LookupReply, error) + // Prepare = Lookup + (if a hit and the cost arbiter approves) restore the + // matched KV prefix into target.slot so llama.cpp only prefills the tail. + Prepare(context.Context, *PrepareRequest) (*PrepareReply, error) + // Commit = Save the KV currently held in target.slot and Put it to the store. + Commit(context.Context, *CommitRequest) (*CommitReply, error) + Stats(context.Context, *StatsRequest) (*StatsReply, error) + mustEmbedUnimplementedKVCacheBusServer() +} + +// UnimplementedKVCacheBusServer must be embedded to have forward compatible implementations. +type UnimplementedKVCacheBusServer struct { +} + +func (UnimplementedKVCacheBusServer) Health(context.Context, *HealthRequest) (*HealthReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Health not implemented") +} +func (UnimplementedKVCacheBusServer) Lookup(context.Context, *LookupRequest) (*LookupReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Lookup not implemented") +} +func (UnimplementedKVCacheBusServer) Prepare(context.Context, *PrepareRequest) (*PrepareReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Prepare not implemented") +} +func (UnimplementedKVCacheBusServer) Commit(context.Context, *CommitRequest) (*CommitReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Commit not implemented") +} +func (UnimplementedKVCacheBusServer) Stats(context.Context, *StatsRequest) (*StatsReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Stats not implemented") +} +func (UnimplementedKVCacheBusServer) mustEmbedUnimplementedKVCacheBusServer() {} + +// UnsafeKVCacheBusServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to KVCacheBusServer will +// result in compilation errors. +type UnsafeKVCacheBusServer interface { + mustEmbedUnimplementedKVCacheBusServer() +} + +func RegisterKVCacheBusServer(s grpc.ServiceRegistrar, srv KVCacheBusServer) { + s.RegisterService(&KVCacheBus_ServiceDesc, srv) +} + +func _KVCacheBus_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KVCacheBusServer).Health(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: KVCacheBus_Health_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KVCacheBusServer).Health(ctx, req.(*HealthRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _KVCacheBus_Lookup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LookupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KVCacheBusServer).Lookup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: KVCacheBus_Lookup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KVCacheBusServer).Lookup(ctx, req.(*LookupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _KVCacheBus_Prepare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PrepareRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KVCacheBusServer).Prepare(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: KVCacheBus_Prepare_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KVCacheBusServer).Prepare(ctx, req.(*PrepareRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _KVCacheBus_Commit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CommitRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KVCacheBusServer).Commit(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: KVCacheBus_Commit_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KVCacheBusServer).Commit(ctx, req.(*CommitRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _KVCacheBus_Stats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(KVCacheBusServer).Stats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: KVCacheBus_Stats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(KVCacheBusServer).Stats(ctx, req.(*StatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// KVCacheBus_ServiceDesc is the grpc.ServiceDesc for KVCacheBus service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var KVCacheBus_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "bridge.v1.KVCacheBus", + HandlerType: (*KVCacheBusServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Health", + Handler: _KVCacheBus_Health_Handler, + }, + { + MethodName: "Lookup", + Handler: _KVCacheBus_Lookup_Handler, + }, + { + MethodName: "Prepare", + Handler: _KVCacheBus_Prepare_Handler, + }, + { + MethodName: "Commit", + Handler: _KVCacheBus_Commit_Handler, + }, + { + MethodName: "Stats", + Handler: _KVCacheBus_Stats_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "bridge.proto", +} diff --git a/mooncake-integration/ollama/bridge/internal/store/local.go b/mooncake-integration/ollama/bridge/internal/store/local.go new file mode 100644 index 00000000..e73cc49b --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/store/local.go @@ -0,0 +1,118 @@ +package store + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "io" + "os" + "path/filepath" + "time" +) + +// LocalBackend is a pure-Go filesystem object store. It backs the "local file" +// baseline (cross-process, single-node, no Mooncake) and serves as a +// zero-dependency fallback. Keys are hashed to sharded file names. +type LocalBackend struct { + root string +} + +func NewLocalBackend(root string) (*LocalBackend, error) { + if err := os.MkdirAll(root, 0o755); err != nil { + return nil, err + } + return &LocalBackend{root: root}, nil +} + +func (l *LocalBackend) Name() string { return "local" } + +func (l *LocalBackend) path(key string) string { + h := sha256.Sum256([]byte(key)) + hs := hex.EncodeToString(h[:]) + return filepath.Join(l.root, hs[:2], hs) +} + +func (l *LocalBackend) Health(_ context.Context) (HealthInfo, error) { + return HealthInfo{OK: true, Backend: "local", Protocol: "file", Master: l.root, Detail: "filesystem store"}, nil +} + +func (l *LocalBackend) Exists(_ context.Context, keys []string) ([]int32, error) { + out := make([]int32, len(keys)) + for i, k := range keys { + if _, err := os.Stat(l.path(k)); err == nil { + out[i] = 1 + } + } + return out, nil +} + +func (l *LocalBackend) PutFile(_ context.Context, key, path string, _ uint32, _ bool, skipIfExists bool) (PutResult, error) { + dst := l.path(key) + if skipIfExists { + if fi, err := os.Stat(dst); err == nil { + return PutResult{Bytes: uint64(fi.Size()), Existed: true}, nil + } + } + t0 := time.Now() + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return PutResult{}, err + } + n, err := copyFile(path, dst) + if err != nil { + return PutResult{}, err + } + return PutResult{Bytes: uint64(n), ElapsedMs: float64(time.Since(t0).Microseconds()) / 1000.0}, nil +} + +func (l *LocalBackend) GetFile(_ context.Context, key, path string) (GetResult, error) { + src := l.path(key) + if _, err := os.Stat(src); err != nil { + return GetResult{Found: false}, nil + } + t0 := time.Now() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return GetResult{}, err + } + n, err := copyFile(src, path) + if err != nil { + return GetResult{}, err + } + return GetResult{Bytes: uint64(n), ElapsedMs: float64(time.Since(t0).Microseconds()) / 1000.0, Found: true}, nil +} + +func (l *LocalBackend) Remove(_ context.Context, key string, _ bool) error { + err := os.Remove(l.path(key)) + if os.IsNotExist(err) { + return nil + } + return err +} + +func (l *LocalBackend) Close() error { return nil } + +// copyFile writes src->dst atomically (via a .tmp + rename) and returns bytes. +func copyFile(src, dst string) (int64, error) { + in, err := os.Open(src) + if err != nil { + return 0, err + } + defer in.Close() + tmp := dst + ".tmp" + out, err := os.Create(tmp) + if err != nil { + return 0, err + } + n, err := io.Copy(out, in) + if cerr := out.Close(); err == nil { + err = cerr + } + if err != nil { + os.Remove(tmp) + return 0, err + } + if err := os.Rename(tmp, dst); err != nil { + os.Remove(tmp) + return 0, err + } + return n, nil +} diff --git a/mooncake-integration/ollama/bridge/internal/store/mooncake.go b/mooncake-integration/ollama/bridge/internal/store/mooncake.go new file mode 100644 index 00000000..5fa452f6 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/store/mooncake.go @@ -0,0 +1,110 @@ +package store + +import ( + "context" + "time" + + storeproxypb "github.com/mooncake-ai/ollama-mooncake-bridge/internal/storeproxy/pb" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// MooncakeBackend talks to the Python store proxy (which owns the real +// MooncakeDistributedStore handle) over gRPC. We pass file paths so KV blobs +// move file<->store inside the proxy, never across this gRPC link. +type MooncakeBackend struct { + conn *grpc.ClientConn + cli storeproxypb.StoreProxyClient + target string +} + +// DialMooncake connects to the store proxy at addr (host:port). +func DialMooncake(addr string) (*MooncakeBackend, error) { + conn, err := grpc.NewClient(addr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(512<<20), + grpc.MaxCallSendMsgSize(512<<20), + ), + ) + if err != nil { + return nil, err + } + return &MooncakeBackend{conn: conn, cli: storeproxypb.NewStoreProxyClient(conn), target: addr}, nil +} + +func (m *MooncakeBackend) Name() string { return "mooncake" } + +func (m *MooncakeBackend) Health(ctx context.Context) (HealthInfo, error) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + r, err := m.cli.Health(ctx, &storeproxypb.HealthRequest{}) + if err != nil { + return HealthInfo{}, err + } + return HealthInfo{OK: r.Ok, Backend: r.Backend, Protocol: r.Protocol, + Master: r.Master, Device: r.Device, Detail: r.Detail}, nil +} + +func (m *MooncakeBackend) Exists(ctx context.Context, keys []string) ([]int32, error) { + if len(keys) == 0 { + return nil, nil + } + r, err := m.cli.Exists(ctx, &storeproxypb.ExistsRequest{Keys: keys}) + if err != nil { + return nil, err + } + return r.Present, nil +} + +func (m *MooncakeBackend) PutFile(ctx context.Context, key, path string, replicaNum uint32, softPin, skipIfExists bool) (PutResult, error) { + r, err := m.cli.PutFile(ctx, &storeproxypb.PutFileRequest{ + Key: key, Path: path, ReplicaNum: replicaNum, SoftPin: softPin, SkipIfExists: skipIfExists, + }) + if err != nil { + return PutResult{}, err + } + if !r.Ok { + return PutResult{}, &Error{Op: "PutFile", Key: key, Msg: r.Error} + } + return PutResult{Bytes: r.Bytes, ElapsedMs: r.ElapsedMs, Existed: r.Existed}, nil +} + +func (m *MooncakeBackend) GetFile(ctx context.Context, key, path string) (GetResult, error) { + r, err := m.cli.GetFile(ctx, &storeproxypb.GetFileRequest{Key: key, Path: path}) + if err != nil { + return GetResult{}, err + } + if !r.Ok { + // A clean miss has found=false and no error message; anything with an + // error message is a real failure and must be surfaced, not hidden as a + // miss (which would degrade silently to a recompute). + if !r.Found && r.Error == "" { + return GetResult{Found: false}, nil + } + return GetResult{}, &Error{Op: "GetFile", Key: key, Msg: r.Error} + } + return GetResult{Bytes: r.Bytes, ElapsedMs: r.ElapsedMs, Found: r.Found}, nil +} + +func (m *MooncakeBackend) Remove(ctx context.Context, key string, force bool) error { + r, err := m.cli.Remove(ctx, &storeproxypb.RemoveRequest{Key: key, Force: force}) + if err != nil { + return err + } + if !r.Ok { + return &Error{Op: "Remove", Key: key, Msg: r.Error} + } + return nil +} + +func (m *MooncakeBackend) Close() error { return m.conn.Close() } + +// Error is a typed store error carrying op + key context. +type Error struct { + Op string + Key string + Msg string +} + +func (e *Error) Error() string { return e.Op + "(" + e.Key + "): " + e.Msg } diff --git a/mooncake-integration/ollama/bridge/internal/store/store.go b/mooncake-integration/ollama/bridge/internal/store/store.go new file mode 100644 index 00000000..1e22be8b --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/store/store.go @@ -0,0 +1,49 @@ +// Package store abstracts the KV-snapshot object store behind a small Backend +// interface so the orchestrator is agnostic to *where* bytes live. Two +// implementations ship: +// +// - mooncake: the real distributed store, reached via the Python store proxy +// over gRPC (TCP or RDMA/GPUDirect). This is the primary path. +// - local: a pure-Go filesystem object store (the "local file" baseline and a +// zero-dependency fallback when no proxy/master is available). +// +// Blobs are addressed by file path, never streamed through this process: a +// multi-GiB KV snapshot is copied at most once (file <-> store). +package store + +import "context" + +// PutResult / GetResult report what a transfer moved and how long it took, so +// the arbiter can learn live store bandwidth. +type PutResult struct { + Bytes uint64 + ElapsedMs float64 + Existed bool // skipped because the key already existed +} + +type GetResult struct { + Bytes uint64 + ElapsedMs float64 + Found bool +} + +type HealthInfo struct { + OK bool + Backend string + Protocol string + Master string + Device string + Detail string +} + +// Backend is the minimal object-store contract the orchestrator needs. +type Backend interface { + Name() string + Health(ctx context.Context) (HealthInfo, error) + // Exists returns one value per key: 1 present, 0 absent, -1 error. + Exists(ctx context.Context, keys []string) ([]int32, error) + PutFile(ctx context.Context, key, path string, replicaNum uint32, softPin, skipIfExists bool) (PutResult, error) + GetFile(ctx context.Context, key, path string) (GetResult, error) + Remove(ctx context.Context, key string, force bool) error + Close() error +} diff --git a/mooncake-integration/ollama/bridge/internal/storeproxy/pb/storeproxy.pb.go b/mooncake-integration/ollama/bridge/internal/storeproxy/pb/storeproxy.pb.go new file mode 100644 index 00000000..ef8fb50e --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/storeproxy/pb/storeproxy.pb.go @@ -0,0 +1,1479 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v3.12.4 +// source: storeproxy.proto + +// StoreProxy is the gRPC contract between the Go sidecar (client) and the +// Python Mooncake store proxy (server). The Python side owns the single, +// long-lived, warm mooncake.store.MooncakeDistributedStore handle. +// +// Design note: KV snapshots can be multiple GiB. We therefore pass *file paths* +// (in the shared --slot-save-path directory) instead of streaming bytes through +// Go. The proxy reads/writes those files directly to/from the store, so a +// multi-GiB KV blob is copied at most once (file <-> store), never through the +// Go process. Small values may still be sent inline via PutBytes/GetBytes. + +package storeproxypb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type HealthRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *HealthRequest) Reset() { + *x = HealthRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthRequest) ProtoMessage() {} + +func (x *HealthRequest) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthRequest.ProtoReflect.Descriptor instead. +func (*HealthRequest) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{0} +} + +type HealthReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Backend string `protobuf:"bytes,2,opt,name=backend,proto3" json:"backend,omitempty"` // "mooncake" | "local" + Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` // "tcp" | "rdma" + Master string `protobuf:"bytes,4,opt,name=master,proto3" json:"master,omitempty"` // master_server_addr + Device string `protobuf:"bytes,5,opt,name=device,proto3" json:"device,omitempty"` // rdma device(s) + Detail string `protobuf:"bytes,6,opt,name=detail,proto3" json:"detail,omitempty"` +} + +func (x *HealthReply) Reset() { + *x = HealthReply{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthReply) ProtoMessage() {} + +func (x *HealthReply) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthReply.ProtoReflect.Descriptor instead. +func (*HealthReply) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{1} +} + +func (x *HealthReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *HealthReply) GetBackend() string { + if x != nil { + return x.Backend + } + return "" +} + +func (x *HealthReply) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *HealthReply) GetMaster() string { + if x != nil { + return x.Master + } + return "" +} + +func (x *HealthReply) GetDevice() string { + if x != nil { + return x.Device + } + return "" +} + +func (x *HealthReply) GetDetail() string { + if x != nil { + return x.Detail + } + return "" +} + +type ExistsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Keys []string `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` +} + +func (x *ExistsRequest) Reset() { + *x = ExistsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExistsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExistsRequest) ProtoMessage() {} + +func (x *ExistsRequest) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExistsRequest.ProtoReflect.Descriptor instead. +func (*ExistsRequest) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{2} +} + +func (x *ExistsRequest) GetKeys() []string { + if x != nil { + return x.Keys + } + return nil +} + +// present[i]: 1 = exists, 0 = absent, -1 = error (mirrors batch_is_exist). +type ExistsReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Present []int32 `protobuf:"varint,1,rep,packed,name=present,proto3" json:"present,omitempty"` +} + +func (x *ExistsReply) Reset() { + *x = ExistsReply{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExistsReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExistsReply) ProtoMessage() {} + +func (x *ExistsReply) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExistsReply.ProtoReflect.Descriptor instead. +func (*ExistsReply) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{3} +} + +func (x *ExistsReply) GetPresent() []int32 { + if x != nil { + return x.Present + } + return nil +} + +type PutFileRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` // local file to read + ReplicaNum uint32 `protobuf:"varint,3,opt,name=replica_num,json=replicaNum,proto3" json:"replica_num,omitempty"` // ReplicateConfig.replica_num (>=1) + SoftPin bool `protobuf:"varint,4,opt,name=soft_pin,json=softPin,proto3" json:"soft_pin,omitempty"` // ReplicateConfig.with_soft_pin (hot prefixes) + SkipIfExists bool `protobuf:"varint,5,opt,name=skip_if_exists,json=skipIfExists,proto3" json:"skip_if_exists,omitempty"` +} + +func (x *PutFileRequest) Reset() { + *x = PutFileRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PutFileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutFileRequest) ProtoMessage() {} + +func (x *PutFileRequest) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutFileRequest.ProtoReflect.Descriptor instead. +func (*PutFileRequest) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{4} +} + +func (x *PutFileRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *PutFileRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *PutFileRequest) GetReplicaNum() uint32 { + if x != nil { + return x.ReplicaNum + } + return 0 +} + +func (x *PutFileRequest) GetSoftPin() bool { + if x != nil { + return x.SoftPin + } + return false +} + +func (x *PutFileRequest) GetSkipIfExists() bool { + if x != nil { + return x.SkipIfExists + } + return false +} + +type PutFileReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Bytes uint64 `protobuf:"varint,2,opt,name=bytes,proto3" json:"bytes,omitempty"` + ElapsedMs float64 `protobuf:"fixed64,3,opt,name=elapsed_ms,json=elapsedMs,proto3" json:"elapsed_ms,omitempty"` + Existed bool `protobuf:"varint,4,opt,name=existed,proto3" json:"existed,omitempty"` // true if skipped because key already present + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *PutFileReply) Reset() { + *x = PutFileReply{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PutFileReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutFileReply) ProtoMessage() {} + +func (x *PutFileReply) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutFileReply.ProtoReflect.Descriptor instead. +func (*PutFileReply) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{5} +} + +func (x *PutFileReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *PutFileReply) GetBytes() uint64 { + if x != nil { + return x.Bytes + } + return 0 +} + +func (x *PutFileReply) GetElapsedMs() float64 { + if x != nil { + return x.ElapsedMs + } + return 0 +} + +func (x *PutFileReply) GetExisted() bool { + if x != nil { + return x.Existed + } + return false +} + +func (x *PutFileReply) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type GetFileRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` // local file to write +} + +func (x *GetFileRequest) Reset() { + *x = GetFileRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetFileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFileRequest) ProtoMessage() {} + +func (x *GetFileRequest) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFileRequest.ProtoReflect.Descriptor instead. +func (*GetFileRequest) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{6} +} + +func (x *GetFileRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *GetFileRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type GetFileReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Bytes uint64 `protobuf:"varint,2,opt,name=bytes,proto3" json:"bytes,omitempty"` + ElapsedMs float64 `protobuf:"fixed64,3,opt,name=elapsed_ms,json=elapsedMs,proto3" json:"elapsed_ms,omitempty"` + Found bool `protobuf:"varint,4,opt,name=found,proto3" json:"found,omitempty"` + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *GetFileReply) Reset() { + *x = GetFileReply{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetFileReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFileReply) ProtoMessage() {} + +func (x *GetFileReply) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFileReply.ProtoReflect.Descriptor instead. +func (*GetFileReply) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{7} +} + +func (x *GetFileReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *GetFileReply) GetBytes() uint64 { + if x != nil { + return x.Bytes + } + return 0 +} + +func (x *GetFileReply) GetElapsedMs() float64 { + if x != nil { + return x.ElapsedMs + } + return 0 +} + +func (x *GetFileReply) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +func (x *GetFileReply) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type PutBytesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + ReplicaNum uint32 `protobuf:"varint,3,opt,name=replica_num,json=replicaNum,proto3" json:"replica_num,omitempty"` + SoftPin bool `protobuf:"varint,4,opt,name=soft_pin,json=softPin,proto3" json:"soft_pin,omitempty"` +} + +func (x *PutBytesRequest) Reset() { + *x = PutBytesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PutBytesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutBytesRequest) ProtoMessage() {} + +func (x *PutBytesRequest) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutBytesRequest.ProtoReflect.Descriptor instead. +func (*PutBytesRequest) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{8} +} + +func (x *PutBytesRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *PutBytesRequest) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *PutBytesRequest) GetReplicaNum() uint32 { + if x != nil { + return x.ReplicaNum + } + return 0 +} + +func (x *PutBytesRequest) GetSoftPin() bool { + if x != nil { + return x.SoftPin + } + return false +} + +type PutBytesReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Bytes uint64 `protobuf:"varint,2,opt,name=bytes,proto3" json:"bytes,omitempty"` + ElapsedMs float64 `protobuf:"fixed64,3,opt,name=elapsed_ms,json=elapsedMs,proto3" json:"elapsed_ms,omitempty"` + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *PutBytesReply) Reset() { + *x = PutBytesReply{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PutBytesReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutBytesReply) ProtoMessage() {} + +func (x *PutBytesReply) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutBytesReply.ProtoReflect.Descriptor instead. +func (*PutBytesReply) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{9} +} + +func (x *PutBytesReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *PutBytesReply) GetBytes() uint64 { + if x != nil { + return x.Bytes + } + return 0 +} + +func (x *PutBytesReply) GetElapsedMs() float64 { + if x != nil { + return x.ElapsedMs + } + return 0 +} + +func (x *PutBytesReply) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type GetBytesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` +} + +func (x *GetBytesRequest) Reset() { + *x = GetBytesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetBytesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBytesRequest) ProtoMessage() {} + +func (x *GetBytesRequest) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetBytesRequest.ProtoReflect.Descriptor instead. +func (*GetBytesRequest) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{10} +} + +func (x *GetBytesRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +type GetBytesReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + ElapsedMs float64 `protobuf:"fixed64,3,opt,name=elapsed_ms,json=elapsedMs,proto3" json:"elapsed_ms,omitempty"` + Found bool `protobuf:"varint,4,opt,name=found,proto3" json:"found,omitempty"` + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *GetBytesReply) Reset() { + *x = GetBytesReply{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetBytesReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBytesReply) ProtoMessage() {} + +func (x *GetBytesReply) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetBytesReply.ProtoReflect.Descriptor instead. +func (*GetBytesReply) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{11} +} + +func (x *GetBytesReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *GetBytesReply) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *GetBytesReply) GetElapsedMs() float64 { + if x != nil { + return x.ElapsedMs + } + return 0 +} + +func (x *GetBytesReply) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +func (x *GetBytesReply) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type RemoveRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Force bool `protobuf:"varint,2,opt,name=force,proto3" json:"force,omitempty"` +} + +func (x *RemoveRequest) Reset() { + *x = RemoveRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RemoveRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveRequest) ProtoMessage() {} + +func (x *RemoveRequest) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveRequest.ProtoReflect.Descriptor instead. +func (*RemoveRequest) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{12} +} + +func (x *RemoveRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *RemoveRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + +type RemoveReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *RemoveReply) Reset() { + *x = RemoveReply{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RemoveReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveReply) ProtoMessage() {} + +func (x *RemoveReply) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveReply.ProtoReflect.Descriptor instead. +func (*RemoveReply) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{13} +} + +func (x *RemoveReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *RemoveReply) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type StatsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *StatsRequest) Reset() { + *x = StatsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatsRequest) ProtoMessage() {} + +func (x *StatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatsRequest.ProtoReflect.Descriptor instead. +func (*StatsRequest) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{14} +} + +type StatsReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PutOps uint64 `protobuf:"varint,1,opt,name=put_ops,json=putOps,proto3" json:"put_ops,omitempty"` + GetOps uint64 `protobuf:"varint,2,opt,name=get_ops,json=getOps,proto3" json:"get_ops,omitempty"` + ExistsOps uint64 `protobuf:"varint,3,opt,name=exists_ops,json=existsOps,proto3" json:"exists_ops,omitempty"` + PutBytes uint64 `protobuf:"varint,4,opt,name=put_bytes,json=putBytes,proto3" json:"put_bytes,omitempty"` + GetBytes uint64 `protobuf:"varint,5,opt,name=get_bytes,json=getBytes,proto3" json:"get_bytes,omitempty"` + PutMsTotal float64 `protobuf:"fixed64,6,opt,name=put_ms_total,json=putMsTotal,proto3" json:"put_ms_total,omitempty"` + GetMsTotal float64 `protobuf:"fixed64,7,opt,name=get_ms_total,json=getMsTotal,proto3" json:"get_ms_total,omitempty"` + Backend string `protobuf:"bytes,8,opt,name=backend,proto3" json:"backend,omitempty"` +} + +func (x *StatsReply) Reset() { + *x = StatsReply{} + if protoimpl.UnsafeEnabled { + mi := &file_storeproxy_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatsReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatsReply) ProtoMessage() {} + +func (x *StatsReply) ProtoReflect() protoreflect.Message { + mi := &file_storeproxy_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatsReply.ProtoReflect.Descriptor instead. +func (*StatsReply) Descriptor() ([]byte, []int) { + return file_storeproxy_proto_rawDescGZIP(), []int{15} +} + +func (x *StatsReply) GetPutOps() uint64 { + if x != nil { + return x.PutOps + } + return 0 +} + +func (x *StatsReply) GetGetOps() uint64 { + if x != nil { + return x.GetOps + } + return 0 +} + +func (x *StatsReply) GetExistsOps() uint64 { + if x != nil { + return x.ExistsOps + } + return 0 +} + +func (x *StatsReply) GetPutBytes() uint64 { + if x != nil { + return x.PutBytes + } + return 0 +} + +func (x *StatsReply) GetGetBytes() uint64 { + if x != nil { + return x.GetBytes + } + return 0 +} + +func (x *StatsReply) GetPutMsTotal() float64 { + if x != nil { + return x.PutMsTotal + } + return 0 +} + +func (x *StatsReply) GetGetMsTotal() float64 { + if x != nil { + return x.GetMsTotal + } + return 0 +} + +func (x *StatsReply) GetBackend() string { + if x != nil { + return x.Backend + } + return "" +} + +var File_storeproxy_proto protoreflect.FileDescriptor + +var file_storeproxy_proto_rawDesc = []byte{ + 0x0a, 0x10, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, + 0x31, 0x22, 0x0f, 0x0a, 0x0d, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x9b, 0x01, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x70, + 0x6c, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, + 0x6f, 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x1a, 0x0a, 0x08, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61, 0x73, 0x74, + 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, + 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, + 0x69, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, + 0x22, 0x23, 0x0a, 0x0d, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x27, 0x0a, 0x0b, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, + 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x05, 0x52, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x22, 0x98, + 0x01, 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x72, 0x65, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x4e, 0x75, 0x6d, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x6f, 0x66, 0x74, + 0x5f, 0x70, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x6f, 0x66, 0x74, + 0x50, 0x69, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x69, 0x66, 0x5f, 0x65, + 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x6b, 0x69, + 0x70, 0x49, 0x66, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x83, 0x01, 0x0a, 0x0c, 0x50, 0x75, + 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6c, 0x61, 0x70, 0x73, 0x65, 0x64, 0x5f, 0x6d, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x65, 0x6c, 0x61, 0x70, 0x73, 0x65, 0x64, 0x4d, 0x73, 0x12, + 0x18, 0x0a, 0x07, 0x65, 0x78, 0x69, 0x73, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x65, 0x78, 0x69, 0x73, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, + 0x36, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x7f, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x46, 0x69, + 0x6c, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, + 0x0a, 0x65, 0x6c, 0x61, 0x70, 0x73, 0x65, 0x64, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x01, 0x52, 0x09, 0x65, 0x6c, 0x61, 0x70, 0x73, 0x65, 0x64, 0x4d, 0x73, 0x12, 0x14, 0x0a, 0x05, + 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x75, + 0x6e, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x75, 0x0a, 0x0f, 0x50, 0x75, 0x74, 0x42, + 0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x5f, 0x6e, + 0x75, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x4e, 0x75, 0x6d, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x6f, 0x66, 0x74, 0x5f, 0x70, 0x69, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x6f, 0x66, 0x74, 0x50, 0x69, 0x6e, 0x22, + 0x6a, 0x0a, 0x0d, 0x50, 0x75, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, + 0x12, 0x14, 0x0a, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6c, 0x61, 0x70, 0x73, 0x65, + 0x64, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x65, 0x6c, 0x61, 0x70, + 0x73, 0x65, 0x64, 0x4d, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x23, 0x0a, 0x0f, 0x47, + 0x65, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x22, 0x80, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x70, + 0x6c, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, + 0x6f, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6c, 0x61, 0x70, + 0x73, 0x65, 0x64, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x65, 0x6c, + 0x61, 0x70, 0x73, 0x65, 0x64, 0x4d, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x75, 0x6e, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x22, 0x37, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0x33, 0x0a, 0x0b, + 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x6f, + 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x22, 0x0e, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0xf5, 0x01, 0x0a, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x12, 0x17, 0x0a, 0x07, 0x70, 0x75, 0x74, 0x5f, 0x6f, 0x70, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x06, 0x70, 0x75, 0x74, 0x4f, 0x70, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x65, 0x74, + 0x5f, 0x6f, 0x70, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x67, 0x65, 0x74, 0x4f, + 0x70, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x5f, 0x6f, 0x70, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x4f, 0x70, + 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x70, 0x75, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1b, + 0x0a, 0x09, 0x67, 0x65, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x08, 0x67, 0x65, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x70, + 0x75, 0x74, 0x5f, 0x6d, 0x73, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x01, 0x52, 0x0a, 0x70, 0x75, 0x74, 0x4d, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x20, 0x0a, + 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x73, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x01, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x4d, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, + 0x18, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x32, 0xbb, 0x04, 0x0a, 0x0a, 0x53, 0x74, + 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x12, 0x42, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x12, 0x1c, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1a, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x31, + 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x42, 0x0a, 0x06, + 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x1c, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x12, 0x45, 0x0a, 0x07, 0x50, 0x75, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1d, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x74, 0x46, + 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x74, 0x46, 0x69, + 0x6c, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x45, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x46, 0x69, + 0x6c, 0x65, 0x12, 0x1d, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1b, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x48, + 0x0a, 0x08, 0x50, 0x75, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x79, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x79, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x48, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x42, + 0x79, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x70, + 0x6c, 0x79, 0x12, 0x42, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x12, 0x1c, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, + 0x6f, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x3f, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, + 0x1b, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, + 0x74, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x42, 0x53, 0x5a, 0x51, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x6f, 0x6f, 0x6e, 0x63, 0x61, 0x6b, 0x65, 0x2d, 0x61, + 0x69, 0x2f, 0x6f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2d, 0x6d, 0x6f, 0x6f, 0x6e, 0x63, 0x61, 0x6b, + 0x65, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x70, 0x62, 0x3b, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_storeproxy_proto_rawDescOnce sync.Once + file_storeproxy_proto_rawDescData = file_storeproxy_proto_rawDesc +) + +func file_storeproxy_proto_rawDescGZIP() []byte { + file_storeproxy_proto_rawDescOnce.Do(func() { + file_storeproxy_proto_rawDescData = protoimpl.X.CompressGZIP(file_storeproxy_proto_rawDescData) + }) + return file_storeproxy_proto_rawDescData +} + +var file_storeproxy_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_storeproxy_proto_goTypes = []any{ + (*HealthRequest)(nil), // 0: storeproxy.v1.HealthRequest + (*HealthReply)(nil), // 1: storeproxy.v1.HealthReply + (*ExistsRequest)(nil), // 2: storeproxy.v1.ExistsRequest + (*ExistsReply)(nil), // 3: storeproxy.v1.ExistsReply + (*PutFileRequest)(nil), // 4: storeproxy.v1.PutFileRequest + (*PutFileReply)(nil), // 5: storeproxy.v1.PutFileReply + (*GetFileRequest)(nil), // 6: storeproxy.v1.GetFileRequest + (*GetFileReply)(nil), // 7: storeproxy.v1.GetFileReply + (*PutBytesRequest)(nil), // 8: storeproxy.v1.PutBytesRequest + (*PutBytesReply)(nil), // 9: storeproxy.v1.PutBytesReply + (*GetBytesRequest)(nil), // 10: storeproxy.v1.GetBytesRequest + (*GetBytesReply)(nil), // 11: storeproxy.v1.GetBytesReply + (*RemoveRequest)(nil), // 12: storeproxy.v1.RemoveRequest + (*RemoveReply)(nil), // 13: storeproxy.v1.RemoveReply + (*StatsRequest)(nil), // 14: storeproxy.v1.StatsRequest + (*StatsReply)(nil), // 15: storeproxy.v1.StatsReply +} +var file_storeproxy_proto_depIdxs = []int32{ + 0, // 0: storeproxy.v1.StoreProxy.Health:input_type -> storeproxy.v1.HealthRequest + 2, // 1: storeproxy.v1.StoreProxy.Exists:input_type -> storeproxy.v1.ExistsRequest + 4, // 2: storeproxy.v1.StoreProxy.PutFile:input_type -> storeproxy.v1.PutFileRequest + 6, // 3: storeproxy.v1.StoreProxy.GetFile:input_type -> storeproxy.v1.GetFileRequest + 8, // 4: storeproxy.v1.StoreProxy.PutBytes:input_type -> storeproxy.v1.PutBytesRequest + 10, // 5: storeproxy.v1.StoreProxy.GetBytes:input_type -> storeproxy.v1.GetBytesRequest + 12, // 6: storeproxy.v1.StoreProxy.Remove:input_type -> storeproxy.v1.RemoveRequest + 14, // 7: storeproxy.v1.StoreProxy.Stats:input_type -> storeproxy.v1.StatsRequest + 1, // 8: storeproxy.v1.StoreProxy.Health:output_type -> storeproxy.v1.HealthReply + 3, // 9: storeproxy.v1.StoreProxy.Exists:output_type -> storeproxy.v1.ExistsReply + 5, // 10: storeproxy.v1.StoreProxy.PutFile:output_type -> storeproxy.v1.PutFileReply + 7, // 11: storeproxy.v1.StoreProxy.GetFile:output_type -> storeproxy.v1.GetFileReply + 9, // 12: storeproxy.v1.StoreProxy.PutBytes:output_type -> storeproxy.v1.PutBytesReply + 11, // 13: storeproxy.v1.StoreProxy.GetBytes:output_type -> storeproxy.v1.GetBytesReply + 13, // 14: storeproxy.v1.StoreProxy.Remove:output_type -> storeproxy.v1.RemoveReply + 15, // 15: storeproxy.v1.StoreProxy.Stats:output_type -> storeproxy.v1.StatsReply + 8, // [8:16] is the sub-list for method output_type + 0, // [0:8] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_storeproxy_proto_init() } +func file_storeproxy_proto_init() { + if File_storeproxy_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_storeproxy_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*HealthRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*HealthReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*ExistsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*ExistsReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*PutFileRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*PutFileReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*GetFileRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*GetFileReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*PutBytesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[9].Exporter = func(v any, i int) any { + switch v := v.(*PutBytesReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[10].Exporter = func(v any, i int) any { + switch v := v.(*GetBytesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[11].Exporter = func(v any, i int) any { + switch v := v.(*GetBytesReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[12].Exporter = func(v any, i int) any { + switch v := v.(*RemoveRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[13].Exporter = func(v any, i int) any { + switch v := v.(*RemoveReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[14].Exporter = func(v any, i int) any { + switch v := v.(*StatsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storeproxy_proto_msgTypes[15].Exporter = func(v any, i int) any { + switch v := v.(*StatsReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_storeproxy_proto_rawDesc, + NumEnums: 0, + NumMessages: 16, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_storeproxy_proto_goTypes, + DependencyIndexes: file_storeproxy_proto_depIdxs, + MessageInfos: file_storeproxy_proto_msgTypes, + }.Build() + File_storeproxy_proto = out.File + file_storeproxy_proto_rawDesc = nil + file_storeproxy_proto_goTypes = nil + file_storeproxy_proto_depIdxs = nil +} diff --git a/mooncake-integration/ollama/bridge/internal/storeproxy/pb/storeproxy_grpc.pb.go b/mooncake-integration/ollama/bridge/internal/storeproxy/pb/storeproxy_grpc.pb.go new file mode 100644 index 00000000..b6712c73 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/storeproxy/pb/storeproxy_grpc.pb.go @@ -0,0 +1,396 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.4.0 +// - protoc v3.12.4 +// source: storeproxy.proto + +// StoreProxy is the gRPC contract between the Go sidecar (client) and the +// Python Mooncake store proxy (server). The Python side owns the single, +// long-lived, warm mooncake.store.MooncakeDistributedStore handle. +// +// Design note: KV snapshots can be multiple GiB. We therefore pass *file paths* +// (in the shared --slot-save-path directory) instead of streaming bytes through +// Go. The proxy reads/writes those files directly to/from the store, so a +// multi-GiB KV blob is copied at most once (file <-> store), never through the +// Go process. Small values may still be sent inline via PutBytes/GetBytes. + +package storeproxypb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.62.0 or later. +const _ = grpc.SupportPackageIsVersion8 + +const ( + StoreProxy_Health_FullMethodName = "/storeproxy.v1.StoreProxy/Health" + StoreProxy_Exists_FullMethodName = "/storeproxy.v1.StoreProxy/Exists" + StoreProxy_PutFile_FullMethodName = "/storeproxy.v1.StoreProxy/PutFile" + StoreProxy_GetFile_FullMethodName = "/storeproxy.v1.StoreProxy/GetFile" + StoreProxy_PutBytes_FullMethodName = "/storeproxy.v1.StoreProxy/PutBytes" + StoreProxy_GetBytes_FullMethodName = "/storeproxy.v1.StoreProxy/GetBytes" + StoreProxy_Remove_FullMethodName = "/storeproxy.v1.StoreProxy/Remove" + StoreProxy_Stats_FullMethodName = "/storeproxy.v1.StoreProxy/Stats" +) + +// StoreProxyClient is the client API for StoreProxy service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type StoreProxyClient interface { + // Liveness + which backend/protocol/master is active. + Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthReply, error) + // Batched existence check (maps to mooncake batch_is_exist). + Exists(ctx context.Context, in *ExistsRequest, opts ...grpc.CallOption) (*ExistsReply, error) + // Store a KV snapshot file under `key`. + PutFile(ctx context.Context, in *PutFileRequest, opts ...grpc.CallOption) (*PutFileReply, error) + // Materialize `key` into a local file at `path`. + GetFile(ctx context.Context, in *GetFileRequest, opts ...grpc.CallOption) (*GetFileReply, error) + // Inline small-value variants (metadata, manifests). + PutBytes(ctx context.Context, in *PutBytesRequest, opts ...grpc.CallOption) (*PutBytesReply, error) + GetBytes(ctx context.Context, in *GetBytesRequest, opts ...grpc.CallOption) (*GetBytesReply, error) + Remove(ctx context.Context, in *RemoveRequest, opts ...grpc.CallOption) (*RemoveReply, error) + Stats(ctx context.Context, in *StatsRequest, opts ...grpc.CallOption) (*StatsReply, error) +} + +type storeProxyClient struct { + cc grpc.ClientConnInterface +} + +func NewStoreProxyClient(cc grpc.ClientConnInterface) StoreProxyClient { + return &storeProxyClient{cc} +} + +func (c *storeProxyClient) Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HealthReply) + err := c.cc.Invoke(ctx, StoreProxy_Health_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storeProxyClient) Exists(ctx context.Context, in *ExistsRequest, opts ...grpc.CallOption) (*ExistsReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExistsReply) + err := c.cc.Invoke(ctx, StoreProxy_Exists_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storeProxyClient) PutFile(ctx context.Context, in *PutFileRequest, opts ...grpc.CallOption) (*PutFileReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PutFileReply) + err := c.cc.Invoke(ctx, StoreProxy_PutFile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storeProxyClient) GetFile(ctx context.Context, in *GetFileRequest, opts ...grpc.CallOption) (*GetFileReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetFileReply) + err := c.cc.Invoke(ctx, StoreProxy_GetFile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storeProxyClient) PutBytes(ctx context.Context, in *PutBytesRequest, opts ...grpc.CallOption) (*PutBytesReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PutBytesReply) + err := c.cc.Invoke(ctx, StoreProxy_PutBytes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storeProxyClient) GetBytes(ctx context.Context, in *GetBytesRequest, opts ...grpc.CallOption) (*GetBytesReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetBytesReply) + err := c.cc.Invoke(ctx, StoreProxy_GetBytes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storeProxyClient) Remove(ctx context.Context, in *RemoveRequest, opts ...grpc.CallOption) (*RemoveReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveReply) + err := c.cc.Invoke(ctx, StoreProxy_Remove_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storeProxyClient) Stats(ctx context.Context, in *StatsRequest, opts ...grpc.CallOption) (*StatsReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StatsReply) + err := c.cc.Invoke(ctx, StoreProxy_Stats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// StoreProxyServer is the server API for StoreProxy service. +// All implementations must embed UnimplementedStoreProxyServer +// for forward compatibility +type StoreProxyServer interface { + // Liveness + which backend/protocol/master is active. + Health(context.Context, *HealthRequest) (*HealthReply, error) + // Batched existence check (maps to mooncake batch_is_exist). + Exists(context.Context, *ExistsRequest) (*ExistsReply, error) + // Store a KV snapshot file under `key`. + PutFile(context.Context, *PutFileRequest) (*PutFileReply, error) + // Materialize `key` into a local file at `path`. + GetFile(context.Context, *GetFileRequest) (*GetFileReply, error) + // Inline small-value variants (metadata, manifests). + PutBytes(context.Context, *PutBytesRequest) (*PutBytesReply, error) + GetBytes(context.Context, *GetBytesRequest) (*GetBytesReply, error) + Remove(context.Context, *RemoveRequest) (*RemoveReply, error) + Stats(context.Context, *StatsRequest) (*StatsReply, error) + mustEmbedUnimplementedStoreProxyServer() +} + +// UnimplementedStoreProxyServer must be embedded to have forward compatible implementations. +type UnimplementedStoreProxyServer struct { +} + +func (UnimplementedStoreProxyServer) Health(context.Context, *HealthRequest) (*HealthReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Health not implemented") +} +func (UnimplementedStoreProxyServer) Exists(context.Context, *ExistsRequest) (*ExistsReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Exists not implemented") +} +func (UnimplementedStoreProxyServer) PutFile(context.Context, *PutFileRequest) (*PutFileReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method PutFile not implemented") +} +func (UnimplementedStoreProxyServer) GetFile(context.Context, *GetFileRequest) (*GetFileReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetFile not implemented") +} +func (UnimplementedStoreProxyServer) PutBytes(context.Context, *PutBytesRequest) (*PutBytesReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method PutBytes not implemented") +} +func (UnimplementedStoreProxyServer) GetBytes(context.Context, *GetBytesRequest) (*GetBytesReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetBytes not implemented") +} +func (UnimplementedStoreProxyServer) Remove(context.Context, *RemoveRequest) (*RemoveReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Remove not implemented") +} +func (UnimplementedStoreProxyServer) Stats(context.Context, *StatsRequest) (*StatsReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Stats not implemented") +} +func (UnimplementedStoreProxyServer) mustEmbedUnimplementedStoreProxyServer() {} + +// UnsafeStoreProxyServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to StoreProxyServer will +// result in compilation errors. +type UnsafeStoreProxyServer interface { + mustEmbedUnimplementedStoreProxyServer() +} + +func RegisterStoreProxyServer(s grpc.ServiceRegistrar, srv StoreProxyServer) { + s.RegisterService(&StoreProxy_ServiceDesc, srv) +} + +func _StoreProxy_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StoreProxyServer).Health(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StoreProxy_Health_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StoreProxyServer).Health(ctx, req.(*HealthRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StoreProxy_Exists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExistsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StoreProxyServer).Exists(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StoreProxy_Exists_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StoreProxyServer).Exists(ctx, req.(*ExistsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StoreProxy_PutFile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PutFileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StoreProxyServer).PutFile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StoreProxy_PutFile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StoreProxyServer).PutFile(ctx, req.(*PutFileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StoreProxy_GetFile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetFileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StoreProxyServer).GetFile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StoreProxy_GetFile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StoreProxyServer).GetFile(ctx, req.(*GetFileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StoreProxy_PutBytes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PutBytesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StoreProxyServer).PutBytes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StoreProxy_PutBytes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StoreProxyServer).PutBytes(ctx, req.(*PutBytesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StoreProxy_GetBytes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetBytesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StoreProxyServer).GetBytes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StoreProxy_GetBytes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StoreProxyServer).GetBytes(ctx, req.(*GetBytesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StoreProxy_Remove_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StoreProxyServer).Remove(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StoreProxy_Remove_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StoreProxyServer).Remove(ctx, req.(*RemoveRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StoreProxy_Stats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StoreProxyServer).Stats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StoreProxy_Stats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StoreProxyServer).Stats(ctx, req.(*StatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// StoreProxy_ServiceDesc is the grpc.ServiceDesc for StoreProxy service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var StoreProxy_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "storeproxy.v1.StoreProxy", + HandlerType: (*StoreProxyServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Health", + Handler: _StoreProxy_Health_Handler, + }, + { + MethodName: "Exists", + Handler: _StoreProxy_Exists_Handler, + }, + { + MethodName: "PutFile", + Handler: _StoreProxy_PutFile_Handler, + }, + { + MethodName: "GetFile", + Handler: _StoreProxy_GetFile_Handler, + }, + { + MethodName: "PutBytes", + Handler: _StoreProxy_PutBytes_Handler, + }, + { + MethodName: "GetBytes", + Handler: _StoreProxy_GetBytes_Handler, + }, + { + MethodName: "Remove", + Handler: _StoreProxy_Remove_Handler, + }, + { + MethodName: "Stats", + Handler: _StoreProxy_Stats_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "storeproxy.proto", +} diff --git a/mooncake-integration/ollama/proto/storeproxy.proto b/mooncake-integration/ollama/proto/storeproxy.proto new file mode 100644 index 00000000..764f9948 --- /dev/null +++ b/mooncake-integration/ollama/proto/storeproxy.proto @@ -0,0 +1,97 @@ +syntax = "proto3"; + +// StoreProxy is the gRPC contract between the Go sidecar (client) and the +// Python Mooncake store proxy (server). The Python side owns the single, +// long-lived, warm mooncake.store.MooncakeDistributedStore handle. +// +// Design note: KV snapshots can be multiple GiB. We therefore pass *file paths* +// (in the shared --slot-save-path directory) instead of streaming bytes through +// Go. The proxy reads/writes those files directly to/from the store, so a +// multi-GiB KV blob is copied at most once (file <-> store), never through the +// Go process. Small values may still be sent inline via PutBytes/GetBytes. +package storeproxy.v1; + +option go_package = "github.com/mooncake-ai/ollama-mooncake-bridge/internal/storeproxy/pb;storeproxypb"; + +service StoreProxy { + // Liveness + which backend/protocol/master is active. + rpc Health(HealthRequest) returns (HealthReply); + // Batched existence check (maps to mooncake batch_is_exist). + rpc Exists(ExistsRequest) returns (ExistsReply); + // Store a KV snapshot file under `key`. + rpc PutFile(PutFileRequest) returns (PutFileReply); + // Materialize `key` into a local file at `path`. + rpc GetFile(GetFileRequest) returns (GetFileReply); + // Inline small-value variants (metadata, manifests). + rpc PutBytes(PutBytesRequest) returns (PutBytesReply); + rpc GetBytes(GetBytesRequest) returns (GetBytesReply); + rpc Remove(RemoveRequest) returns (RemoveReply); + rpc Stats(StatsRequest) returns (StatsReply); +} + +message HealthRequest {} +message HealthReply { + bool ok = 1; + string backend = 2; // "mooncake" | "local" + string protocol = 3; // "tcp" | "rdma" + string master = 4; // master_server_addr + string device = 5; // rdma device(s) + string detail = 6; +} + +message ExistsRequest { repeated string keys = 1; } +// present[i]: 1 = exists, 0 = absent, -1 = error (mirrors batch_is_exist). +message ExistsReply { repeated int32 present = 1; } + +message PutFileRequest { + string key = 1; + string path = 2; // local file to read + uint32 replica_num = 3; // ReplicateConfig.replica_num (>=1) + bool soft_pin = 4; // ReplicateConfig.with_soft_pin (hot prefixes) + bool skip_if_exists = 5; +} +message PutFileReply { + bool ok = 1; + uint64 bytes = 2; + double elapsed_ms = 3; + bool existed = 4; // true if skipped because key already present + string error = 5; +} + +message GetFileRequest { + string key = 1; + string path = 2; // local file to write +} +message GetFileReply { + bool ok = 1; + uint64 bytes = 2; + double elapsed_ms = 3; + bool found = 4; + string error = 5; +} + +message PutBytesRequest { + string key = 1; + bytes value = 2; + uint32 replica_num = 3; + bool soft_pin = 4; +} +message PutBytesReply { bool ok = 1; uint64 bytes = 2; double elapsed_ms = 3; string error = 4; } + +message GetBytesRequest { string key = 1; } +message GetBytesReply { bool ok = 1; bytes value = 2; double elapsed_ms = 3; bool found = 4; string error = 5; } + +message RemoveRequest { string key = 1; bool force = 2; } +message RemoveReply { bool ok = 1; string error = 2; } + +message StatsRequest {} +message StatsReply { + uint64 put_ops = 1; + uint64 get_ops = 2; + uint64 exists_ops = 3; + uint64 put_bytes = 4; + uint64 get_bytes = 5; + double put_ms_total = 6; + double get_ms_total = 7; + string backend = 8; +} -- 2.34.1 From 0b208be4e6868f139d667af78082c147f0fb1f1a Mon Sep 17 00:00:00 2001 From: zbtrs2 Date: Mon, 29 Jun 2026 01:50:44 +0800 Subject: [PATCH 3/9] ollama-kvcache: add sidecar orchestrator, llama.cpp client and server Wire the pieces into the bridged daemon. orchestrator implements the three-stage reuse flow. Lookup builds the per-block keys and finds the longest stored prefix with a single batched existence query (correct across nodes). Prepare additionally restores the matched KV into a target llama.cpp slot when the arbiter approves, so the server only prefills the uncached tail. Commit saves the slot KV and stores it under the block key for the largest block-aligned prefix, with skip-if-exists giving single-writer dedup when concurrent agents share a prefix. It feeds the arbiter the full restore wall time and the observed prefill rate so estimates self-calibrate, and tracks per-model KV bytes/token for transfer sizing. llamabridge drives the stock llama.cpp server over HTTP: /tokenize, /completion (reading timings.prompt_n to count avoided prefill), /slots save|restore|erase and /props. No llama.cpp source change is needed for this path. metrics exposes Prometheus instrumentation; the headline series is the cumulative count of prompt tokens not re-prefilled thanks to reuse. server adapts the orchestrator onto the gRPC KVCacheBus and an HTTP/JSON gateway, and cmd/bridged is the daemon entrypoint serving gRPC (TCP and unix socket), the JSON gateway and /metrics. --- .../ollama/bridge/cmd/bridged/main.go | 151 ++++++ .../bridge/internal/llamabridge/slots_http.go | 205 ++++++++ .../ollama/bridge/internal/metrics/prom.go | 95 ++++ .../bridge/internal/orchestrator/reuse.go | 494 ++++++++++++++++++ .../ollama/bridge/internal/server/grpc.go | 151 ++++++ .../ollama/bridge/internal/server/http.go | 211 ++++++++ 6 files changed, 1307 insertions(+) create mode 100644 mooncake-integration/ollama/bridge/cmd/bridged/main.go create mode 100644 mooncake-integration/ollama/bridge/internal/llamabridge/slots_http.go create mode 100644 mooncake-integration/ollama/bridge/internal/metrics/prom.go create mode 100644 mooncake-integration/ollama/bridge/internal/orchestrator/reuse.go create mode 100644 mooncake-integration/ollama/bridge/internal/server/grpc.go create mode 100644 mooncake-integration/ollama/bridge/internal/server/http.go diff --git a/mooncake-integration/ollama/bridge/cmd/bridged/main.go b/mooncake-integration/ollama/bridge/cmd/bridged/main.go new file mode 100644 index 00000000..055bf7ad --- /dev/null +++ b/mooncake-integration/ollama/bridge/cmd/bridged/main.go @@ -0,0 +1,151 @@ +// Command bridged is the ollama-mooncake-bridge sidecar daemon. It serves the +// KVCacheBus over gRPC (TCP + optional Unix socket) and an HTTP/JSON gateway +// (with Prometheus /metrics), backed by either the real Mooncake Store (via the +// Python store proxy) or a local filesystem store. +package main + +import ( + "context" + "flag" + "log" + "net" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + bridgepb "github.com/mooncake-ai/ollama-mooncake-bridge/internal/bridge/pb" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/arbiter" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/metrics" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/orchestrator" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/server" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/store" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" +) + +func main() { + var ( + grpcAddr = flag.String("grpc-addr", "127.0.0.1:52051", "gRPC listen address") + grpcUnix = flag.String("grpc-unix", "", "optional gRPC unix socket path") + httpAddr = flag.String("http-addr", "127.0.0.1:52052", "HTTP/JSON + /metrics listen address") + backend = flag.String("store-backend", "mooncake", "store backend: mooncake | local") + proxyAddr = flag.String("store-proxy-addr", "127.0.0.1:52060", "mooncake store proxy gRPC address") + localDir = flag.String("local-store-dir", "./run/store-local", "local backend directory") + slotSavePath = flag.String("slot-save-path", "./run/slots/", "llama.cpp --slot-save-path (shared FS, trailing slash)") + blockSize = flag.Int("block-size", 256, "default cache block size (tokens)") + replicaNum = flag.Uint("replica-num", 1, "default store replica count") + minPrefix = flag.Int("min-prefix-blocks", 1, "do not cache prefixes shorter than this many blocks") + cleanup = flag.Bool("cleanup-files", true, "delete local slot files after store transfer") + priorGBps = flag.Float64("prior-get-gbps", 8.0, "arbiter prior: store read GB/s") + priorTPS = flag.Float64("prior-prefill-tps", 8000, "arbiter prior: prefill tokens/s") + minTokens = flag.Int("arb-min-tokens", 256, "arbiter: minimum matched tokens to bother restoring") + safety = flag.Float64("arb-safety", 0.9, "arbiter: restore only if est_restore < safety*est_recompute") + ) + flag.Parse() + log.SetFlags(log.LstdFlags | log.Lmicroseconds) + + // Store backend. + var be store.Backend + var err error + switch *backend { + case "local": + be, err = store.NewLocalBackend(*localDir) + case "mooncake": + be, err = store.DialMooncake(*proxyAddr) + default: + log.Fatalf("unknown store backend %q", *backend) + } + if err != nil { + log.Fatalf("store backend init: %v", err) + } + defer be.Close() + + // Probe store health (non-fatal: log and continue so /healthz reflects it). + { + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) + if hi, herr := be.Health(ctx); herr != nil { + log.Printf("WARNING store health: %v", herr) + } else { + log.Printf("store backend=%s protocol=%s master=%s device=%s", hi.Backend, hi.Protocol, hi.Master, hi.Device) + } + cancel() + } + + reg := prometheus.NewRegistry() + mx := metrics.New(reg) + arb := arbiter.New(arbiter.Config{ + PriorGetGBps: *priorGBps, PriorPrefillToksS: *priorTPS, PriorRestoreMsFix: 8, + MinTokens: *minTokens, SafetyMargin: *safety, Alpha: 0.3, + }) + orch := orchestrator.New(be, arb, mx, orchestrator.Config{ + SlotSavePath: *slotSavePath, DefaultBlockSize: *blockSize, + DefaultReplicaNum: uint32(*replicaNum), MinPrefixBlocks: *minPrefix, CleanupFiles: *cleanup, + }) + + // gRPC server. + gs := grpc.NewServer( + grpc.MaxRecvMsgSize(256<<20), + grpc.MaxSendMsgSize(256<<20), + ) + bridgepb.RegisterKVCacheBusServer(gs, server.NewGRPC(orch, be, mx)) + + var listeners []net.Listener + tl, err := net.Listen("tcp", *grpcAddr) + if err != nil { + log.Fatalf("grpc tcp listen: %v", err) + } + listeners = append(listeners, tl) + if *grpcUnix != "" { + os.Remove(*grpcUnix) + ul, uerr := net.Listen("unix", *grpcUnix) + if uerr != nil { + log.Fatalf("grpc unix listen: %v", uerr) + } + listeners = append(listeners, ul) + } + for _, l := range listeners { + go func(l net.Listener) { + log.Printf("gRPC KVCacheBus on %s", l.Addr()) + if serr := gs.Serve(l); serr != nil { + log.Printf("grpc serve(%s) stopped: %v", l.Addr(), serr) + } + }(l) + } + + // HTTP gateway + metrics. + hs := server.NewHTTP(orch, be, mx, reg) + httpSrv := &http.Server{Addr: *httpAddr, Handler: hs.Mux()} + go func() { + log.Printf("HTTP/JSON + /metrics on %s", *httpAddr) + if herr := httpSrv.ListenAndServe(); herr != nil && herr != http.ErrServerClosed { + log.Printf("http serve stopped: %v", herr) + } + }() + + // periodic gauge refresh so /metrics shows learned rates even when idle + stop := make(chan struct{}) + go func() { + t := time.NewTicker(2 * time.Second) + defer t.Stop() + for { + select { + case <-stop: + return + case <-t.C: + orch.RefreshGauges() + } + } + }() + + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig + log.Printf("shutting down...") + close(stop) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + _ = httpSrv.Shutdown(ctx) + gs.GracefulStop() +} diff --git a/mooncake-integration/ollama/bridge/internal/llamabridge/slots_http.go b/mooncake-integration/ollama/bridge/internal/llamabridge/slots_http.go new file mode 100644 index 00000000..fd023965 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/llamabridge/slots_http.go @@ -0,0 +1,205 @@ +// Package llamabridge is the Stage-1 client for a running llama.cpp server. It +// drives exactly the endpoints the reuse flow relies on: +// +// POST /tokenize -> token ids (so we can hash blocks) +// POST /completion {cache_prompt,...} -> content + timings.prompt_n (prefilled) +// POST /slots/{id}?action=save -> {n_saved, n_written} +// POST /slots/{id}?action=restore -> {n_restored, n_read} +// POST /slots/{id}?action=erase +// GET /props -> model/runtime info for fingerprinting +// +// The save file lands in the server's --slot-save-path; the sidecar then ships +// that file to the Mooncake Store (and vice-versa on restore). No llama.cpp +// source changes are required for Stage 1. +package llamabridge + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type Client struct { + hc *http.Client +} + +func New() *Client { + return &Client{hc: &http.Client{Timeout: 0}} // long ops (big prefills) use ctx deadlines +} + +func (c *Client) postJSON(ctx context.Context, url string, body any, out any) error { + var buf bytes.Buffer + if body != nil { + if err := json.NewEncoder(&buf).Encode(body); err != nil { + return err + } + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + resp, err := c.hc.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + data, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("%s -> %d: %s", url, resp.StatusCode, truncate(string(data), 256)) + } + if out != nil { + if err := json.Unmarshal(data, out); err != nil { + return fmt.Errorf("decode %s: %w (body=%s)", url, err, truncate(string(data), 256)) + } + } + return nil +} + +func (c *Client) getJSON(ctx context.Context, url string, out any) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return err + } + resp, err := c.hc.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + data, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("%s -> %d: %s", url, resp.StatusCode, truncate(string(data), 256)) + } + return json.Unmarshal(data, out) +} + +// Tokenize returns the token ids for text. add_special controls BOS/EOS. +func (c *Client) Tokenize(ctx context.Context, baseURL, text string, addSpecial bool) ([]int32, error) { + var out struct { + Tokens []int32 `json:"tokens"` + } + err := c.postJSON(ctx, baseURL+"/tokenize", map[string]any{ + "content": text, "add_special": addSpecial, "with_pieces": false, + }, &out) + return out.Tokens, err +} + +// CompletionTimings mirrors llama.cpp's timings object. +type CompletionTimings struct { + PromptN int `json:"prompt_n"` // tokens actually prefilled (NOT served from cache) + PromptMs float64 `json:"prompt_ms"` // prefill wall time + PredictedN int `json:"predicted_n"` // tokens generated + PredictedMs float64 `json:"predicted_ms"` // generation wall time +} + +type CompletionResult struct { + Content string `json:"content"` + Timings CompletionTimings `json:"timings"` + TokensEvaluated int `json:"tokens_evaluated"` + Truncated bool `json:"truncated"` +} + +// CompletionRequest is the subset we use. Prompt may be a string or []int32. +type CompletionRequest struct { + Prompt any `json:"prompt"` + IDSlot int `json:"id_slot"` + CachePrompt bool `json:"cache_prompt"` + NPredict int `json:"n_predict"` + Temperature float64 `json:"temperature"` + Seed int `json:"seed,omitempty"` + Stream bool `json:"stream"` +} + +func (c *Client) Completion(ctx context.Context, baseURL string, req CompletionRequest) (*CompletionResult, error) { + req.Stream = false + var out CompletionResult + if err := c.postJSON(ctx, baseURL+"/completion", req, &out); err != nil { + return nil, err + } + return &out, nil +} + +// SaveResult / RestoreResult expose token + byte counts. +type SaveResult struct { + IDSlot int `json:"id_slot"` + Filename string `json:"filename"` + NSaved int `json:"n_saved"` // tokens + NWritten uint64 `json:"n_written"` // bytes +} +type RestoreResult struct { + IDSlot int `json:"id_slot"` + Filename string `json:"filename"` + NRestored int `json:"n_restored"` // tokens + NRead uint64 `json:"n_read"` // bytes +} + +func (c *Client) SaveSlot(ctx context.Context, baseURL string, slot int, filename string) (*SaveResult, error) { + url := fmt.Sprintf("%s/slots/%d?action=save", baseURL, slot) + var out SaveResult + if err := c.postJSON(ctx, url, map[string]any{"filename": filename}, &out); err != nil { + return nil, err + } + return &out, nil +} + +func (c *Client) RestoreSlot(ctx context.Context, baseURL string, slot int, filename string) (*RestoreResult, error) { + url := fmt.Sprintf("%s/slots/%d?action=restore", baseURL, slot) + var out RestoreResult + if err := c.postJSON(ctx, url, map[string]any{"filename": filename}, &out); err != nil { + return nil, err + } + return &out, nil +} + +func (c *Client) EraseSlot(ctx context.Context, baseURL string, slot int) error { + url := fmt.Sprintf("%s/slots/%d?action=erase", baseURL, slot) + return c.postJSON(ctx, url, map[string]any{}, nil) +} + +// Props is a subset of GET /props used for fingerprinting / health. +type Props struct { + DefaultGenerationSettings struct { + NCtx int `json:"n_ctx"` + } `json:"default_generation_settings"` + ModelPath string `json:"model_path"` + TotalSlots int `json:"total_slots"` +} + +func (c *Client) GetProps(ctx context.Context, baseURL string) (*Props, error) { + var out Props + if err := c.getJSON(ctx, baseURL+"/props", &out); err != nil { + return nil, err + } + return &out, nil +} + +// WaitHealthy polls /health until ready or ctx expires. +func (c *Client) WaitHealthy(ctx context.Context, baseURL string) error { + for { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/health", nil) + resp, err := c.hc.Do(req) + if err == nil { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return nil + } + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(300 * time.Millisecond): + } + } +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} diff --git a/mooncake-integration/ollama/bridge/internal/metrics/prom.go b/mooncake-integration/ollama/bridge/internal/metrics/prom.go new file mode 100644 index 00000000..a8913069 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/metrics/prom.go @@ -0,0 +1,95 @@ +// Package metrics exposes the sidecar's Prometheus instrumentation. The primary +// series is mooncake_bridge_saved_prefill_tokens_total — the number of prompt +// tokens that did NOT have to be re-prefilled because their KV was restored from +// the Mooncake Store. A Grafana panel integrates this live. +package metrics + +import ( + "sync/atomic" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type Metrics struct { + LookupTotal prometheus.Counter + PrepareTotal prometheus.Counter + CommitTotal prometheus.Counter + Hits prometheus.Counter + Misses prometheus.Counter + HitBlocks prometheus.Counter + MissBlocks prometheus.Counter + RestoreCount prometheus.Counter + RecomputeChosen prometheus.Counter + BytesGet prometheus.Counter + BytesPut prometheus.Counter + SavedPrefillTok prometheus.Counter + + RestoreLatency prometheus.Histogram + SaveLatency prometheus.Histogram + StoreGetLatency prometheus.Histogram + StorePutLatency prometheus.Histogram + + GetGBps prometheus.Gauge + PrefillToksS prometheus.Gauge + IndexSnapshots prometheus.Gauge + IndexBytes prometheus.Gauge + + // plain atomics mirrored into Stats RPC + savedTokens uint64 + bytesGet uint64 + bytesPut uint64 +} + +func New(reg prometheus.Registerer) *Metrics { + af := promauto.With(reg) + lat := func(name, help string) prometheus.Histogram { + return af.NewHistogram(prometheus.HistogramOpts{ + Name: name, + Help: help, + Buckets: []float64{0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000}, + }) + } + ctr := func(name, help string) prometheus.Counter { + return af.NewCounter(prometheus.CounterOpts{Name: name, Help: help}) + } + g := func(name, help string) prometheus.Gauge { + return af.NewGauge(prometheus.GaugeOpts{Name: name, Help: help}) + } + return &Metrics{ + LookupTotal: ctr("mooncake_bridge_lookup_total", "Lookup/Prepare requests served"), + PrepareTotal: ctr("mooncake_bridge_prepare_total", "Prepare requests served"), + CommitTotal: ctr("mooncake_bridge_commit_total", "Commit (save) requests served"), + Hits: ctr("mooncake_bridge_hits_total", "Requests with a usable cached prefix"), + Misses: ctr("mooncake_bridge_misses_total", "Requests with no usable cached prefix"), + HitBlocks: ctr("mooncake_bridge_hit_blocks_total", "Blocks served from cache"), + MissBlocks: ctr("mooncake_bridge_miss_blocks_total", "Blocks that had to be (re)computed"), + RestoreCount: ctr("mooncake_bridge_restore_total", "KV restores performed"), + RecomputeChosen: ctr("mooncake_bridge_recompute_chosen_total", "Times the arbiter chose recompute over restore"), + BytesGet: ctr("mooncake_bridge_bytes_get_total", "Bytes read from the store"), + BytesPut: ctr("mooncake_bridge_bytes_put_total", "Bytes written to the store"), + SavedPrefillTok: ctr("mooncake_bridge_saved_prefill_tokens_total", "Prompt tokens NOT re-prefilled thanks to KV reuse"), + RestoreLatency: lat("mooncake_bridge_restore_latency_ms", "llama.cpp /slots restore latency (ms)"), + SaveLatency: lat("mooncake_bridge_save_latency_ms", "llama.cpp /slots save latency (ms)"), + StoreGetLatency: lat("mooncake_bridge_store_get_latency_ms", "Store GetFile latency (ms)"), + StorePutLatency: lat("mooncake_bridge_store_put_latency_ms", "Store PutFile latency (ms)"), + GetGBps: g("mooncake_bridge_learned_get_gbps", "Arbiter's learned store read bandwidth (GB/s)"), + PrefillToksS: g("mooncake_bridge_learned_prefill_tps", "Arbiter's learned prefill rate (tokens/s)"), + IndexSnapshots: g("mooncake_bridge_index_snapshots", "KV prefix snapshots tracked in the radix index"), + IndexBytes: g("mooncake_bridge_index_bytes", "Total bytes of snapshots tracked"), + } +} + +func (m *Metrics) AddSavedTokens(n int) { + if n <= 0 { + return + } + m.SavedPrefillTok.Add(float64(n)) + atomic.AddUint64(&m.savedTokens, uint64(n)) +} +func (m *Metrics) AddBytesGet(n uint64) { m.BytesGet.Add(float64(n)); atomic.AddUint64(&m.bytesGet, n) } +func (m *Metrics) AddBytesPut(n uint64) { m.BytesPut.Add(float64(n)); atomic.AddUint64(&m.bytesPut, n) } + +func (m *Metrics) SavedTokens() uint64 { return atomic.LoadUint64(&m.savedTokens) } +func (m *Metrics) TotalBytesGet() uint64 { return atomic.LoadUint64(&m.bytesGet) } +func (m *Metrics) TotalBytesPut() uint64 { return atomic.LoadUint64(&m.bytesPut) } diff --git a/mooncake-integration/ollama/bridge/internal/orchestrator/reuse.go b/mooncake-integration/ollama/bridge/internal/orchestrator/reuse.go new file mode 100644 index 00000000..616e5cb0 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/orchestrator/reuse.go @@ -0,0 +1,494 @@ +// Package orchestrator implements the three-stage KV reuse flow: +// +// Lookup compute block-prefix keys, find the longest prefix present in the +// store (authoritative, batched), and consult the radix index. +// Prepare Lookup + (if the cost arbiter approves) GetFile the matched KV and +// restore it into a llama.cpp slot, so only the tail is prefilled. +// Commit Save the slot KV and PutFile it to the store under its block key, +// with single-writer dedup so concurrent agents store a shared prefix +// exactly once. +// +// It owns the cross-cutting policy: cache-key construction, longest-prefix +// matching, the restore-vs-recompute arbiter, the radix index, per-model +// bytes/token learning, and Prometheus accounting. +package orchestrator + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + "sync/atomic" + "time" + + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/arbiter" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/cachekey" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/llamabridge" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/metrics" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/prefixindex" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/store" +) + +type Config struct { + SlotSavePath string // == llama.cpp --slot-save-path (shared filesystem) + DefaultBlockSize int + DefaultReplicaNum uint32 + MinPrefixBlocks int + CleanupFiles bool // delete the local save file after store put / after restore +} + +func (c *Config) defaults() { + if c.DefaultBlockSize <= 0 { + c.DefaultBlockSize = 256 + } + if c.DefaultReplicaNum == 0 { + c.DefaultReplicaNum = 1 + } + if c.MinPrefixBlocks <= 0 { + c.MinPrefixBlocks = 1 + } +} + +type Orchestrator struct { + store store.Backend + llama *llamabridge.Client + index *prefixindex.Index + arb *arbiter.Arbiter + mx *metrics.Metrics + cfg Config + + fpMu sync.Mutex + fpCache map[string]cachekey.ModelFingerprint // gguf path -> completed fingerprint + bptMu sync.Mutex + bytesPerTok map[string]float64 // fingerprint prefix -> learned KV bytes/token + nonce atomic.Uint64 // makes per-request slot filenames unique +} + +func New(b store.Backend, arb *arbiter.Arbiter, mx *metrics.Metrics, cfg Config) *Orchestrator { + cfg.defaults() + return &Orchestrator{ + store: b, llama: llamabridge.New(), index: prefixindex.New(), + arb: arb, mx: mx, cfg: cfg, + fpCache: map[string]cachekey.ModelFingerprint{}, + bytesPerTok: map[string]float64{}, + } +} + +func (o *Orchestrator) Index() *prefixindex.Index { return o.index } +func (o *Orchestrator) Arbiter() *arbiter.Arbiter { return o.arb } + +// ---- request/response types (decoupled from gRPC wire types) ---- + +type Policy struct { + Enable bool + Namespace string + Read bool + Write bool + BlockSize int + ReplicaNum uint32 + SoftPin bool + MinPrefixBlocks int +} + +type Target struct { + BaseURL string + Slot int +} + +type LookupResult struct { + Hit bool + MatchedBlocks int + MatchedTokens int + TotalBlocks int + TotalTokens int + Decision string // restore | recompute | miss + MatchedKey string + Reason string +} + +type PrepareResult struct { + LookupResult + Restored bool + RestoredTokens int + RestoreMs float64 + StoreGetMs float64 + Bytes uint64 +} + +type CommitResult struct { + OK bool + Stored bool + StoredBlocks int + StoredTokens int + Bytes uint64 + SaveMs float64 + StorePutMs float64 + Key string + Reason string +} + +// completeFingerprint fills empty fields by parsing the GGUF at ModelPath +// (cached). The completed fingerprint is what every key derives from, enforcing +// the "different model/tokenizer/rope => different key space" safety rule. +func (o *Orchestrator) completeFingerprint(fp cachekey.ModelFingerprint, modelPath string, swa bool) cachekey.ModelFingerprint { + if fp.BlockSize <= 0 { + fp.BlockSize = o.cfg.DefaultBlockSize + } + needEnrich := fp.Arch == "" || fp.TokenizerHash == "" || fp.RopeHash == "" + if modelPath == "" || !needEnrich { + if fp.KVType == "" { + fp.KVType = "f16" + } + if fp.ModelDigest == "" { + fp.ModelDigest = "unknown" + } + return fp + } + o.fpMu.Lock() + defer o.fpMu.Unlock() + if cached, ok := o.fpCache[modelPath]; ok { + // keep caller-provided block size / kv type + cached.BlockSize = fp.BlockSize + if fp.KVType != "" { + cached.KVType = fp.KVType + } + return cached + } + meta, err := cachekey.ReadGGUFMeta(modelPath) + if err != nil { + if fp.KVType == "" { + fp.KVType = "f16" + } + if fp.ModelDigest == "" { + fp.ModelDigest = "ggufpath-" + filepath.Base(modelPath) + } + return fp + } + kv := fp.KVType + if kv == "" { + kv = "f16" + } + full := meta.Fingerprint(fp.ModelDigest, kv, swa, fp.BlockSize) + o.fpCache[modelPath] = full + return full +} + +type planned struct { + fp cachekey.ModelFingerprint + rootKey string + modelKey string // per-model arbiter key (model digest + kv type), namespace-independent + chain cachekey.Chain + keys []string // keys[i] = boundary i+1 (i.e. prefix of i+1 blocks) +} + +func (o *Orchestrator) plan(fp cachekey.ModelFingerprint, pol Policy, tokens []int32, modelPath string) planned { + full := o.completeFingerprint(fp, modelPath, pol.BlockSize < 0) + bs := full.BlockSize + if pol.BlockSize > 0 { + bs = pol.BlockSize + full.BlockSize = bs + } + rootKey := full.Prefix(pol.Namespace) + modelKey := full.ModelDigest + "|" + full.KVType + "|" + full.Arch + chain := cachekey.ChainBlockHashes(rootKey, tokens, bs) + keys := make([]string, chain.FullBlocks) + for i := 0; i < chain.FullBlocks; i++ { + keys[i] = full.BlockKey(pol.Namespace, i+1, chain.Hex[i]) + } + return planned{fp: full, rootKey: rootKey, modelKey: modelKey, chain: chain, keys: keys} +} + +// longestPresent returns the largest boundary M (in blocks) whose key exists in +// the store, querying all boundaries in one batched call (multi-node correct). +func (o *Orchestrator) longestPresent(ctx context.Context, p planned) (int, string, error) { + if len(p.keys) == 0 { + return 0, "", nil + } + present, err := o.store.Exists(ctx, p.keys) + if err != nil { + return 0, "", err + } + for i := len(present) - 1; i >= 0; i-- { + if i < len(present) && present[i] == 1 { + return i + 1, p.keys[i], nil + } + } + return 0, "", nil +} + +func (o *Orchestrator) learnedBytesPerTok(rootKey string, fallbackTokens int, fallbackBytes uint64) float64 { + o.bptMu.Lock() + defer o.bptMu.Unlock() + if v, ok := o.bytesPerTok[rootKey]; ok && v > 0 { + return v + } + if fallbackTokens > 0 && fallbackBytes > 0 { + return float64(fallbackBytes) / float64(fallbackTokens) + } + return 0 +} + +func (o *Orchestrator) updateBytesPerTok(rootKey string, tokens int, bytes uint64) { + if tokens <= 0 || bytes == 0 { + return + } + o.bptMu.Lock() + defer o.bptMu.Unlock() + bpt := float64(bytes) / float64(tokens) + if old, ok := o.bytesPerTok[rootKey]; ok { + o.bytesPerTok[rootKey] = 0.5*old + 0.5*bpt + } else { + o.bytesPerTok[rootKey] = bpt + } +} + +// Lookup is read-only: longest-prefix match + arbiter decision, no llama I/O. +func (o *Orchestrator) Lookup(ctx context.Context, fp cachekey.ModelFingerprint, pol Policy, tokens []int32, modelPath string) (LookupResult, error) { + o.mx.LookupTotal.Inc() + p := o.plan(fp, pol, tokens, modelPath) + res := LookupResult{TotalBlocks: p.chain.FullBlocks, TotalTokens: p.chain.FullBlocks * p.fp.BlockSize} + if p.chain.FullBlocks == 0 { + res.Decision = "miss" + res.Reason = "prompt shorter than one block" + return res, nil + } + m, key, err := o.longestPresent(ctx, p) + if err != nil { + return res, err + } + if m == 0 { + res.Decision = "miss" + res.Reason = "no cached prefix" + return res, nil + } + matchedTokens := m * p.fp.BlockSize + // size estimate for the arbiter + im := o.index.LongestMatch(p.rootKey, p.chain.Hex, false) + var estBytes uint64 + if im.Found && im.Blocks == m { + estBytes = im.Bytes + } else { + bpt := o.learnedBytesPerTok(p.rootKey, 0, 0) + estBytes = uint64(float64(matchedTokens) * bpt) + } + dec := o.arb.Decide(p.modelKey, matchedTokens, estBytes) + res.Hit = true + res.MatchedBlocks = m + res.MatchedTokens = matchedTokens + res.MatchedKey = key + res.Reason = dec.Reason + if dec.Restore { + res.Decision = "restore" + } else { + res.Decision = "recompute" + } + return res, nil +} + +// Prepare runs Lookup and, if the arbiter approves and a target is given, +// restores the matched KV into target.Slot. +func (o *Orchestrator) Prepare(ctx context.Context, fp cachekey.ModelFingerprint, pol Policy, tokens []int32, modelPath string, tgt *Target) (PrepareResult, error) { + o.mx.PrepareTotal.Inc() + prepStart := time.Now() // full restore cost incl lookup + transfer + GPU load + orchestration + p := o.plan(fp, pol, tokens, modelPath) + out := PrepareResult{} + out.TotalBlocks = p.chain.FullBlocks + out.TotalTokens = p.chain.FullBlocks * p.fp.BlockSize + + if !pol.Read || p.chain.FullBlocks == 0 { + out.Decision = "miss" + out.Reason = "read disabled or prompt < 1 block" + o.mx.Misses.Inc() + o.mx.MissBlocks.Add(float64(p.chain.FullBlocks)) + return out, nil + } + m, key, err := o.longestPresent(ctx, p) + if err != nil { + return out, err + } + if m == 0 { + out.Decision = "miss" + out.Reason = "no cached prefix" + o.mx.Misses.Inc() + o.mx.MissBlocks.Add(float64(p.chain.FullBlocks)) + return out, nil + } + out.Hit = true + out.MatchedBlocks = m + out.MatchedTokens = m * p.fp.BlockSize + out.MatchedKey = key + + im := o.index.LongestMatch(p.rootKey, p.chain.Hex, true) + var estBytes uint64 + if im.Found && im.Blocks == m { + estBytes = im.Bytes + } else { + estBytes = uint64(float64(out.MatchedTokens) * o.learnedBytesPerTok(p.rootKey, 0, 0)) + } + dec := o.arb.Decide(p.modelKey, out.MatchedTokens, estBytes) + out.Reason = dec.Reason + if !dec.Restore { + out.Decision = "recompute" + o.mx.RecomputeChosen.Inc() + o.mx.MissBlocks.Add(float64(p.chain.FullBlocks)) // will be recomputed + return out, nil + } + out.Decision = "restore" + if tgt == nil || tgt.BaseURL == "" { + // plan-only (no target): report the decision without doing I/O. + return out, nil + } + + // ---- Load stage ---- + fname := fmt.Sprintf("omb-r-%x-%d.bin", hashKey(key), o.nonce.Add(1)) + abspath := filepath.Join(o.cfg.SlotSavePath, fname) + gr, err := o.store.GetFile(ctx, key, abspath) + if err != nil { + return out, fmt.Errorf("store get: %w", err) + } + if !gr.Found { + // raced with eviction; degrade to miss + out.Decision = "miss" + out.Hit = false + out.Reason = "matched key vanished (evicted); recompute" + o.mx.Misses.Inc() + o.mx.MissBlocks.Add(float64(p.chain.FullBlocks)) + return out, nil + } + out.StoreGetMs = gr.ElapsedMs + out.Bytes = gr.Bytes + o.mx.StoreGetLatency.Observe(gr.ElapsedMs) + o.mx.AddBytesGet(gr.Bytes) + + tRestore := time.Now() + rr, err := o.llama.RestoreSlot(ctx, tgt.BaseURL, tgt.Slot, fname) + restoreMs := float64(time.Since(tRestore).Microseconds()) / 1000.0 + if o.cfg.CleanupFiles { + os.Remove(abspath) + } + if err != nil { + return out, fmt.Errorf("llama restore: %w", err) + } + out.Restored = true + out.RestoredTokens = rr.NRestored + out.RestoreMs = restoreMs + // Feed the arbiter the FULL prepare wall (lookup + store fetch + GPU load + + // orchestration). For small blobs this is dominated by fixed overhead, so + // the learned "restore bandwidth" is low and the arbiter declines next time; + // for large blobs it is transfer-dominated and restore wins. This is what + // makes the policy adaptive and loss-free across model/hardware regimes. + fullMs := float64(time.Since(prepStart).Microseconds()) / 1000.0 + o.arb.ObserveGet(p.modelKey, gr.Bytes, fullMs) + o.mx.RestoreLatency.Observe(fullMs) + o.mx.RestoreCount.Inc() + o.mx.Hits.Inc() + o.mx.HitBlocks.Add(float64(m)) + o.mx.MissBlocks.Add(float64(p.chain.FullBlocks - m)) + o.mx.AddSavedTokens(out.MatchedTokens) + if im.Found { + // learned size correction + o.updateBytesPerTok(p.rootKey, out.MatchedTokens, gr.Bytes) + } + return out, nil +} + +// Commit saves the KV currently in target.Slot and stores it under the block +// key for the largest block-aligned prefix of `tokens`. skip-if-exists gives +// single-writer dedup across concurrent agents sharing a prefix. +func (o *Orchestrator) Commit(ctx context.Context, fp cachekey.ModelFingerprint, pol Policy, tokens []int32, modelPath string, tgt *Target, prefillN int, prefillMs float64) (CommitResult, error) { + o.mx.CommitTotal.Inc() + out := CommitResult{} + if !pol.Write { + out.Reason = "write disabled" + return out, nil + } + p := o.plan(fp, pol, tokens, modelPath) + // Learn the live prefill rate (per model) so the arbiter can compare. + o.arb.ObservePrefill(p.modelKey, prefillN, prefillMs) + minB := pol.MinPrefixBlocks + if minB <= 0 { + minB = o.cfg.MinPrefixBlocks + } + if p.chain.FullBlocks < minB { + out.Reason = fmt.Sprintf("prefix %d blocks < min %d; not cached", p.chain.FullBlocks, minB) + return out, nil + } + if tgt == nil || tgt.BaseURL == "" { + out.Reason = "no target to save from" + return out, nil + } + blocks := p.chain.FullBlocks + key := p.keys[blocks-1] + out.Key = key + out.StoredBlocks = blocks + out.StoredTokens = blocks * p.fp.BlockSize + + // Fast path: someone already stored this exact prefix. + if present, err := o.store.Exists(ctx, []string{key}); err == nil && len(present) == 1 && present[0] == 1 { + out.OK = true + out.Stored = false + out.Reason = "already present (dedup)" + return out, nil + } + + fname := fmt.Sprintf("omb-s-%x-%d.bin", hashKey(key), o.nonce.Add(1)) + abspath := filepath.Join(o.cfg.SlotSavePath, fname) + tSave := time.Now() + sr, err := o.llama.SaveSlot(ctx, tgt.BaseURL, tgt.Slot, fname) + if err != nil { + return out, fmt.Errorf("llama save: %w", err) + } + out.SaveMs = float64(time.Since(tSave).Microseconds()) / 1000.0 + o.mx.SaveLatency.Observe(out.SaveMs) + repl := pol.ReplicaNum + if repl == 0 { + repl = o.cfg.DefaultReplicaNum + } + pr, err := o.store.PutFile(ctx, key, abspath, repl, pol.SoftPin, true) + if o.cfg.CleanupFiles { + os.Remove(abspath) + } + if err != nil { + return out, fmt.Errorf("store put: %w", err) + } + out.StorePutMs = pr.ElapsedMs + out.Bytes = pr.Bytes + out.OK = true + out.Stored = !pr.Existed + o.mx.StorePutLatency.Observe(pr.ElapsedMs) + if out.Stored { + o.mx.AddBytesPut(pr.Bytes) + o.index.Insert(p.rootKey, p.chain.Hex, blocks, out.StoredTokens, key, pr.Bytes) + } + // learn bytes/token from the real save (n_written / n_saved) + if sr.NSaved > 0 { + o.updateBytesPerTok(p.rootKey, sr.NSaved, sr.NWritten) + } + if out.Stored { + out.Reason = "stored" + } else { + out.Reason = "already present (dedup)" + } + return out, nil +} + +// RefreshGauges pushes learned arbiter rates + index stats into Prometheus. +func (o *Orchestrator) RefreshGauges() { + s := o.arb.Snapshot() + o.mx.GetGBps.Set(s.GetGBps) + o.mx.PrefillToksS.Set(s.PrefillToksS) + is := o.index.Stats() + o.mx.IndexSnapshots.Set(float64(is.Snapshots)) + o.mx.IndexBytes.Set(float64(is.Bytes)) +} + +func hashKey(s string) uint64 { + // FNV-1a 64 + var h uint64 = 1469598103934665603 + for i := 0; i < len(s); i++ { + h ^= uint64(s[i]) + h *= 1099511628211 + } + return h +} diff --git a/mooncake-integration/ollama/bridge/internal/server/grpc.go b/mooncake-integration/ollama/bridge/internal/server/grpc.go new file mode 100644 index 00000000..04f8c1e4 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/server/grpc.go @@ -0,0 +1,151 @@ +// Package server adapts the gRPC KVCacheBus contract and an HTTP/JSON gateway +// onto the orchestrator. gRPC is the primary transport; the HTTP/JSON gateway +// exists so the (Go) Ollama patch and quick curl tests can drive the bus with a +// single net/http call and no protobuf dependency. +package server + +import ( + "context" + + bridgepb "github.com/mooncake-ai/ollama-mooncake-bridge/internal/bridge/pb" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/cachekey" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/metrics" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/orchestrator" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/store" + dto "github.com/prometheus/client_model/go" + "github.com/prometheus/client_golang/prometheus" +) + +const Version = "0.1.0" + +// GRPCServer implements bridgepb.KVCacheBusServer. +type GRPCServer struct { + bridgepb.UnimplementedKVCacheBusServer + orch *orchestrator.Orchestrator + store store.Backend + mx *metrics.Metrics +} + +func NewGRPC(o *orchestrator.Orchestrator, b store.Backend, mx *metrics.Metrics) *GRPCServer { + return &GRPCServer{orch: o, store: b, mx: mx} +} + +func fpFromProto(p *bridgepb.ModelFingerprint) (cachekey.ModelFingerprint, string) { + if p == nil { + return cachekey.ModelFingerprint{}, "" + } + return cachekey.ModelFingerprint{ + ModelDigest: p.ModelDigest, + Arch: p.Arch, + TokenizerHash: p.TokenizerHash, + RopeHash: p.RopeHash, + KVType: p.KvType, + KVLayout: p.KvLayout, + NCtxTrain: int(p.NCtxTrain), + BlockSize: int(p.BlockSize), + }, p.ModelPath +} + +func polFromProto(p *bridgepb.CachePolicy) orchestrator.Policy { + if p == nil { + return orchestrator.Policy{Enable: true, Read: true, Write: true} + } + return orchestrator.Policy{ + Enable: p.Enable, + Namespace: p.Namespace, + Read: p.Read, + Write: p.Write, + BlockSize: int(p.BlockSize), + ReplicaNum: p.ReplicaNum, + SoftPin: p.SoftPin, + MinPrefixBlocks: int(p.MinPrefixBlocks), + } +} + +func tgtFromProto(p *bridgepb.LlamaTarget) *orchestrator.Target { + if p == nil || p.BaseUrl == "" { + return nil + } + return &orchestrator.Target{BaseURL: p.BaseUrl, Slot: int(p.SlotId)} +} + +func (s *GRPCServer) Health(ctx context.Context, _ *bridgepb.HealthRequest) (*bridgepb.HealthReply, error) { + hi, err := s.store.Health(ctx) + rep := &bridgepb.HealthReply{Version: Version, StoreBackend: s.store.Name()} + if err != nil { + rep.Ok = false + rep.Detail = err.Error() + return rep, nil + } + rep.Ok = true + rep.StoreOk = hi.OK + rep.Protocol = hi.Protocol + rep.Detail = hi.Detail + return rep, nil +} + +func (s *GRPCServer) Lookup(ctx context.Context, req *bridgepb.LookupRequest) (*bridgepb.LookupReply, error) { + fp, mp := fpFromProto(req.Fp) + r, err := s.orch.Lookup(ctx, fp, polFromProto(req.Policy), req.Tokens, mp) + if err != nil { + return &bridgepb.LookupReply{Error: err.Error()}, nil + } + return &bridgepb.LookupReply{ + Hit: r.Hit, MatchedBlocks: int32(r.MatchedBlocks), MatchedTokens: int32(r.MatchedTokens), + TotalBlocks: int32(r.TotalBlocks), TotalTokens: int32(r.TotalTokens), + Decision: r.Decision, MatchedKey: r.MatchedKey, Reason: r.Reason, + }, nil +} + +func (s *GRPCServer) Prepare(ctx context.Context, req *bridgepb.PrepareRequest) (*bridgepb.PrepareReply, error) { + fp, mp := fpFromProto(req.Fp) + r, err := s.orch.Prepare(ctx, fp, polFromProto(req.Policy), req.Tokens, mp, tgtFromProto(req.Target)) + if err != nil { + return &bridgepb.PrepareReply{Error: err.Error()}, nil + } + return &bridgepb.PrepareReply{ + Hit: r.Hit, MatchedBlocks: int32(r.MatchedBlocks), MatchedTokens: int32(r.MatchedTokens), + TotalBlocks: int32(r.TotalBlocks), TotalTokens: int32(r.TotalTokens), + Decision: r.Decision, MatchedKey: r.MatchedKey, + Restored: r.Restored, RestoredTokens: int32(r.RestoredTokens), + RestoreMs: r.RestoreMs, StoreGetMs: r.StoreGetMs, Bytes: r.Bytes, Reason: r.Reason, + }, nil +} + +func (s *GRPCServer) Commit(ctx context.Context, req *bridgepb.CommitRequest) (*bridgepb.CommitReply, error) { + fp, mp := fpFromProto(req.Fp) + r, err := s.orch.Commit(ctx, fp, polFromProto(req.Policy), req.Tokens, mp, tgtFromProto(req.Target), int(req.PrefillN), req.PrefillMs) + if err != nil { + return &bridgepb.CommitReply{Error: err.Error()}, nil + } + return &bridgepb.CommitReply{ + Ok: r.OK, Stored: r.Stored, StoredBlocks: int32(r.StoredBlocks), StoredTokens: int32(r.StoredTokens), + Bytes: r.Bytes, SaveMs: r.SaveMs, StorePutMs: r.StorePutMs, Key: r.Key, Reason: r.Reason, + }, nil +} + +func (s *GRPCServer) Stats(_ context.Context, _ *bridgepb.StatsRequest) (*bridgepb.StatsReply, error) { + s.orch.RefreshGauges() + m := s.mx + return &bridgepb.StatsReply{ + PrepareTotal: u(m.PrepareTotal), + Hits: u(m.Hits), + Misses: u(m.Misses), + HitBlocks: u(m.HitBlocks), + MissBlocks: u(m.MissBlocks), + SavedPrefillTokens: m.SavedTokens(), + RestoreCount: u(m.RestoreCount), + CommitCount: u(m.CommitTotal), + BytesGet: m.TotalBytesGet(), + BytesPut: m.TotalBytesPut(), + }, nil +} + +// u reads a prometheus counter's current value as uint64. +func u(c prometheus.Counter) uint64 { + var m dto.Metric + if err := c.Write(&m); err != nil { + return 0 + } + return uint64(m.GetCounter().GetValue()) +} diff --git a/mooncake-integration/ollama/bridge/internal/server/http.go b/mooncake-integration/ollama/bridge/internal/server/http.go new file mode 100644 index 00000000..4ba09f33 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/server/http.go @@ -0,0 +1,211 @@ +package server + +import ( + "encoding/json" + "net/http" + + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/cachekey" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/metrics" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/orchestrator" + "github.com/mooncake-ai/ollama-mooncake-bridge/internal/store" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// --- JSON DTOs (snake_case mirrors the proto for a uniform API) --- + +type jsonFP struct { + ModelDigest string `json:"model_digest"` + Arch string `json:"arch"` + TokenizerHash string `json:"tokenizer_hash"` + RopeHash string `json:"rope_hash"` + KVType string `json:"kv_type"` + KVLayout string `json:"kv_layout"` + NCtxTrain int `json:"n_ctx_train"` + BlockSize int `json:"block_size"` + ModelPath string `json:"model_path"` +} + +func (f jsonFP) to() (cachekey.ModelFingerprint, string) { + return cachekey.ModelFingerprint{ + ModelDigest: f.ModelDigest, Arch: f.Arch, TokenizerHash: f.TokenizerHash, + RopeHash: f.RopeHash, KVType: f.KVType, KVLayout: f.KVLayout, + NCtxTrain: f.NCtxTrain, BlockSize: f.BlockSize, + }, f.ModelPath +} + +type jsonPolicy struct { + Enable bool `json:"enable"` + Namespace string `json:"namespace"` + Read bool `json:"read"` + Write bool `json:"write"` + BlockSize int `json:"block_size"` + ReplicaNum uint32 `json:"replica_num"` + SoftPin bool `json:"soft_pin"` + MinPrefixBlocks int `json:"min_prefix_blocks"` +} + +func (p jsonPolicy) to() orchestrator.Policy { + return orchestrator.Policy{ + Enable: p.Enable, Namespace: p.Namespace, Read: p.Read, Write: p.Write, + BlockSize: p.BlockSize, ReplicaNum: p.ReplicaNum, SoftPin: p.SoftPin, + MinPrefixBlocks: p.MinPrefixBlocks, + } +} + +type jsonTarget struct { + BaseURL string `json:"base_url"` + Slot int `json:"slot_id"` +} + +type jsonReq struct { + FP jsonFP `json:"fp"` + Policy jsonPolicy `json:"policy"` + Tokens []int32 `json:"tokens"` + Target *jsonTarget `json:"target"` + PrefillN int `json:"prefill_n"` + PrefillMs float64 `json:"prefill_ms"` +} + +func (r jsonReq) target() *orchestrator.Target { + if r.Target == nil || r.Target.BaseURL == "" { + return nil + } + return &orchestrator.Target{BaseURL: r.Target.BaseURL, Slot: r.Target.Slot} +} + +// HTTPServer wires the JSON gateway + Prometheus endpoint. +type HTTPServer struct { + orch *orchestrator.Orchestrator + store store.Backend + mx *metrics.Metrics + reg *prometheus.Registry +} + +func NewHTTP(o *orchestrator.Orchestrator, b store.Backend, mx *metrics.Metrics, reg *prometheus.Registry) *HTTPServer { + return &HTTPServer{orch: o, store: b, mx: mx, reg: reg} +} + +func (h *HTTPServer) Mux() *http.ServeMux { + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.HandlerFor(h.reg, promhttp.HandlerOpts{})) + mux.HandleFunc("/healthz", h.healthz) + mux.HandleFunc("/stats", h.stats) + mux.HandleFunc("/v1/lookup", h.lookup) + mux.HandleFunc("/v1/prepare", h.prepare) + mux.HandleFunc("/v1/commit", h.commit) + return mux +} + +func writeJSON(w http.ResponseWriter, code int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(v) +} + +func (h *HTTPServer) decode(w http.ResponseWriter, r *http.Request) (jsonReq, bool) { + var req jsonReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) + return req, false + } + return req, true +} + +func (h *HTTPServer) healthz(w http.ResponseWriter, r *http.Request) { + hi, err := h.store.Health(r.Context()) + resp := map[string]any{"version": Version, "store_backend": h.store.Name()} + if err != nil { + resp["ok"] = false + resp["detail"] = err.Error() + writeJSON(w, http.StatusOK, resp) + return + } + resp["ok"] = true + resp["store_ok"] = hi.OK + resp["protocol"] = hi.Protocol + resp["master"] = hi.Master + resp["device"] = hi.Device + resp["detail"] = hi.Detail + writeJSON(w, http.StatusOK, resp) +} + +func (h *HTTPServer) stats(w http.ResponseWriter, _ *http.Request) { + h.orch.RefreshGauges() + snap := h.orch.Arbiter().Snapshot() + is := h.orch.Index().Stats() + writeJSON(w, http.StatusOK, map[string]any{ + "prepare_total": u(h.mx.PrepareTotal), + "hits": u(h.mx.Hits), + "misses": u(h.mx.Misses), + "recompute_chosen": u(h.mx.RecomputeChosen), + "hit_blocks": u(h.mx.HitBlocks), + "miss_blocks": u(h.mx.MissBlocks), + "restore_count": u(h.mx.RestoreCount), + "commit_count": u(h.mx.CommitTotal), + "saved_prefill_tokens": h.mx.SavedTokens(), + "bytes_get": h.mx.TotalBytesGet(), + "bytes_put": h.mx.TotalBytesPut(), + "learned_get_gbps": snap.GetGBps, + "learned_prefill_tps": snap.PrefillToksS, + "index_snapshots": is.Snapshots, + "index_bytes": is.Bytes, + }) +} + +func (h *HTTPServer) lookup(w http.ResponseWriter, r *http.Request) { + req, ok := h.decode(w, r) + if !ok { + return + } + fp, mp := req.FP.to() + res, err := h.orch.Lookup(r.Context(), fp, req.Policy.to(), req.Tokens, mp) + if err != nil { + writeJSON(w, http.StatusOK, map[string]any{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "hit": res.Hit, "matched_blocks": res.MatchedBlocks, "matched_tokens": res.MatchedTokens, + "total_blocks": res.TotalBlocks, "total_tokens": res.TotalTokens, + "decision": res.Decision, "matched_key": res.MatchedKey, "reason": res.Reason, + }) +} + +func (h *HTTPServer) prepare(w http.ResponseWriter, r *http.Request) { + req, ok := h.decode(w, r) + if !ok { + return + } + fp, mp := req.FP.to() + res, err := h.orch.Prepare(r.Context(), fp, req.Policy.to(), req.Tokens, mp, req.target()) + if err != nil { + writeJSON(w, http.StatusOK, map[string]any{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "hit": res.Hit, "matched_blocks": res.MatchedBlocks, "matched_tokens": res.MatchedTokens, + "total_blocks": res.TotalBlocks, "total_tokens": res.TotalTokens, + "decision": res.Decision, "matched_key": res.MatchedKey, + "restored": res.Restored, "restored_tokens": res.RestoredTokens, + "restore_ms": res.RestoreMs, "store_get_ms": res.StoreGetMs, "bytes": res.Bytes, + "reason": res.Reason, + }) +} + +func (h *HTTPServer) commit(w http.ResponseWriter, r *http.Request) { + req, ok := h.decode(w, r) + if !ok { + return + } + fp, mp := req.FP.to() + res, err := h.orch.Commit(r.Context(), fp, req.Policy.to(), req.Tokens, mp, req.target(), req.PrefillN, req.PrefillMs) + if err != nil { + writeJSON(w, http.StatusOK, map[string]any{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "ok": res.OK, "stored": res.Stored, "stored_blocks": res.StoredBlocks, + "stored_tokens": res.StoredTokens, "bytes": res.Bytes, + "save_ms": res.SaveMs, "store_put_ms": res.StorePutMs, "key": res.Key, "reason": res.Reason, + }) +} -- 2.34.1 From 56efccf73c965789a04b5de7d7e4ca793b83df25 Mon Sep 17 00:00:00 2001 From: zbtrs2 Date: Mon, 29 Jun 2026 01:51:08 +0800 Subject: [PATCH 4/9] ollama-kvcache: add Mooncake store proxy A long-lived gRPC service that owns one warm MooncakeDistributedStore handle and exposes it to the Go sidecar, which cannot link the Python bindings directly. Using the official store client keeps the integration on the supported API surface. KV snapshots are multi-GiB, so the proxy moves them by file path: PutFile reads a slot file and stores it, GetFile materialises an object straight into the slot directory, and the blob is copied at most once. Objects are striped into <=64 MiB chunks transferred in parallel with batch_put_from / batch_get_into, because single-object RDMA degrades for large objects while striped batches sustain full bandwidth. A pool of pre-registered (pinned) staging buffers amortises RDMA memory registration across operations. Eviction or lease-expiry races on read are reported as not-found so the caller falls back to recompute rather than failing. A filesystem backend (no master required) backs the local baseline, and a selftest verifies put/get round-trips and existence checks over both backends. --- .../ollama/store-proxy/gen/__init__.py | 0 .../ollama/store-proxy/gen/storeproxy_pb2.py | 59 ++ .../store-proxy/gen/storeproxy_pb2_grpc.py | 302 ++++++++ .../ollama/store-proxy/store_proxy.py | 669 ++++++++++++++++++ 4 files changed, 1030 insertions(+) create mode 100644 mooncake-integration/ollama/store-proxy/gen/__init__.py create mode 100644 mooncake-integration/ollama/store-proxy/gen/storeproxy_pb2.py create mode 100644 mooncake-integration/ollama/store-proxy/gen/storeproxy_pb2_grpc.py create mode 100644 mooncake-integration/ollama/store-proxy/store_proxy.py diff --git a/mooncake-integration/ollama/store-proxy/gen/__init__.py b/mooncake-integration/ollama/store-proxy/gen/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mooncake-integration/ollama/store-proxy/gen/storeproxy_pb2.py b/mooncake-integration/ollama/store-proxy/gen/storeproxy_pb2.py new file mode 100644 index 00000000..11739fc0 --- /dev/null +++ b/mooncake-integration/ollama/store-proxy/gen/storeproxy_pb2.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: storeproxy.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10storeproxy.proto\x12\rstoreproxy.v1\"\x0f\n\rHealthRequest\"l\n\x0bHealthReply\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x0f\n\x07\x62\x61\x63kend\x18\x02 \x01(\t\x12\x10\n\x08protocol\x18\x03 \x01(\t\x12\x0e\n\x06master\x18\x04 \x01(\t\x12\x0e\n\x06\x64\x65vice\x18\x05 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x06 \x01(\t\"\x1d\n\rExistsRequest\x12\x0c\n\x04keys\x18\x01 \x03(\t\"\x1e\n\x0b\x45xistsReply\x12\x0f\n\x07present\x18\x01 \x03(\x05\"j\n\x0ePutFileRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\x12\x13\n\x0breplica_num\x18\x03 \x01(\r\x12\x10\n\x08soft_pin\x18\x04 \x01(\x08\x12\x16\n\x0eskip_if_exists\x18\x05 \x01(\x08\"]\n\x0cPutFileReply\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x62ytes\x18\x02 \x01(\x04\x12\x12\n\nelapsed_ms\x18\x03 \x01(\x01\x12\x0f\n\x07\x65xisted\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\"+\n\x0eGetFileRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\"[\n\x0cGetFileReply\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x62ytes\x18\x02 \x01(\x04\x12\x12\n\nelapsed_ms\x18\x03 \x01(\x01\x12\r\n\x05\x66ound\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\"T\n\x0fPutBytesRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c\x12\x13\n\x0breplica_num\x18\x03 \x01(\r\x12\x10\n\x08soft_pin\x18\x04 \x01(\x08\"M\n\rPutBytesReply\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x62ytes\x18\x02 \x01(\x04\x12\x12\n\nelapsed_ms\x18\x03 \x01(\x01\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\x1e\n\x0fGetBytesRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\"\\\n\rGetBytesReply\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x0c\x12\x12\n\nelapsed_ms\x18\x03 \x01(\x01\x12\r\n\x05\x66ound\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\"+\n\rRemoveRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05\x66orce\x18\x02 \x01(\x08\"(\n\x0bRemoveReply\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x0e\n\x0cStatsRequest\"\xa5\x01\n\nStatsReply\x12\x0f\n\x07put_ops\x18\x01 \x01(\x04\x12\x0f\n\x07get_ops\x18\x02 \x01(\x04\x12\x12\n\nexists_ops\x18\x03 \x01(\x04\x12\x11\n\tput_bytes\x18\x04 \x01(\x04\x12\x11\n\tget_bytes\x18\x05 \x01(\x04\x12\x14\n\x0cput_ms_total\x18\x06 \x01(\x01\x12\x14\n\x0cget_ms_total\x18\x07 \x01(\x01\x12\x0f\n\x07\x62\x61\x63kend\x18\x08 \x01(\t2\xbb\x04\n\nStoreProxy\x12\x42\n\x06Health\x12\x1c.storeproxy.v1.HealthRequest\x1a\x1a.storeproxy.v1.HealthReply\x12\x42\n\x06\x45xists\x12\x1c.storeproxy.v1.ExistsRequest\x1a\x1a.storeproxy.v1.ExistsReply\x12\x45\n\x07PutFile\x12\x1d.storeproxy.v1.PutFileRequest\x1a\x1b.storeproxy.v1.PutFileReply\x12\x45\n\x07GetFile\x12\x1d.storeproxy.v1.GetFileRequest\x1a\x1b.storeproxy.v1.GetFileReply\x12H\n\x08PutBytes\x12\x1e.storeproxy.v1.PutBytesRequest\x1a\x1c.storeproxy.v1.PutBytesReply\x12H\n\x08GetBytes\x12\x1e.storeproxy.v1.GetBytesRequest\x1a\x1c.storeproxy.v1.GetBytesReply\x12\x42\n\x06Remove\x12\x1c.storeproxy.v1.RemoveRequest\x1a\x1a.storeproxy.v1.RemoveReply\x12?\n\x05Stats\x12\x1b.storeproxy.v1.StatsRequest\x1a\x19.storeproxy.v1.StatsReplyBSZQgithub.com/mooncake-ai/ollama-mooncake-bridge/internal/storeproxy/pb;storeproxypbb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'storeproxy_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'ZQgithub.com/mooncake-ai/ollama-mooncake-bridge/internal/storeproxy/pb;storeproxypb' + _globals['_HEALTHREQUEST']._serialized_start=35 + _globals['_HEALTHREQUEST']._serialized_end=50 + _globals['_HEALTHREPLY']._serialized_start=52 + _globals['_HEALTHREPLY']._serialized_end=160 + _globals['_EXISTSREQUEST']._serialized_start=162 + _globals['_EXISTSREQUEST']._serialized_end=191 + _globals['_EXISTSREPLY']._serialized_start=193 + _globals['_EXISTSREPLY']._serialized_end=223 + _globals['_PUTFILEREQUEST']._serialized_start=225 + _globals['_PUTFILEREQUEST']._serialized_end=331 + _globals['_PUTFILEREPLY']._serialized_start=333 + _globals['_PUTFILEREPLY']._serialized_end=426 + _globals['_GETFILEREQUEST']._serialized_start=428 + _globals['_GETFILEREQUEST']._serialized_end=471 + _globals['_GETFILEREPLY']._serialized_start=473 + _globals['_GETFILEREPLY']._serialized_end=564 + _globals['_PUTBYTESREQUEST']._serialized_start=566 + _globals['_PUTBYTESREQUEST']._serialized_end=650 + _globals['_PUTBYTESREPLY']._serialized_start=652 + _globals['_PUTBYTESREPLY']._serialized_end=729 + _globals['_GETBYTESREQUEST']._serialized_start=731 + _globals['_GETBYTESREQUEST']._serialized_end=761 + _globals['_GETBYTESREPLY']._serialized_start=763 + _globals['_GETBYTESREPLY']._serialized_end=855 + _globals['_REMOVEREQUEST']._serialized_start=857 + _globals['_REMOVEREQUEST']._serialized_end=900 + _globals['_REMOVEREPLY']._serialized_start=902 + _globals['_REMOVEREPLY']._serialized_end=942 + _globals['_STATSREQUEST']._serialized_start=944 + _globals['_STATSREQUEST']._serialized_end=958 + _globals['_STATSREPLY']._serialized_start=961 + _globals['_STATSREPLY']._serialized_end=1126 + _globals['_STOREPROXY']._serialized_start=1129 + _globals['_STOREPROXY']._serialized_end=1700 +# @@protoc_insertion_point(module_scope) diff --git a/mooncake-integration/ollama/store-proxy/gen/storeproxy_pb2_grpc.py b/mooncake-integration/ollama/store-proxy/gen/storeproxy_pb2_grpc.py new file mode 100644 index 00000000..0f27217b --- /dev/null +++ b/mooncake-integration/ollama/store-proxy/gen/storeproxy_pb2_grpc.py @@ -0,0 +1,302 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from . import storeproxy_pb2 as storeproxy__pb2 + + +class StoreProxyStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Health = channel.unary_unary( + '/storeproxy.v1.StoreProxy/Health', + request_serializer=storeproxy__pb2.HealthRequest.SerializeToString, + response_deserializer=storeproxy__pb2.HealthReply.FromString, + ) + self.Exists = channel.unary_unary( + '/storeproxy.v1.StoreProxy/Exists', + request_serializer=storeproxy__pb2.ExistsRequest.SerializeToString, + response_deserializer=storeproxy__pb2.ExistsReply.FromString, + ) + self.PutFile = channel.unary_unary( + '/storeproxy.v1.StoreProxy/PutFile', + request_serializer=storeproxy__pb2.PutFileRequest.SerializeToString, + response_deserializer=storeproxy__pb2.PutFileReply.FromString, + ) + self.GetFile = channel.unary_unary( + '/storeproxy.v1.StoreProxy/GetFile', + request_serializer=storeproxy__pb2.GetFileRequest.SerializeToString, + response_deserializer=storeproxy__pb2.GetFileReply.FromString, + ) + self.PutBytes = channel.unary_unary( + '/storeproxy.v1.StoreProxy/PutBytes', + request_serializer=storeproxy__pb2.PutBytesRequest.SerializeToString, + response_deserializer=storeproxy__pb2.PutBytesReply.FromString, + ) + self.GetBytes = channel.unary_unary( + '/storeproxy.v1.StoreProxy/GetBytes', + request_serializer=storeproxy__pb2.GetBytesRequest.SerializeToString, + response_deserializer=storeproxy__pb2.GetBytesReply.FromString, + ) + self.Remove = channel.unary_unary( + '/storeproxy.v1.StoreProxy/Remove', + request_serializer=storeproxy__pb2.RemoveRequest.SerializeToString, + response_deserializer=storeproxy__pb2.RemoveReply.FromString, + ) + self.Stats = channel.unary_unary( + '/storeproxy.v1.StoreProxy/Stats', + request_serializer=storeproxy__pb2.StatsRequest.SerializeToString, + response_deserializer=storeproxy__pb2.StatsReply.FromString, + ) + + +class StoreProxyServicer(object): + """Missing associated documentation comment in .proto file.""" + + def Health(self, request, context): + """Liveness + which backend/protocol/master is active. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Exists(self, request, context): + """Batched existence check (maps to mooncake batch_is_exist). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def PutFile(self, request, context): + """Store a KV snapshot file under `key`. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetFile(self, request, context): + """Materialize `key` into a local file at `path`. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def PutBytes(self, request, context): + """Inline small-value variants (metadata, manifests). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetBytes(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Remove(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Stats(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_StoreProxyServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Health': grpc.unary_unary_rpc_method_handler( + servicer.Health, + request_deserializer=storeproxy__pb2.HealthRequest.FromString, + response_serializer=storeproxy__pb2.HealthReply.SerializeToString, + ), + 'Exists': grpc.unary_unary_rpc_method_handler( + servicer.Exists, + request_deserializer=storeproxy__pb2.ExistsRequest.FromString, + response_serializer=storeproxy__pb2.ExistsReply.SerializeToString, + ), + 'PutFile': grpc.unary_unary_rpc_method_handler( + servicer.PutFile, + request_deserializer=storeproxy__pb2.PutFileRequest.FromString, + response_serializer=storeproxy__pb2.PutFileReply.SerializeToString, + ), + 'GetFile': grpc.unary_unary_rpc_method_handler( + servicer.GetFile, + request_deserializer=storeproxy__pb2.GetFileRequest.FromString, + response_serializer=storeproxy__pb2.GetFileReply.SerializeToString, + ), + 'PutBytes': grpc.unary_unary_rpc_method_handler( + servicer.PutBytes, + request_deserializer=storeproxy__pb2.PutBytesRequest.FromString, + response_serializer=storeproxy__pb2.PutBytesReply.SerializeToString, + ), + 'GetBytes': grpc.unary_unary_rpc_method_handler( + servicer.GetBytes, + request_deserializer=storeproxy__pb2.GetBytesRequest.FromString, + response_serializer=storeproxy__pb2.GetBytesReply.SerializeToString, + ), + 'Remove': grpc.unary_unary_rpc_method_handler( + servicer.Remove, + request_deserializer=storeproxy__pb2.RemoveRequest.FromString, + response_serializer=storeproxy__pb2.RemoveReply.SerializeToString, + ), + 'Stats': grpc.unary_unary_rpc_method_handler( + servicer.Stats, + request_deserializer=storeproxy__pb2.StatsRequest.FromString, + response_serializer=storeproxy__pb2.StatsReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'storeproxy.v1.StoreProxy', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class StoreProxy(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def Health(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/storeproxy.v1.StoreProxy/Health', + storeproxy__pb2.HealthRequest.SerializeToString, + storeproxy__pb2.HealthReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Exists(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/storeproxy.v1.StoreProxy/Exists', + storeproxy__pb2.ExistsRequest.SerializeToString, + storeproxy__pb2.ExistsReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def PutFile(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/storeproxy.v1.StoreProxy/PutFile', + storeproxy__pb2.PutFileRequest.SerializeToString, + storeproxy__pb2.PutFileReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetFile(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/storeproxy.v1.StoreProxy/GetFile', + storeproxy__pb2.GetFileRequest.SerializeToString, + storeproxy__pb2.GetFileReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def PutBytes(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/storeproxy.v1.StoreProxy/PutBytes', + storeproxy__pb2.PutBytesRequest.SerializeToString, + storeproxy__pb2.PutBytesReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetBytes(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/storeproxy.v1.StoreProxy/GetBytes', + storeproxy__pb2.GetBytesRequest.SerializeToString, + storeproxy__pb2.GetBytesReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Remove(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/storeproxy.v1.StoreProxy/Remove', + storeproxy__pb2.RemoveRequest.SerializeToString, + storeproxy__pb2.RemoveReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Stats(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/storeproxy.v1.StoreProxy/Stats', + storeproxy__pb2.StatsRequest.SerializeToString, + storeproxy__pb2.StatsReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/mooncake-integration/ollama/store-proxy/store_proxy.py b/mooncake-integration/ollama/store-proxy/store_proxy.py new file mode 100644 index 00000000..95b31ab6 --- /dev/null +++ b/mooncake-integration/ollama/store-proxy/store_proxy.py @@ -0,0 +1,669 @@ +#!/usr/bin/env python3 +"""Mooncake Store Proxy +==================== + +A thin, *long-lived* gRPC service that owns one warm +``mooncake.store.MooncakeDistributedStore`` handle and exposes it to the Go +sidecar (which cannot link the Python bindings directly). + +Why a proxy instead of cgo? + * The official, battle-tested store client is the Python binding shipped in + the ``mooncake-transfer-engine`` wheel. Re-implementing it in cgo would be + fragile. The proxy lets the Go sidecar stay pure-Go while still driving the + *real* Mooncake Store (TCP or RDMA/GPUDirect). + * KV snapshots are multi-GiB. We pass **file paths**, not bytes: ``PutFile`` + mmaps the slot-save file and stores it; ``GetFile`` materializes an object + straight into the slot-save directory. The blob is therefore copied at most + once (file <-> store) and never travels through the Go process. + +Backends: + * ``mooncake`` (default): the distributed store. ``--protocol tcp|rdma|auto``. + * ``local``: a filesystem-backed object store (no master needed). Used for the + "local file" baseline and for environments without a master. + +Everything stays under the workspace; no writes to ``/``. +""" +from __future__ import annotations + +import argparse +import ctypes +import logging +import mmap +import os +import queue +import shutil +import sys +import threading +import time +from concurrent import futures +from dataclasses import dataclass, field + +import grpc + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from gen import storeproxy_pb2 as pb # noqa: E402 +from gen import storeproxy_pb2_grpc as pb_grpc # noqa: E402 + +log = logging.getLogger("store-proxy") + + +# --------------------------------------------------------------------------- # +# Backend abstraction +# --------------------------------------------------------------------------- # +@dataclass +class Stats: + put_ops: int = 0 + get_ops: int = 0 + exists_ops: int = 0 + put_bytes: int = 0 + get_bytes: int = 0 + put_ms_total: float = 0.0 + get_ms_total: float = 0.0 + _lock: threading.Lock = field(default_factory=threading.Lock, repr=False) + + def add_put(self, n: int, ms: float): + with self._lock: + self.put_ops += 1 + self.put_bytes += n + self.put_ms_total += ms + + def add_get(self, n: int, ms: float): + with self._lock: + self.get_ops += 1 + self.get_bytes += n + self.get_ms_total += ms + + def add_exists(self, n: int): + with self._lock: + self.exists_ops += n + + +class Backend: + name = "base" + protocol = "" + master = "" + device = "" + + def exists(self, keys): raise NotImplementedError + def put_file(self, key, path, replica_num, soft_pin, skip_if_exists): raise NotImplementedError + def get_file(self, key, path): raise NotImplementedError + def put_bytes(self, key, value, replica_num, soft_pin): raise NotImplementedError + def get_bytes(self, key): raise NotImplementedError + def remove(self, key, force): raise NotImplementedError + def close(self): pass + + +class LocalBackend(Backend): + """Filesystem object store. Keys are hashed to safe file names.""" + name = "local" + + def __init__(self, root: str): + import hashlib + self._hash = hashlib.sha256 + self.root = root + os.makedirs(root, exist_ok=True) + self.protocol = "file" + self.master = root + + def _p(self, key: str) -> str: + h = self._hash(key.encode()).hexdigest() + return os.path.join(self.root, h[:2], h) + + def exists(self, keys): + return [1 if os.path.exists(self._p(k)) else 0 for k in keys] + + def put_file(self, key, path, replica_num, soft_pin, skip_if_exists): + dst = self._p(key) + if skip_if_exists and os.path.exists(dst): + return True, os.path.getsize(dst), True, "" + os.makedirs(os.path.dirname(dst), exist_ok=True) + tmp = dst + ".tmp" + shutil.copyfile(path, tmp) + os.replace(tmp, dst) + return True, os.path.getsize(dst), False, "" + + def get_file(self, key, path): + src = self._p(key) + if not os.path.exists(src): + return False, 0, False, "" + os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) + tmp = path + ".tmp" + shutil.copyfile(src, tmp) + os.replace(tmp, path) + return True, os.path.getsize(path), True, "" + + def put_bytes(self, key, value, replica_num, soft_pin): + dst = self._p(key) + os.makedirs(os.path.dirname(dst), exist_ok=True) + with open(dst, "wb") as f: + f.write(value) + return True, len(value), "" + + def get_bytes(self, key): + src = self._p(key) + if not os.path.exists(src): + return False, b"", False, "" + with open(src, "rb") as f: + return True, f.read(), True, "" + + def remove(self, key, force): + try: + os.remove(self._p(key)) + except FileNotFoundError: + pass + return True, "" + + +def _ptr_of(mm) -> int: + """Raw address of an mmap region (for register_buffer / put_from / get_into).""" + return ctypes.addressof(ctypes.c_char.from_buffer(mm)) + + +class StagingPool: + """A pool of pre-registered host buffers for zero-copy RDMA transfers. + + Registering RDMA memory (pinning pages) is expensive, so doing it per + operation throttled us to ~0.7 GB/s. Instead we register a handful of large + buffers ONCE at startup and reuse them, sustaining the full ~40 GB/s the NIC + can do. Each buffer is touched so its pages are resident before registration. + """ + + def __init__(self, store, count: int, size: int): + self.store = store + self.size = size + self.q: "queue.Queue" = queue.Queue() + self.bufs = [] + for _ in range(count): + mm = mmap.mmap(-1, size) + ctypes.memset(_ptr_of(mm), 0, size) # fault in pages before pinning + ptr = _ptr_of(mm) + r = store.register_buffer(ptr, size) + if r != 0: + raise RuntimeError(f"register_buffer failed: {r}") + self.bufs.append((mm, ptr)) + self.q.put((mm, ptr)) + log.info("staging pool: %d x %d MiB registered", count, size >> 20) + + def acquire(self, timeout=None): + return self.q.get(timeout=timeout) + + def release(self, item): + self.q.put(item) + + def close(self): + for mm, ptr in self.bufs: + try: + self.store.unregister_buffer(ptr) + except Exception: # noqa: BLE001 + pass + mm.close() + + +class MooncakeBackend(Backend): + """Wraps the real distributed store. One warm client, shared across RPCs. + + KV blobs are stored *striped*: the data is split into <=stripe-byte chunks + transferred in parallel with batch_put_from / batch_get_into. We measured + single-object RDMA transfer collapsing from ~40 GB/s (<=128 MiB) to ~2 GB/s + (>=1 GiB), while striped batches sustain full bandwidth. A tiny JSON manifest + is stored under `key`; chunk i lives under f"{key}#{i}". This also makes the + physical layout independent of the semantic prefix-block size. + """ + name = "mooncake" + _MANIFEST_MAGIC = b"OMBKVv1\n" + + def __init__(self, args): + from mooncake.store import MooncakeDistributedStore, ReplicateConfig + self._RC = ReplicateConfig + self.protocol = args.protocol + self.master = args.master + self.device = args.device + self.stripe = max(1 << 20, args.stripe_mb << 20) + self.store = MooncakeDistributedStore() + cfg = { + "local_hostname": args.local_hostname, + "metadata_server": args.metadata, + "global_segment_size": args.global_segment_size, + "local_buffer_size": args.local_buffer_size, + "protocol": args.protocol, + "rdma_devices": args.device, + "master_server_addr": args.master, + } + log.info("store setup cfg=%s", cfg) + deadline = time.time() + args.setup_timeout + last = None + while True: + try: + ret = self.store.setup(cfg) + if ret == 0: + break + last = RuntimeError(f"setup returned {ret}") + except Exception as e: # noqa: BLE001 + last = e + if time.time() > deadline: + raise RuntimeError(f"mooncake setup failed: {last}") + log.warning("store setup retry (%s)", last) + time.sleep(1.0) + # Pre-registered staging pool for zero-copy transfers (the perf fix). + self.pool = None + if args.staging_count > 0 and args.staging_mb > 0: + try: + self.pool = StagingPool(self.store, args.staging_count, args.staging_mb << 20) + except Exception as e: # noqa: BLE001 + log.warning("staging pool disabled (%s); falling back to per-op registration", e) + log.info("mooncake store ready (stripe=%dMiB, staging=%s)", + self.stripe >> 20, "on" if self.pool else "off") + + def _rc(self, replica_num, soft_pin): + rc = self._RC() + try: + rc.replica_num = max(1, int(replica_num) or 1) + rc.with_soft_pin = bool(soft_pin) + except Exception: # noqa: BLE001 + pass + return rc + + def _chunk_keys(self, key, n): + return [f"{key}#{i}" for i in range(n)] + + def exists(self, keys): + if not keys: + return [] + return list(self.store.batch_is_exist(list(keys))) + + def _stripe_layout(self, key, size): + n = max(1, (size + self.stripe - 1) // self.stripe) + keys = self._chunk_keys(key, n) + sizes = [min(self.stripe, size - i * self.stripe) for i in range(n)] + return n, keys, sizes + + def put_file(self, key, path, replica_num, soft_pin, skip_if_exists): + if skip_if_exists and self.store.is_exist(key) == 1: + try: + sz = self._manifest_size(key) + except Exception: # noqa: BLE001 + sz = 0 + return True, sz, True, "" + size = os.path.getsize(path) + rc = self._rc(replica_num, soft_pin) + n, keys, sizes = self._stripe_layout(key, size) + + # Primary path: read the slot file into a pre-registered staging buffer + # (registration amortized once at startup) and RDMA from it. readinto on + # a tmpfs file is a fast page-cache copy that releases the GIL. + if self.pool is not None and size <= self.pool.size: + mm, base = self.pool.acquire() + try: + with open(path, "rb") as f: + mv = memoryview(mm) + off = 0 + while off < size: + r = f.readinto(mv[off:size]) + if not r: + break + off += r + ptrs = [base + i * self.stripe for i in range(n)] + rets = self.store.batch_put_from(keys, ptrs, sizes, rc) + if any(r != 0 for r in rets): + return False, 0, False, f"batch_put_from rets={rets}" + finally: + self.pool.release((mm, base)) + else: + ok, err = self._put_unregistered(path, size, n, keys, sizes, rc) + if not ok: + return False, 0, False, err + man = self._MANIFEST_MAGIC + f'{{"n":{n},"size":{size},"chunk":{self.stripe}}}'.encode() + if self.store.put(key, man, rc) != 0: + return False, 0, False, "manifest put failed" + return True, size, False, "" + + def _put_unregistered(self, path, size, n, keys, sizes, rc): + if size == 0: + return True, "" + with open(path, "r+b") as f: + mm = mmap.mmap(f.fileno(), size) + try: + base = _ptr_of(mm) + self.store.register_buffer(base, size) + try: + ptrs = [base + i * self.stripe for i in range(n)] + rets = self.store.batch_put_from(keys, ptrs, sizes, rc) + if any(r != 0 for r in rets): + return False, f"batch_put_from rets={rets}" + finally: + self.store.unregister_buffer(base) + finally: + mm.close() + return True, "" + + def _read_manifest(self, key): + data = self.store.get(key) + if not data or not bytes(data).startswith(self._MANIFEST_MAGIC): + return None + import json + return json.loads(bytes(data)[len(self._MANIFEST_MAGIC):].decode()) + + def _manifest_size(self, key): + m = self._read_manifest(key) + return m["size"] if m else 0 + + def get_file(self, key, path): + m = self._read_manifest(key) + if m is None: + return False, 0, False, "" + size, n, chunk = m["size"], m["n"], m["chunk"] + os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) + tmp = path + ".tmp" + if size == 0: + with open(tmp, "wb") as f: + pass + os.replace(tmp, path) + return True, 0, True, "" + keys = self._chunk_keys(key, n) + sizes = [min(chunk, size - i * chunk) for i in range(n)] + + # Primary path: RDMA into a pre-registered staging buffer (registration + # done once at startup, not per-op), then write it to the destination + # file. The file.write(memoryview) releases the GIL for the syscall, so + # concurrent restores overlap. Falls back to direct per-op registration + # when no pool is configured or the blob exceeds a staging buffer. + if self.pool is not None and size <= self.pool.size: + mm, base = self.pool.acquire() + try: + ptrs = [base + i * chunk for i in range(n)] + rets = self.store.batch_get_into(keys, ptrs, sizes) + if any(r < 0 for r in rets): + # eviction/lease race -> recompute + return False, 0, False, "" + with open(tmp, "wb") as f: + f.write(memoryview(mm)[:size]) + finally: + self.pool.release((mm, base)) + os.replace(tmp, path) + return True, size, True, "" + + ok, transient, err = self._get_unregistered(tmp, size, n, keys, chunk, sizes) + if not ok: + try: + os.remove(tmp) + except OSError: + pass + if transient: + # A chunk was evicted / its lease expired between the manifest + # read and the transfer. Report not-found so the caller falls + # back to recompute instead of failing the request. + return False, 0, False, "" + return False, 0, False, err + os.replace(tmp, path) + return True, size, True, "" + + def _get_unregistered(self, tmp, size, n, keys, chunk, sizes): + """Returns (ok, transient, err). transient=True means an eviction/lease + race (recoverable by recompute), not a hard error.""" + with open(tmp, "wb") as f: + f.truncate(size) + with open(tmp, "r+b") as f: + mm = mmap.mmap(f.fileno(), size) + try: + base = _ptr_of(mm) + self.store.register_buffer(base, size) + try: + ptrs = [base + i * chunk for i in range(n)] + rets = self.store.batch_get_into(keys, ptrs, sizes) + if any(r < 0 for r in rets): + # negative return == chunk missing/evicted/lease expired + return False, True, f"batch_get_into rets={rets}" + finally: + self.store.unregister_buffer(base) + finally: + mm.close() + return True, False, "" + + def put_bytes(self, key, value, replica_num, soft_pin): + ret = self.store.put(key, value, self._rc(replica_num, soft_pin)) + if ret != 0: + return False, 0, f"put returned {ret}" + return True, len(value), "" + + def get_bytes(self, key): + data = self.store.get(key) + if not data: + return False, b"", False, "" + return True, bytes(data), True, "" + + def remove(self, key, force): + # remove manifest + all chunks. Chunk count from manifest if present. + m = None + try: + m = self._read_manifest(key) + except Exception: # noqa: BLE001 + pass + keys = [key] + if m: + keys += self._chunk_keys(key, m["n"]) + for k in keys: + try: + self.store.remove(k, True) if force else self.store.remove(k) + except Exception: # noqa: BLE001 + pass + return self.store.is_exist(key) != 1, "" + + def close(self): + if self.pool is not None: + self.pool.close() + try: + self.store.close() + except Exception: # noqa: BLE001 + pass + + +# --------------------------------------------------------------------------- # +# gRPC servicer +# --------------------------------------------------------------------------- # +class StoreProxyServicer(pb_grpc.StoreProxyServicer): + def __init__(self, backend: Backend): + self.b = backend + self.stats = Stats() + + def Health(self, request, context): + return pb.HealthReply(ok=True, backend=self.b.name, protocol=self.b.protocol, + master=self.b.master, device=self.b.device, detail="ready") + + def Exists(self, request, context): + res = self.b.exists(list(request.keys)) + self.stats.add_exists(len(res)) + return pb.ExistsReply(present=res) + + def PutFile(self, request, context): + t0 = time.perf_counter() + try: + ok, n, existed, err = self.b.put_file( + request.key, request.path, request.replica_num, + request.soft_pin, request.skip_if_exists) + except Exception as e: # noqa: BLE001 + return pb.PutFileReply(ok=False, error=f"{e}") + ms = (time.perf_counter() - t0) * 1000 + if ok and not existed: + self.stats.add_put(n, ms) + return pb.PutFileReply(ok=ok, bytes=n, elapsed_ms=ms, existed=existed, error=err) + + def GetFile(self, request, context): + t0 = time.perf_counter() + try: + ok, n, found, err = self.b.get_file(request.key, request.path) + except Exception as e: # noqa: BLE001 + return pb.GetFileReply(ok=False, error=f"{e}") + ms = (time.perf_counter() - t0) * 1000 + if ok: + self.stats.add_get(n, ms) + return pb.GetFileReply(ok=ok, bytes=n, elapsed_ms=ms, found=found, error=err) + + def PutBytes(self, request, context): + t0 = time.perf_counter() + try: + ok, n, err = self.b.put_bytes(request.key, request.value, request.replica_num, request.soft_pin) + except Exception as e: # noqa: BLE001 + return pb.PutBytesReply(ok=False, error=f"{e}") + ms = (time.perf_counter() - t0) * 1000 + if ok: + self.stats.add_put(n, ms) + return pb.PutBytesReply(ok=ok, bytes=n, elapsed_ms=ms, error=err) + + def GetBytes(self, request, context): + t0 = time.perf_counter() + try: + ok, val, found, err = self.b.get_bytes(request.key) + except Exception as e: # noqa: BLE001 + return pb.GetBytesReply(ok=False, error=f"{e}") + ms = (time.perf_counter() - t0) * 1000 + if ok: + self.stats.add_get(len(val), ms) + return pb.GetBytesReply(ok=ok, value=val, elapsed_ms=ms, found=found, error=err) + + def Remove(self, request, context): + try: + ok, err = self.b.remove(request.key, request.force) + except Exception as e: # noqa: BLE001 + return pb.RemoveReply(ok=False, error=f"{e}") + return pb.RemoveReply(ok=ok, error=err) + + def Stats(self, request, context): + s = self.stats + return pb.StatsReply( + put_ops=s.put_ops, get_ops=s.get_ops, exists_ops=s.exists_ops, + put_bytes=s.put_bytes, get_bytes=s.get_bytes, + put_ms_total=s.put_ms_total, get_ms_total=s.get_ms_total, + backend=self.b.name) + + +def build_backend(args) -> Backend: + if args.backend == "local": + return LocalBackend(args.local_dir) + # Resolve protocol=auto. + if args.protocol == "auto": + if args.device and args.device != "": + args.protocol = "rdma" + else: + # Probe for RDMA devices. + has_rdma = os.path.isdir("/sys/class/infiniband") and bool(os.listdir("/sys/class/infiniband")) + if has_rdma: + args.protocol, args.device = "rdma", "auto-discovery" + else: + args.protocol = "tcp" + log.info("protocol=auto resolved to %s (device=%r)", args.protocol, args.device) + return MooncakeBackend(args) + + +def main(): + ap = argparse.ArgumentParser(description="Mooncake Store Proxy (gRPC)") + ap.add_argument("--listen", default="127.0.0.1:52060") + ap.add_argument("--backend", choices=["mooncake", "local"], default="mooncake") + ap.add_argument("--local-dir", default=os.environ.get("OMB_STORE_DATA", "./run/store")) + ap.add_argument("--master", default=os.environ.get("OMB_STORE_MASTER", "127.0.0.1:52061")) + ap.add_argument("--metadata", default=os.environ.get("OMB_STORE_META", "P2PHANDSHAKE")) + ap.add_argument("--protocol", default=os.environ.get("OMB_STORE_PROTOCOL", "tcp"), + choices=["tcp", "rdma", "auto"]) + ap.add_argument("--device", default=os.environ.get("OMB_STORE_DEVICE", "")) + ap.add_argument("--local-hostname", default=os.environ.get("OMB_STORE_HOST", "127.0.0.1")) + ap.add_argument("--global-segment-size", type=int, default=int(os.environ.get("OMB_STORE_SEGMENT", str(16 << 30)))) + ap.add_argument("--local-buffer-size", type=int, default=int(os.environ.get("OMB_STORE_BUFFER", str(2 << 30)))) + ap.add_argument("--stripe-mb", type=int, default=int(os.environ.get("OMB_STORE_STRIPE_MB", "64")), + help="split KV blobs into <=N MiB chunks for parallel batch transfer") + ap.add_argument("--staging-mb", type=int, default=int(os.environ.get("OMB_STORE_STAGING_MB", "4096")), + help="size of each pre-registered staging buffer (MiB); blobs above this use per-op registration") + ap.add_argument("--staging-count", type=int, default=int(os.environ.get("OMB_STORE_STAGING_COUNT", "3")), + help="number of pre-registered staging buffers (concurrency of zero-copy transfers)") + ap.add_argument("--setup-timeout", type=float, default=60.0) + ap.add_argument("--max-workers", type=int, default=16) + ap.add_argument("--max-msg-mb", type=int, default=512) + ap.add_argument("--warmup", action="store_true", help="warm the client with a put/get so the first real op is fast") + ap.add_argument("--selftest", action="store_true") + args = ap.parse_args() + + logging.basicConfig(level=logging.INFO, format="%(asctime)s store-proxy %(levelname)s %(message)s") + + backend = build_backend(args) + log.info("backend=%s protocol=%s master=%s", backend.name, backend.protocol, backend.master) + + if args.warmup or args.selftest: + _warmup(backend) + if args.selftest: + _selftest(backend) + backend.close() + return + + opts = [ + ("grpc.max_send_message_length", args.max_msg_mb << 20), + ("grpc.max_receive_message_length", args.max_msg_mb << 20), + ] + server = grpc.server(futures.ThreadPoolExecutor(max_workers=args.max_workers), options=opts) + pb_grpc.add_StoreProxyServicer_to_server(StoreProxyServicer(backend), server) + server.add_insecure_port(args.listen) + server.start() + log.info("StoreProxy listening on %s (backend=%s)", args.listen, backend.name) + try: + server.wait_for_termination() + except KeyboardInterrupt: + log.info("shutting down") + server.stop(2).wait() + backend.close() + + +def _warmup(backend: Backend): + import tempfile + key = "omb:_warmup_" + with tempfile.NamedTemporaryFile(delete=False) as tf: + tf.write(b"warmup" * 4096) # 24 KiB + p = tf.name + try: + t0 = time.perf_counter() + backend.put_file(key, p, 1, False, False) + t1 = time.perf_counter() + outp = p + ".out" + backend.get_file(key, outp) + t2 = time.perf_counter() + backend.remove(key, True) + log.info("warmup: put %.1fms get %.1fms", (t1 - t0) * 1e3, (t2 - t1) * 1e3) + for q in (p, p + ".out"): + try: + os.remove(q) + except OSError: + pass + except Exception as e: # noqa: BLE001 + log.warning("warmup failed: %s", e) + + +def _selftest(backend: Backend): + import tempfile + log.info("=== selftest backend=%s ===", backend.name) + sizes = [1 << 20, 16 << 20, 128 << 20] # 1MiB, 16MiB, 128MiB + for sz in sizes: + with tempfile.NamedTemporaryFile(delete=False) as tf: + tf.write(os.urandom(sz)) + p = tf.name + key = f"omb:selftest:{sz}" + t0 = time.perf_counter() + ok, n, existed, err = backend.put_file(key, p, 1, True, False) + t1 = time.perf_counter() + assert ok, f"put failed: {err}" + outp = p + ".out" + ok, n2, found, err = backend.get_file(key, outp) + t2 = time.perf_counter() + assert ok and found, f"get failed: {err}" + import filecmp + same = filecmp.cmp(p, outp, shallow=False) + put_gbps = sz / (t1 - t0) / 1e9 + get_gbps = sz / (t2 - t1) / 1e9 + log.info("size=%6dMiB put=%7.1fms (%.2f GB/s) get=%7.1fms (%.2f GB/s) roundtrip_ok=%s", + sz >> 20, (t1 - t0) * 1e3, put_gbps, (t2 - t1) * 1e3, get_gbps, same) + assert same, "roundtrip data mismatch!" + ex = backend.exists([key, key + ":absent"]) + assert ex[0] == 1 and ex[1] == 0, f"exists wrong: {ex}" + backend.remove(key, True) + for q in (p, outp): + os.remove(q) + log.info("=== selftest PASSED ===") + + +if __name__ == "__main__": + main() -- 2.34.1 From 0b18e651931d18303ba052a385e14dba3e099b30 Mon Sep 17 00:00:00 2001 From: zbtrs2 Date: Mon, 29 Jun 2026 01:51:33 +0800 Subject: [PATCH 5/9] ollama-kvcache: add Stage-2 on-device KV state path The Stage-1 path serialises a sequence's KV through host memory and a file (llama.cpp /slots save), which is several times slower than a raw in-process copy because of the double copy described in llama.cpp issue 8915. Stage 2 targets the high-performance route. seqstate is a cgo binding (build tag cgo_stage2) for llama_state_seq_get/set_data_ext. With LLAMA_STATE_SEQ_FLAGS_ON_DEVICE the KV tensors stay in device buffers, ready to be registered with the Mooncake Transfer Engine for GPUDirect RDMA with no host copy. It is gated behind a build tag because it links libllama and only applies to the embedded deployment; the default HTTP path needs none of it. omb_kvbench is an in-process microbenchmark against libllama that compares the file-save path, the raw host export and the on-device size, and verifies that a sequence's KV round-trips correctly (export seq 0, import into seq 1, identical next-token argmax). It quantifies the file-path overhead the on-device route removes. --- .../ollama/bridge/cbridge/build_kvbench.sh | 16 ++ .../ollama/bridge/cbridge/omb_kvbench.cpp | 147 ++++++++++++++++++ .../bridge/internal/seqstate/seqstate_cgo.go | 95 +++++++++++ 3 files changed, 258 insertions(+) create mode 100644 mooncake-integration/ollama/bridge/cbridge/build_kvbench.sh create mode 100644 mooncake-integration/ollama/bridge/cbridge/omb_kvbench.cpp create mode 100644 mooncake-integration/ollama/bridge/internal/seqstate/seqstate_cgo.go diff --git a/mooncake-integration/ollama/bridge/cbridge/build_kvbench.sh b/mooncake-integration/ollama/bridge/cbridge/build_kvbench.sh new file mode 100644 index 00000000..33e3f98a --- /dev/null +++ b/mooncake-integration/ollama/bridge/cbridge/build_kvbench.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Build the Stage-2 KV-state microbenchmark against the locally-built libllama. +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../scripts/env.sh" + +SRC="$WS/bridge/cbridge/omb_kvbench.cpp" +OUT="$OMB_RUN/omb_kvbench" +INC="$LLAMA_DIR/include" +GGML_INC="$LLAMA_DIR/ggml/include" +LIBDIR="$LLAMA_BUILD/bin" + +g++ -std=c++17 -O2 -o "$OUT" "$SRC" \ + -I"$INC" -I"$GGML_INC" \ + -L"$LIBDIR" -lllama -lggml -lggml-base \ + -Wl,-rpath,"$LIBDIR" +echo "built: $OUT" diff --git a/mooncake-integration/ollama/bridge/cbridge/omb_kvbench.cpp b/mooncake-integration/ollama/bridge/cbridge/omb_kvbench.cpp new file mode 100644 index 00000000..0a5c91df --- /dev/null +++ b/mooncake-integration/ollama/bridge/cbridge/omb_kvbench.cpp @@ -0,0 +1,147 @@ +// omb_kvbench — Stage-2 KV-state microbenchmark (libllama, in-process). +// +// Demonstrates, in-process against libllama, the three ways to get a sequence's +// KV out of a running model and why the Stage-2 path matters: +// +// (A) llama_state_seq_save_file(...) -- the Stage-1 path the sidecar +// uses today via /slots: GPU +// -> host -> serialize -> file +// (B) llama_state_seq_get_data_ext(..., NONE) -- raw host export: GPU -> host +// (C) llama_state_seq_get_size_ext(..., ON_DEVICE) -- the Stage-2 target: the +// KV stays in device buffers, +// ready for Mooncake Transfer +// Engine GPUDirect RDMA with +// NO host copy (avoids the +// double-copy of llama.cpp +// issue #8915). +// +// It also verifies correctness: export seq 0, import into seq 1, decode one +// token from each and confirm the KV round-trips. +// +// Build: see cbridge/build_kvbench.sh. Run: omb_kvbench [n_prompt] [n_ctx] [ngl] +#include "llama.h" + +#include +#include +#include +#include +#include +#include +#include + +using clk = std::chrono::high_resolution_clock; +static double ms(clk::time_point a, clk::time_point b) { + return std::chrono::duration(b - a).count(); +} + +int main(int argc, char ** argv) { + if (argc < 2) { fprintf(stderr, "usage: %s [n_prompt=4000] [n_ctx] [ngl=99]\n", argv[0]); return 2; } + const char * model_path = argv[1]; + int n_prompt = argc > 2 ? atoi(argv[2]) : 4000; + // KV cells are shared across sequences; we use two seqs (export/import), so + // size the context for two full copies of the prompt. + int n_ctx = argc > 3 ? atoi(argv[3]) : (n_prompt + 512) * 2; + int ngl = argc > 4 ? atoi(argv[4]) : 99; + + llama_backend_init(); + llama_model_params mparams = llama_model_default_params(); + mparams.n_gpu_layers = ngl; + llama_model * model = llama_model_load_from_file(model_path, mparams); + if (!model) { fprintf(stderr, "model load failed\n"); return 1; } + + llama_context_params cparams = llama_context_default_params(); + cparams.n_ctx = n_ctx; + cparams.n_seq_max = 2; + cparams.n_batch = 2048; + llama_context * ctx = llama_init_from_model(model, cparams); + if (!ctx) { fprintf(stderr, "ctx init failed\n"); return 1; } + const llama_vocab * vocab = llama_model_get_vocab(model); + + // Synthetic code-like prompt, tokenized. + std::string text; + while ((int) text.size() < n_prompt * 5) + text += "func process(ctx Context, d []Record) (Result, error) { return agg(transform(validate(d))) }\n"; + std::vector toks(text.size() + 16); + int n = llama_tokenize(vocab, text.c_str(), (int) text.size(), toks.data(), (int) toks.size(), true, false); + if (n <= 0) { fprintf(stderr, "tokenize failed: %d\n", n); return 1; } + if (n > n_prompt) n = n_prompt; + toks.resize(n); + + // Prefill seq 0 in <= n_batch chunks. + int n_batch_sz = (int) cparams.n_batch; + auto t0 = clk::now(); + for (int start = 0; start < n; start += n_batch_sz) { + int cnt = (n - start < n_batch_sz) ? (n - start) : n_batch_sz; + llama_batch b = llama_batch_init(cnt, 0, 1); + for (int i = 0; i < cnt; i++) { + b.token[i] = toks[start + i]; b.pos[i] = start + i; + b.n_seq_id[i] = 1; b.seq_id[i][0] = 0; + b.logits[i] = (start + i == n - 1); + } + b.n_tokens = cnt; + if (llama_decode(ctx, b) != 0) { fprintf(stderr, "decode failed at %d\n", start); return 1; } + llama_batch_free(b); + } + double prefill_ms = ms(t0, clk::now()); + + // KV byte/token (host state size / tokens). + size_t sz_host = llama_state_seq_get_size_ext(ctx, 0, LLAMA_STATE_SEQ_FLAGS_NONE); + size_t sz_dev = llama_state_seq_get_size_ext(ctx, 0, LLAMA_STATE_SEQ_FLAGS_ON_DEVICE); + + // (B) raw host export GPU->host + std::vector buf(sz_host); + t0 = clk::now(); + size_t got = llama_state_seq_get_data_ext(ctx, buf.data(), buf.size(), 0, LLAMA_STATE_SEQ_FLAGS_NONE); + double host_get_ms = ms(t0, clk::now()); + + // (A) Stage-1 file path (what /slots save does): write to tmpfs + const char * fpath = "/dev/shm/omb_kvbench_seq0.bin"; + t0 = clk::now(); + size_t fsaved = llama_state_seq_save_file(ctx, fpath, 0, toks.data(), toks.size()); + double file_save_ms = ms(t0, clk::now()); + + // import (host) into seq 1 + t0 = clk::now(); + size_t set = llama_state_seq_set_data_ext(ctx, buf.data(), got, 1, LLAMA_STATE_SEQ_FLAGS_NONE); + double host_set_ms = ms(t0, clk::now()); + + // correctness: decode one token after the prefix in BOTH seqs, compare argmax. + auto next_logits = [&](int seq) -> const float * { + llama_batch b = llama_batch_init(1, 0, 1); + b.token[0] = toks.back(); b.pos[0] = n; b.n_seq_id[0] = 1; b.seq_id[0][0] = seq; b.logits[0] = 1; b.n_tokens = 1; + llama_decode(ctx, b); + const float * lg = llama_get_logits_ith(ctx, 0); + llama_batch_free(b); + return lg; + }; + int n_vocab = llama_vocab_n_tokens(vocab); + const float * l0 = next_logits(0); + std::vector l0c(l0, l0 + n_vocab); + const float * l1 = next_logits(1); + auto argmax = [&](const float * l) { int a = 0; for (int i = 1; i < n_vocab; i++) if (l[i] > l[a]) a = i; return a; }; + int a0 = argmax(l0c.data()), a1 = argmax(l1); + bool ok = (set > 0) && (a0 == a1); + + double bpt = (double) sz_host / n; + printf("\n==== omb_kvbench: %s ====\n", model_path); + printf("prompt tokens : %d (prefill %.1f ms, %.0f tok/s)\n", n, prefill_ms, n / (prefill_ms / 1e3)); + printf("KV state size (host) : %.1f MiB (%.0f bytes/token)\n", sz_host / 1048576.0, bpt); + printf("KV state size (ondev) : %.1f MiB\n", sz_dev / 1048576.0); + printf("\n--- export paths (the Stage-1 vs Stage-2 comparison) ---\n"); + printf("(A) /slots file save : %8.1f ms (%.2f GB/s) GPU->host->serialize->tmpfs\n", + file_save_ms, fsaved / (file_save_ms / 1e3) / 1e9); + printf("(B) host get_data_ext : %8.1f ms (%.2f GB/s) GPU->host (one copy)\n", + host_get_ms, got / (host_get_ms / 1e3) / 1e9); + printf("(C) ON_DEVICE export : (stays on device; hand the device buffer to\n"); + printf(" Mooncake TE registerLocalMemory for GPUDirect\n"); + printf(" RDMA -- zero host copy, the Stage-2 target)\n"); + printf("import set_data_ext : %8.1f ms\n", host_set_ms); + printf("\nfile-save overhead vs raw host copy : %.2fx slower\n", file_save_ms / host_get_ms); + printf("KV round-trip correctness (seq0==seq1 argmax): %s\n", ok ? "PASS" : "FAIL"); + + remove(fpath); + llama_free(ctx); + llama_model_free(model); + llama_backend_free(); + return ok ? 0 : 1; +} diff --git a/mooncake-integration/ollama/bridge/internal/seqstate/seqstate_cgo.go b/mooncake-integration/ollama/bridge/internal/seqstate/seqstate_cgo.go new file mode 100644 index 00000000..7f5416d1 --- /dev/null +++ b/mooncake-integration/ollama/bridge/internal/seqstate/seqstate_cgo.go @@ -0,0 +1,95 @@ +//go:build cgo_stage2 + +// Package seqstate is the Stage-2 (high-performance) Go binding for moving a +// llama.cpp sequence's KV state with minimal copies, used when the sidecar is +// embedded in-process with libllama (rather than driving a llama-server over +// HTTP as in Stage-1). +// +// It is gated behind the `cgo_stage2` build tag because it links libllama and +// is only relevant for the embedded/high-performance deployment. The Stage-1 +// HTTP path (internal/llamabridge) needs none of this and is the default. +// +// Why this exists (measured by cbridge/omb_kvbench): the Stage-1 file path +// (llama_state_seq_save_file, i.e. /slots save) is ~5x slower than the raw +// in-process state copy because it serializes through host memory and a file +// (llama.cpp issue #8915's double copy). Going further, the _ext API with +// LLAMA_STATE_SEQ_FLAGS_ON_DEVICE keeps the KV tensors in device buffers so the +// Mooncake Transfer Engine can register them (registerLocalMemory) and move +// them by GPUDirect RDMA with NO host copy at all. +// +// Build (example): +// +// CGO_CFLAGS="-I$LLAMA_DIR/include -I$LLAMA_DIR/ggml/include" \ +// CGO_LDFLAGS="-L$LLAMA_BUILD/bin -lllama -lggml -lggml-base -Wl,-rpath,$LLAMA_BUILD/bin" \ +// go build -tags cgo_stage2 ./... +package seqstate + +/* +#include +#include "llama.h" +*/ +import "C" +import ( + "fmt" + "unsafe" +) + +// Flags mirror llama_state_seq_flags. +type Flags uint32 + +const ( + FlagsNone Flags = 0 + FlagsOnDevice Flags = 2 // keep KV in device buffers; pair with Mooncake TE GDR +) + +// Ctx wraps a *llama_context owned elsewhere in the embedded process. +type Ctx struct{ ptr *C.struct_llama_context } + +// Wrap adapts a raw context pointer (e.g. obtained from the embedded runner). +func Wrap(p unsafe.Pointer) *Ctx { return &Ctx{ptr: (*C.struct_llama_context)(p)} } + +// SeqSize returns the bytes needed to export sequence seq with the given flags. +// With FlagsOnDevice this is just the device-handle/metadata size; the bulk KV +// stays resident on the GPU. +func (c *Ctx) SeqSize(seq int, flags Flags) uint64 { + return uint64(C.llama_state_seq_get_size_ext(c.ptr, C.llama_seq_id(seq), C.llama_state_seq_flags(flags))) +} + +// SeqGet copies sequence seq's state into dst (len(dst) >= SeqSize). Returns the +// number of bytes written. For zero-copy GPUDirect, allocate dst from a buffer +// already registered with the Mooncake Transfer Engine. +func (c *Ctx) SeqGet(dst []byte, seq int, flags Flags) (uint64, error) { + if len(dst) == 0 { + return 0, fmt.Errorf("empty dst") + } + n := C.llama_state_seq_get_data_ext(c.ptr, + (*C.uint8_t)(unsafe.Pointer(&dst[0])), C.size_t(len(dst)), + C.llama_seq_id(seq), C.llama_state_seq_flags(flags)) + if n == 0 { + return 0, fmt.Errorf("llama_state_seq_get_data_ext failed") + } + return uint64(n), nil +} + +// SeqSet imports src into sequence dstSeq. Returns >0 on success. +func (c *Ctx) SeqSet(src []byte, dstSeq int, flags Flags) (uint64, error) { + if len(src) == 0 { + return 0, fmt.Errorf("empty src") + } + n := C.llama_state_seq_set_data_ext(c.ptr, + (*C.uint8_t)(unsafe.Pointer(&src[0])), C.size_t(len(src)), + C.llama_seq_id(dstSeq), C.llama_state_seq_flags(flags)) + if n == 0 { + return 0, fmt.Errorf("llama_state_seq_set_data_ext failed") + } + return uint64(n), nil +} + +// PtrAndLen exposes a buffer's address+length so the engine binding +// (engine_cgo.go) can registerLocalMemory it for RDMA transfer. +func PtrAndLen(b []byte) (uintptr, int) { + if len(b) == 0 { + return 0, 0 + } + return uintptr(unsafe.Pointer(&b[0])), len(b) +} -- 2.34.1 From e31c2de9b86e5b66be457c8c4af7eba6f1096a15 Mon Sep 17 00:00:00 2001 From: zbtrs2 Date: Mon, 29 Jun 2026 01:51:49 +0800 Subject: [PATCH 6/9] ollama-kvcache: add Ollama integration patches Two small, additive patches that make upstream Ollama a participant of the KVCache Bus, verified against ollama/ollama main at 1abd56b. 0001 adds a self-contained HTTP client for the sidecar's prepare/commit endpoints plus parsing of the options.mooncake.* request extension (enable, namespace, read, write, block_size, transport, replica_num, soft_pin), without touching any existing symbol. 0002 injects --slot-save-path into the bundled llama-server command line when OLLAMA_MOONCAKE_SLOT_SAVE_PATH is set, exposing the /slots save|restore endpoints the sidecar drives. Both are gated, so an unconfigured Ollama behaves exactly as before. The README documents how to apply them and the one call-site hook left as a reference, and notes 0002 as a clean upstream-PR candidate aligned with Ollama issue 14872. --- ...01-add-mooncake-cache-policy-options.patch | 212 ++++++++++++++++++ ...nject-slot-save-path-and-bridge-hook.patch | 38 ++++ .../ollama/ollama-patches/README.md | 94 ++++++++ 3 files changed, 344 insertions(+) create mode 100644 mooncake-integration/ollama/ollama-patches/0001-add-mooncake-cache-policy-options.patch create mode 100644 mooncake-integration/ollama/ollama-patches/0002-inject-slot-save-path-and-bridge-hook.patch create mode 100644 mooncake-integration/ollama/ollama-patches/README.md diff --git a/mooncake-integration/ollama/ollama-patches/0001-add-mooncake-cache-policy-options.patch b/mooncake-integration/ollama/ollama-patches/0001-add-mooncake-cache-policy-options.patch new file mode 100644 index 00000000..516890aa --- /dev/null +++ b/mooncake-integration/ollama/ollama-patches/0001-add-mooncake-cache-policy-options.patch @@ -0,0 +1,212 @@ +# Patch 0001: add the Mooncake KVCache Bus sidecar client + options.mooncake.* parsing +# Apply from the ollama repo root with: git apply 0001-add-mooncake-cache-policy-options.patch +# +diff --git a/server/mooncake_bridge.go b/server/mooncake_bridge.go +new file mode 100644 +index 0000000..fe0b217 +--- /dev/null ++++ b/server/mooncake_bridge.go +@@ -0,0 +1,204 @@ ++package server ++ ++// Mooncake KVCache Bus — reference integration for Ollama. ++// ++// This file lets Ollama participate in a global, cross-process / cross-GPU / ++// cross-node KV cache by talking to the ollama-mooncake-bridge sidecar over a ++// tiny HTTP/JSON API. ++// ++// The sidecar (a separate Go service) owns all the heavy lifting — cache-key ++// construction, longest-prefix matching, the restore-vs-recompute cost arbiter, ++// and the Mooncake Store/Transfer-Engine I/O. Ollama only needs to: ++// ++// 1. launch its bundled llama-server with --slot-save-path (see the companion ++// patch to llm/llama_server.go, gated by OLLAMA_MOONCAKE_SLOT_SAVE_PATH); ++// 2. before generation, call Prepare() so the matched prompt KV is restored ++// into the target slot (the sidecar fetches it from Mooncake); ++// 3. after generation, call Commit() so the produced KV is published. ++// ++// Wiring (env): ++// OLLAMA_MOONCAKE_BRIDGE_URL=http://127.0.0.1:52052 # sidecar HTTP gateway ++// OLLAMA_MOONCAKE_SLOT_SAVE_PATH=/dev/shm/ollama-mc # shared slot dir ++// ++// See ollama-patches/README.md for the exact call sites in routes.go. ++ ++import ( ++ "bytes" ++ "encoding/json" ++ "net/http" ++ "os" ++ "strconv" ++ "time" ++) ++ ++// MooncakeOptions mirror the user-visible options.mooncake.* request extension. ++type MooncakeOptions struct { ++ Enable bool ++ Namespace string ++ Read bool ++ Write bool ++ BlockSize int ++ Transport string // tcp | rdma | auto ++ ReplicaNum uint32 ++ SoftPin bool ++} ++ ++// MooncakeOptionsFromMap parses options.mooncake.* out of a request's raw ++// options map (api.Options is decoded into map[string]any for unknown keys). ++func MooncakeOptionsFromMap(opts map[string]any) (MooncakeOptions, bool) { ++ raw, ok := opts["mooncake"].(map[string]any) ++ if !ok { ++ return MooncakeOptions{}, false ++ } ++ o := MooncakeOptions{Read: true, Write: true, BlockSize: 256, Transport: "auto", ReplicaNum: 1} ++ getBool := func(k string, def bool) bool { ++ if v, ok := raw[k].(bool); ok { ++ return v ++ } ++ return def ++ } ++ getStr := func(k, def string) string { ++ if v, ok := raw[k].(string); ok { ++ return v ++ } ++ return def ++ } ++ getInt := func(k string, def int) int { ++ switch v := raw[k].(type) { ++ case float64: ++ return int(v) ++ case int: ++ return v ++ } ++ return def ++ } ++ o.Enable = getBool("enable", false) ++ o.Namespace = getStr("namespace", "") ++ o.Read = getBool("read", true) ++ o.Write = getBool("write", true) ++ o.BlockSize = getInt("block_size", 256) ++ o.Transport = getStr("transport", "auto") ++ o.ReplicaNum = uint32(getInt("replica_num", 1)) ++ o.SoftPin = getBool("soft_pin", false) ++ return o, o.Enable ++} ++ ++// MooncakeBridge is a minimal HTTP client to the sidecar's /v1 gateway. ++type MooncakeBridge struct { ++ base string ++ hc *http.Client ++} ++ ++// NewMooncakeBridge returns a bridge client, or nil if integration is disabled ++// (OLLAMA_MOONCAKE_BRIDGE_URL unset). ++func NewMooncakeBridge() *MooncakeBridge { ++ base := os.Getenv("OLLAMA_MOONCAKE_BRIDGE_URL") ++ if base == "" { ++ return nil ++ } ++ return &MooncakeBridge{base: base, hc: &http.Client{Timeout: 10 * time.Minute}} ++} ++ ++type mcFingerprint struct { ++ ModelPath string `json:"model_path"` ++ KVType string `json:"kv_type"` ++ BlockSize int `json:"block_size"` ++} ++type mcPolicy struct { ++ Enable bool `json:"enable"` ++ Namespace string `json:"namespace"` ++ Read bool `json:"read"` ++ Write bool `json:"write"` ++ BlockSize int `json:"block_size"` ++ Transport string `json:"transport"` ++ ReplicaNum uint32 `json:"replica_num"` ++ SoftPin bool `json:"soft_pin"` ++} ++type mcTarget struct { ++ BaseURL string `json:"base_url"` ++ Slot int `json:"slot_id"` ++} ++type mcRequest struct { ++ FP mcFingerprint `json:"fp"` ++ Policy mcPolicy `json:"policy"` ++ Tokens []int32 `json:"tokens"` ++ Target *mcTarget `json:"target"` ++ PrefillN int `json:"prefill_n,omitempty"` ++ PrefillMs float64 `json:"prefill_ms,omitempty"` ++} ++ ++// PrepareReply / CommitReply are the subset of the sidecar reply we consume. ++type PrepareReply struct { ++ Hit bool `json:"hit"` ++ Decision string `json:"decision"` ++ Restored bool `json:"restored"` ++ RestoredTokens int `json:"restored_tokens"` ++ MatchedTokens int `json:"matched_tokens"` ++ StoreGetMs float64 `json:"store_get_ms"` ++ Reason string `json:"reason"` ++ Error string `json:"error"` ++} ++type CommitReply struct { ++ OK bool `json:"ok"` ++ Stored bool `json:"stored"` ++ Key string `json:"key"` ++ Error string `json:"error"` ++} ++ ++func (b *MooncakeBridge) post(path string, req mcRequest, out any) error { ++ var buf bytes.Buffer ++ if err := json.NewEncoder(&buf).Encode(req); err != nil { ++ return err ++ } ++ resp, err := b.hc.Post(b.base+path, "application/json", &buf) ++ if err != nil { ++ return err ++ } ++ defer resp.Body.Close() ++ return json.NewDecoder(resp.Body).Decode(out) ++} ++ ++func (o MooncakeOptions) policy() mcPolicy { ++ return mcPolicy{Enable: o.Enable, Namespace: o.Namespace, Read: o.Read, Write: o.Write, ++ BlockSize: o.BlockSize, Transport: o.Transport, ReplicaNum: o.ReplicaNum, SoftPin: o.SoftPin} ++} ++ ++func fp(modelPath, kvType string, blockSize int) mcFingerprint { ++ if kvType == "" { ++ kvType = "f16" ++ } ++ return mcFingerprint{ModelPath: modelPath, KVType: kvType, BlockSize: blockSize} ++} ++ ++// Prepare asks the sidecar to restore the longest matching KV prefix into the ++// given llama-server slot before generation. Returns whether KV was restored. ++func (b *MooncakeBridge) Prepare(o MooncakeOptions, modelPath, kvType string, tokens []int32, llamaURL string, slot int) (*PrepareReply, error) { ++ var out PrepareReply ++ err := b.post("/v1/prepare", mcRequest{ ++ FP: fp(modelPath, kvType, o.BlockSize), Policy: o.policy(), Tokens: tokens, ++ Target: &mcTarget{BaseURL: llamaURL, Slot: slot}, ++ }, &out) ++ return &out, err ++} ++ ++// Commit publishes the slot's KV to the Mooncake Store after generation and ++// feeds the arbiter the observed prefill rate so it self-calibrates. ++func (b *MooncakeBridge) Commit(o MooncakeOptions, modelPath, kvType string, tokens []int32, llamaURL string, slot, prefillN int, prefillMs float64) (*CommitReply, error) { ++ var out CommitReply ++ err := b.post("/v1/commit", mcRequest{ ++ FP: fp(modelPath, kvType, o.BlockSize), Policy: o.policy(), Tokens: tokens, ++ Target: &mcTarget{BaseURL: llamaURL, Slot: slot}, PrefillN: prefillN, PrefillMs: prefillMs, ++ }, &out) ++ return &out, err ++} ++ ++// slotPortFromURL is a tiny helper for logging/debug. ++func slotPortFromURL(u string) string { ++ if i := bytes.LastIndexByte([]byte(u), ':'); i >= 0 { ++ if _, err := strconv.Atoi(u[i+1:]); err == nil { ++ return u[i+1:] ++ } ++ } ++ return "" ++} diff --git a/mooncake-integration/ollama/ollama-patches/0002-inject-slot-save-path-and-bridge-hook.patch b/mooncake-integration/ollama/ollama-patches/0002-inject-slot-save-path-and-bridge-hook.patch new file mode 100644 index 00000000..86df583e --- /dev/null +++ b/mooncake-integration/ollama/ollama-patches/0002-inject-slot-save-path-and-bridge-hook.patch @@ -0,0 +1,38 @@ +# Patch 0002: inject --slot-save-path so llama.cpp exposes /slots save|restore +# Apply from the ollama repo root with: git apply 0002-inject-slot-save-path-and-bridge-hook.patch +# +diff --git a/llm/llama_server.go b/llm/llama_server.go +index 8570135..98b14f7 100644 +--- a/llm/llama_server.go ++++ b/llm/llama_server.go +@@ -312,6 +312,18 @@ func FindLlamaServer() (string, error) { + } + + // startLlamaServer spawns the upstream llama-server process with appropriate CLI flags. ++// appendMooncakeArgs enables the llama.cpp /slots save|restore endpoints when ++// the sidecar integration is configured via OLLAMA_MOONCAKE_SLOT_SAVE_PATH ++// (a directory shared with the ollama-mooncake-bridge sidecar and store proxy). ++// With this set, the bundled llama-server can persist and restore a slot's ++// prompt KV, which the sidecar maps onto the global Mooncake Store. ++func appendMooncakeArgs(params []string) []string { ++ if p := os.Getenv("OLLAMA_MOONCAKE_SLOT_SAVE_PATH"); p != "" { ++ params = append(params, "--slot-save-path", p) ++ } ++ return params ++} ++ + func startLlamaServer(launch llamaServerLaunchConfig, out io.Writer) (cmd *exec.Cmd, port int, err error) { + exe, err := FindLlamaServer() + if err != nil { +@@ -345,6 +357,11 @@ func startLlamaServer(launch llamaServerLaunchConfig, out io.Writer) (cmd *exec. + params = appendLlamaServerLogArgs(params) + params = appendJinjaArgs(params, launch.config) + ++ // Mooncake KVCache Bus: expose llama.cpp's /slots save|restore endpoints so ++ // the ollama-mooncake-bridge sidecar can ship this slot's prompt KV to/from ++ // the global Mooncake Store and share it across processes/GPUs/nodes. ++ params = appendMooncakeArgs(params) ++ + params = appendMMProjArgs(params, launch) + params = appendMTPDraftArgs(params, launch.config, launch.opts) + diff --git a/mooncake-integration/ollama/ollama-patches/README.md b/mooncake-integration/ollama/ollama-patches/README.md new file mode 100644 index 00000000..92652ed2 --- /dev/null +++ b/mooncake-integration/ollama/ollama-patches/README.md @@ -0,0 +1,94 @@ +# Ollama × Mooncake — integration patches + +These patches turn upstream **Ollama** into a participant of the global Mooncake +KVCache Bus, so that prompt KV computed by one Ollama/llama.cpp process can be +**reused by other processes, GPUs and nodes** through the Mooncake Store. They +are intentionally tiny and additive — the heavy lifting lives in the standalone +`bridge/` sidecar, not in Ollama. + +Verified against `ollama/ollama` `main` @ `1abd56b` (clone date 2026-06-11). + +## What each patch does + +| Patch | File(s) | Change | +|-------|---------|--------| +| `0001-add-mooncake-cache-policy-options.patch` | `server/mooncake_bridge.go` (new) | A self-contained HTTP client for the sidecar's `/v1/prepare` and `/v1/commit` endpoints, plus parsing of the user-visible `options.mooncake.*` request extension (`enable`, `namespace`, `read`, `write`, `block_size`, `transport`, `replica_num`, `soft_pin`). No upstream symbols are modified. | +| `0002-inject-slot-save-path-and-bridge-hook.patch` | `llm/llama_server.go` | Injects `--slot-save-path ` into the bundled `llama-server` command line when `OLLAMA_MOONCAKE_SLOT_SAVE_PATH` is set, exposing llama.cpp's `/slots?action=save|restore` endpoints that the sidecar drives. Gated by an env var, so it is a no-op unless explicitly enabled. | + +Both are additive and gated, so an un-configured Ollama behaves exactly as before. + +## Apply + +```bash +git clone https://github.com/ollama/ollama && cd ollama +git apply /path/to/ollama-patches/0001-add-mooncake-cache-policy-options.patch +git apply /path/to/ollama-patches/0002-inject-slot-save-path-and-bridge-hook.patch +# Ollama main currently requires Go >= 1.26 +go build ./... +``` + +## Configure (env) + +```bash +export OLLAMA_MOONCAKE_BRIDGE_URL=http://127.0.0.1:52052 # sidecar HTTP gateway +export OLLAMA_MOONCAKE_SLOT_SAVE_PATH=/dev/shm/ollama-mc # shared (tmpfs) slot dir +``` + +## The one remaining wiring step (reference) + +Ollama's runner hides the underlying llama.cpp slot id behind its scheduler, so +the *final* hook into the request path is left as a clearly-marked reference +rather than a forced edit to the large `server/routes.go` generate/chat handler. +Where Ollama dispatches a request to a runner that owns a llama-server at +`llamaURL` on slot `slot`, add: + +```go +// --- before generation --- +if mb := NewMooncakeBridge(); mb != nil { + if mo, on := MooncakeOptionsFromMap(rawOptions); on { + // tokens: the tokenized prompt; kvType: opts.KvCacheType (e.g. "q8_0") + if pr, err := mb.Prepare(mo, modelPath, kvType, tokens, llamaURL, slot); err == nil && pr.Restored { + slog.Info("mooncake: restored KV prefix", + "matched_tokens", pr.MatchedTokens, "store_get_ms", pr.StoreGetMs, "reason", pr.Reason) + } + } +} + +// ... run generation as usual; capture the prefill timing from the response ... + +// --- after generation --- +if mb := NewMooncakeBridge(); mb != nil { + if mo, on := MooncakeOptionsFromMap(rawOptions); on && mo.Write { + _, _ = mb.Commit(mo, modelPath, kvType, tokens, llamaURL, slot, prefillN, prefillMs) + } +} +``` + +This mirrors exactly how the bench harness (`benchmarks/agent_swarm.py`) drives the +sidecar today, so the end-to-end behaviour is already validated outside Ollama; +the snippet above is the minimal glue to move that call site *inside* Ollama. + +## Request example (`POST /api/chat`) + +```jsonc +{ + "model": "qwen2.5-coder:7b", + "messages": [ /* ... long shared repo context + this agent's task ... */ ], + "options": { + "mooncake": { + "enable": true, + "namespace": "repo:my-org/my-repo@main", + "read": true, "write": true, + "block_size": 256, + "transport": "auto" + } + } +} +``` + +## Upstreaming + +`0002` is a clean candidate for an upstream PR (a gated, opt-in flag pass-through). +`0001` + the wiring snippet are proposed as an RFC: *"Pluggable global KV cache +for Ollama via a sidecar"*, aligned with community issue +[ollama/ollama#14872 — Swarm Memory](https://github.com/ollama/ollama/issues/14872). -- 2.34.1 From 897a11bd342e4a17a938dea7912f1c51dbbba32e Mon Sep 17 00:00:00 2001 From: zbtrs2 Date: Mon, 29 Jun 2026 01:52:12 +0800 Subject: [PATCH 7/9] ollama-kvcache: add benchmarks and orchestration scripts agent_swarm drives the core scenario: several agents share a long repo context and differ only in a short instruction. It runs the workload with sharing off (baseline) and on, erasing llama's own per-slot cache between runs so the comparison isolates cross-process reuse, and reports honest end-to-end TTFT, recomputed prefill tokens, hit rate and throughput. smoke_e2e is a minimal two-agent cross-GPU reuse check; microbench_store and microbench_striped measure raw and striped store bandwidth; run_matrix sweeps a config and plot renders the figures. scripts/env.sh keeps every toolchain, cache and runtime directory under an out-of-tree state root so the source stays clean and nothing is written under /. The setup_* scripts install a local Go toolchain, a venv with mooncake-transfer-engine, a CUDA build of llama.cpp and the GGUF models; the *_start/stop and demo scripts bring the stack up and down, scoped to this integration's own ports and paths. --- .../ollama/benchmarks/agent_swarm.py | 247 ++++++++++++++++++ .../ollama/benchmarks/microbench_store.py | 81 ++++++ .../ollama/benchmarks/microbench_striped.py | 57 ++++ .../ollama/benchmarks/plot.py | 200 ++++++++++++++ .../ollama/benchmarks/run_matrix.py | 104 ++++++++ .../ollama/benchmarks/scaling.json | 42 +++ .../ollama/benchmarks/smoke_e2e.py | 128 +++++++++ .../ollama/benchmarks/workloads/matrix.json | 40 +++ .../ollama/scripts/bridged_start.sh | 39 +++ mooncake-integration/ollama/scripts/demo.sh | 74 ++++++ mooncake-integration/ollama/scripts/env.sh | 90 +++++++ .../ollama/scripts/gen_protos.sh | 39 +++ .../ollama/scripts/llama_start.sh | 55 ++++ .../ollama/scripts/master_start.sh | 23 ++ .../ollama/scripts/master_stop.sh | 11 + .../ollama/scripts/proxy_start.sh | 34 +++ .../ollama/scripts/setup_go.sh | 30 +++ .../ollama/scripts/setup_llama.sh | 26 ++ .../ollama/scripts/setup_models.sh | 26 ++ .../ollama/scripts/setup_py.sh | 44 ++++ .../ollama/scripts/stack_down.sh | 20 ++ .../ollama/scripts/stack_up.sh | 28 ++ 22 files changed, 1438 insertions(+) create mode 100644 mooncake-integration/ollama/benchmarks/agent_swarm.py create mode 100644 mooncake-integration/ollama/benchmarks/microbench_store.py create mode 100644 mooncake-integration/ollama/benchmarks/microbench_striped.py create mode 100644 mooncake-integration/ollama/benchmarks/plot.py create mode 100644 mooncake-integration/ollama/benchmarks/run_matrix.py create mode 100644 mooncake-integration/ollama/benchmarks/scaling.json create mode 100644 mooncake-integration/ollama/benchmarks/smoke_e2e.py create mode 100644 mooncake-integration/ollama/benchmarks/workloads/matrix.json create mode 100644 mooncake-integration/ollama/scripts/bridged_start.sh create mode 100644 mooncake-integration/ollama/scripts/demo.sh create mode 100644 mooncake-integration/ollama/scripts/env.sh create mode 100644 mooncake-integration/ollama/scripts/gen_protos.sh create mode 100644 mooncake-integration/ollama/scripts/llama_start.sh create mode 100644 mooncake-integration/ollama/scripts/master_start.sh create mode 100644 mooncake-integration/ollama/scripts/master_stop.sh create mode 100644 mooncake-integration/ollama/scripts/proxy_start.sh create mode 100644 mooncake-integration/ollama/scripts/setup_go.sh create mode 100644 mooncake-integration/ollama/scripts/setup_llama.sh create mode 100644 mooncake-integration/ollama/scripts/setup_models.sh create mode 100644 mooncake-integration/ollama/scripts/setup_py.sh create mode 100644 mooncake-integration/ollama/scripts/stack_down.sh create mode 100644 mooncake-integration/ollama/scripts/stack_up.sh diff --git a/mooncake-integration/ollama/benchmarks/agent_swarm.py b/mooncake-integration/ollama/benchmarks/agent_swarm.py new file mode 100644 index 00000000..aaa5ec4f --- /dev/null +++ b/mooncake-integration/ollama/benchmarks/agent_swarm.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""Multi-agent KV-reuse workload driver. + +Models the core scenario: several coding agents share a long repo context and +differ only in a short instruction. The first agent prefills + stores its KV; +the rest restore it from Mooncake and prefill only their unique tail. + +Run a workload twice (cache OFF = baseline, cache ON) and compare honest +end-to-end TTFT, recomputed prefill tokens, hit rate, and aggregate throughput. +""" +from __future__ import annotations +import argparse, json, statistics, sys, threading, time +from concurrent.futures import ThreadPoolExecutor +import requests + +SESSION = requests.Session() + + +def tokenize(llama, text, add_special=True): + r = SESSION.post(f"{llama}/tokenize", json={"content": text, "add_special": add_special}, timeout=120) + r.raise_for_status() + return r.json()["tokens"] + + +def completion(llama, tokens, slot, n_predict): + r = SESSION.post(f"{llama}/completion", json={ + "prompt": tokens, "id_slot": slot, "cache_prompt": True, + "n_predict": n_predict, "temperature": 0.0, + }, timeout=600) + r.raise_for_status() + return r.json() + + +def bridge_call(bridge, ep, fp, policy, tokens, target, extra=None): + body = {"fp": fp, "policy": policy, "tokens": tokens, "target": target} + if extra: + body.update(extra) + r = SESSION.post(f"{bridge}/v1/{ep}", json=body, timeout=600) + r.raise_for_status() + return r.json() + + +def erase_all_slots(llamas, slots_per_server): + """Wipe llama.cpp's *own* per-slot prefix cache so a 'baseline' run truly + prefills cold and the cached run's only reuse path is Mooncake. Without this, + llama's single-process cache contaminates the cross-process comparison.""" + for u in llamas: + for s in range(slots_per_server): + try: + SESSION.post(f"{u}/slots/{s}?action=erase", timeout=30) + except Exception: # noqa: BLE001 + pass + + +def build_repo_context(llama, ctx_tokens): + """A synthetic but realistic multi-file Go repo context of ~ctx_tokens.""" + header = ("You are an autonomous coding agent reviewing a Go service repository.\n" + "Below is the full source tree. Read it carefully.\n\n") + unit = ("// ===== file internal/pkg{i}/service.go =====\n" + "package pkg{i}\n" + "import (\"context\"; \"fmt\"; \"time\")\n" + "// Service{i} coordinates the {i}-th processing stage of the pipeline.\n" + "type Service{i} struct {{ store *Store; clock time.Clock; retries int }}\n" + "func (s *Service{i}) Handle(ctx context.Context, r Record) (Result, error) {{\n" + " if err := r.Validate(); err != nil {{ return Result{{}}, fmt.Errorf(\"pkg{i}: %w\", err) }}\n" + " out := s.store.Transform(ctx, r)\n" + " return s.store.Aggregate(ctx, out), nil\n}}\n\n") + text = header + i = 0 + # grow until token target reached (tokenize incrementally but cheaply) + while True: + chunk = "".join(unit.format(i=j) for j in range(i, i + 12)) + if len(tokenize(llama, text + chunk)) > ctx_tokens: + break + text += chunk + i += 12 + return text + + +QUERIES = [ + "\n\nTASK: Find and explain any nil-pointer risks in Service handlers.", + "\n\nTASK: Suggest where to add structured logging across the pipeline.", + "\n\nTASK: Write a unit test for the Aggregate step of stage 3.", + "\n\nTASK: Identify duplicated validation logic and propose a refactor.", + "\n\nTASK: Where would a context cancellation be dropped? Fix it.", + "\n\nTASK: Propose a retry/backoff policy for transient store errors.", + "\n\nTASK: Add metrics counters; list the call sites to instrument.", + "\n\nTASK: Review error wrapping for consistency; cite files.", + "\n\nTASK: Find concurrency hazards in shared Store access.", + "\n\nTASK: Summarize the end-to-end data flow in 5 bullet points.", +] + + +def make_agents(llama_tok, n, ctx_tokens, share_mode, block_size): + """Return n token sequences that share an EXACT, block-aligned token prefix. + + We tokenize the shared repo context once and block-align it, then append + each agent's distinct query as separately-tokenized ids. Concatenating token + *ids* (not text) guarantees the shared prefix is byte-identical across agents + so the chained block hashes match and the cache actually hits. + """ + base = build_repo_context(llama_tok, ctx_tokens) + base_tokens = tokenize(llama_tok, base) + aligned = (len(base_tokens) // block_size) * block_size + base_tokens = base_tokens[:aligned] + agents = [] + for i in range(n): + q = QUERIES[0] if share_mode == "identical" else QUERIES[i % len(QUERIES)] + qt = tokenize(llama_tok, q, add_special=False) + agents.append(list(base_tokens) + list(qt)) + return agents, aligned + + +def run_agent(idx, bridge, llama, slot, tokens, fp, policy, n_predict, cache_on): + res = {"idx": idx, "llama": llama, "tokens": len(tokens)} + prep_ms = 0.0 + if cache_on: + t = time.perf_counter() + p = bridge_call(bridge, "prepare", fp, policy, tokens, {"base_url": llama, "slot_id": slot}) + prep_ms = (time.perf_counter() - t) * 1e3 + if p.get("error"): + # surface bridge errors instead of crashing the whole workload + raise RuntimeError(f"prepare error (agent {idx}, slot {slot}): {p['error']}") + res.update(hit=p.get("hit", False), decision=p.get("decision", "miss"), + restored=p.get("restored", False), + store_get_ms=p.get("store_get_ms", 0.0), bytes=p.get("bytes", 0)) + t = time.perf_counter() + c = completion(llama, tokens, slot, n_predict) + comp_ms = (time.perf_counter() - t) * 1e3 + if cache_on and policy.get("write"): + bridge_call(bridge, "commit", fp, policy, tokens, {"base_url": llama, "slot_id": slot}, + extra={"prefill_n": c["timings"]["prompt_n"], "prefill_ms": c["timings"]["prompt_ms"]}) + res.update(prepare_ms=prep_ms, prompt_n=c["timings"]["prompt_n"], + prompt_ms=c["timings"]["prompt_ms"], comp_wall_ms=comp_ms, + ttft_ms=prep_ms + c["timings"]["prompt_ms"]) + return res + + +def run_workload(bridge, llamas, slots_per_server, agents, fp, policy, n_predict, cache_on, concurrency): + # Each agent gets a UNIQUE (server, slot) so concurrent agents never share a + # llama slot. Agent 0 runs alone first (cold: prefill + store); the rest run + # concurrently and reuse. With cache off, all run concurrently. + results = [] + + def place(i): + server = llamas[i % len(llamas)] + slot = (i // len(llamas)) % slots_per_server + return server, slot + + t0 = time.perf_counter() + if cache_on and len(agents) > 1: + s, sl = place(0) + results.append(run_agent(0, bridge, s, sl, agents[0], fp, policy, n_predict, True)) + rest = list(range(1, len(agents))) + else: + rest = list(range(len(agents))) + + with ThreadPoolExecutor(max_workers=concurrency) as ex: + futs = [] + for i in rest: + s, sl = place(i) + futs.append(ex.submit(run_agent, i, bridge, s, sl, agents[i], fp, policy, n_predict, cache_on)) + for f in futs: + results.append(f.result()) + wall = time.perf_counter() - t0 + results.sort(key=lambda r: r["idx"]) + return results, wall + + +def summarize(results, wall, label): + ttfts = [r["ttft_ms"] for r in results] + recomputed = sum(r["prompt_n"] for r in results) + total_prompt = sum(r["tokens"] for r in results) + hits = sum(1 for r in results if r.get("hit")) + return { + "label": label, + "agents": len(results), + "wall_s": round(wall, 3), + "ttft_ms_mean": round(statistics.mean(ttfts), 1), + "ttft_ms_median": round(statistics.median(ttfts), 1), + "ttft_ms_p90": round(sorted(ttfts)[int(0.9 * (len(ttfts) - 1))], 1), + "prompt_tokens_recomputed": recomputed, + "prompt_tokens_total": total_prompt, + "recompute_ratio": round(recomputed / max(total_prompt, 1), 4), + "hit_agents": hits, + "throughput_agents_per_s": round(len(results) / max(wall, 1e-9), 3), + } + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--bridge", default="http://127.0.0.1:52052") + ap.add_argument("--llamas", required=True, help="comma-separated llama server URLs") + ap.add_argument("--model-path", required=True) + ap.add_argument("--agents", type=int, default=5) + ap.add_argument("--ctx-tokens", type=int, default=16000) + ap.add_argument("--share-mode", choices=["identical", "shared_prefix"], default="shared_prefix") + ap.add_argument("--n-predict", type=int, default=8) + ap.add_argument("--block-size", type=int, default=256) + ap.add_argument("--kv-type", default="f16", help="must match the llama server --cache-type (f16|q8_0|q4_0)") + ap.add_argument("--namespace", default="bench") + ap.add_argument("--concurrency", type=int, default=8) + ap.add_argument("--slots-per-server", type=int, default=4) + ap.add_argument("--replica-num", type=int, default=1) + ap.add_argument("--out", default="") + args = ap.parse_args() + + llamas = args.llamas.split(",") + fp = {"model_path": args.model_path, "kv_type": args.kv_type, "block_size": args.block_size} + pol_on = {"enable": True, "namespace": f"{args.namespace}-{args.ctx_tokens}", "read": True, "write": True, + "block_size": args.block_size, "replica_num": args.replica_num, "min_prefix_blocks": 1} + pol_off = {**pol_on, "enable": False, "read": False, "write": False} + + print(f"building workload: {args.agents} agents, ctx~{args.ctx_tokens}, share={args.share_mode}") + agents, aligned = make_agents(llamas[0], args.agents, args.ctx_tokens, args.share_mode, args.block_size) + print(f" shared block-aligned prefix: {aligned} tokens; prompts: {[len(a) for a in agents]}") + + # Baseline: cache OFF (erase llama's own slot cache first => true cold prefill) + erase_all_slots(llamas, args.slots_per_server) + base_res, base_wall = run_workload(args.bridge, llamas, args.slots_per_server, agents, fp, pol_off, args.n_predict, False, args.concurrency) + base = summarize(base_res, base_wall, "cache_off") + # Cached: cache ON. Erase slots again so the ONLY reuse is via Mooncake. + erase_all_slots(llamas, args.slots_per_server) + cached_res, cached_wall = run_workload(args.bridge, llamas, args.slots_per_server, agents, fp, pol_on, args.n_predict, True, args.concurrency) + cached = summarize(cached_res, cached_wall, "cache_on") + + ttft_red = 100 * (1 - cached["ttft_ms_mean"] / max(base["ttft_ms_mean"], 1e-9)) + speedup = base["wall_s"] / max(cached["wall_s"], 1e-9) + out = { + "config": vars(args), "baseline": base, "cached": cached, + "ttft_reduction_pct": round(ttft_red, 1), "throughput_speedup_x": round(speedup, 2), + "per_agent_cached": cached_res, + } + print("\n--- RESULT ---") + print(f" mean TTFT: {base['ttft_ms_mean']:.0f}ms -> {cached['ttft_ms_mean']:.0f}ms ({ttft_red:.1f}% lower)") + print(f" recompute: {base['recompute_ratio']*100:.0f}% -> {cached['recompute_ratio']*100:.0f}% of prompt tokens") + print(f" wall/throughput: {base['wall_s']:.2f}s -> {cached['wall_s']:.2f}s ({speedup:.2f}x)") + print(f" hit agents: {cached['hit_agents']}/{cached['agents']}") + if args.out: + with open(args.out, "w") as f: + json.dump(out, f, indent=2) + print(f" saved {args.out}") + return out + + +if __name__ == "__main__": + main() diff --git a/mooncake-integration/ollama/benchmarks/microbench_store.py b/mooncake-integration/ollama/benchmarks/microbench_store.py new file mode 100644 index 00000000..3274503b --- /dev/null +++ b/mooncake-integration/ollama/benchmarks/microbench_store.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Microbenchmark the Mooncake Store transfer path (zero-copy put_from/get_into). + +Measures steady-state put/get bandwidth for a registered host buffer over a +given protocol (tcp or rdma). This tells the cost-arbiter how fast the store +can deliver KV bytes, which decides restore-vs-recompute. + +Usage: + python benchmarks/microbench_store.py --protocol tcp --master 127.0.0.1:52061 + python benchmarks/microbench_store.py --protocol rdma --device mlx5_0 --master 127.0.0.1:52061 +""" +import argparse, ctypes, os, time, mmap as _mmap +from mooncake.store import MooncakeDistributedStore, ReplicateConfig + + +def ptr_of(buf): + return ctypes.addressof(ctypes.c_char.from_buffer(buf)) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--master", default=os.environ.get("OMB_STORE_MASTER", "127.0.0.1:52061")) + ap.add_argument("--metadata", default="P2PHANDSHAKE") + ap.add_argument("--protocol", default="tcp") + ap.add_argument("--device", default="") + ap.add_argument("--sizes-mb", default="16,128,512") + ap.add_argument("--iters", type=int, default=3) + ap.add_argument("--segment-gb", type=int, default=16) + ap.add_argument("--register", action="store_true", default=True) + args = ap.parse_args() + + s = MooncakeDistributedStore() + cfg = { + "local_hostname": "127.0.0.1", + "metadata_server": args.metadata, + "global_segment_size": args.segment_gb << 30, + "local_buffer_size": 4 << 30, + "protocol": args.protocol, + "rdma_devices": args.device, + "master_server_addr": args.master, + } + assert s.setup(cfg) == 0, "setup failed" + rc = ReplicateConfig(); rc.replica_num = 1 + + sizes = [int(x) << 20 for x in args.sizes_mb.split(",")] + maxsz = max(sizes) + buf = _mmap.mmap(-1, maxsz) + src = ctypes.create_string_buffer(b"", maxsz) + # fill with a pattern + for i in range(0, maxsz, 1 << 20): + buf[i:i+16] = os.urandom(16) + p = ptr_of(buf) + if args.register: + r = s.register_buffer(p, maxsz) + print(f"register_buffer -> {r}") + + # warm (absorb one-time ~20s first-put cost) + t0 = time.perf_counter() + s.put_from("omb:bench:warm", p, 1 << 20, rc) + print(f"warmup put: {(time.perf_counter()-t0)*1e3:.0f} ms") + s.get_into("omb:bench:warm", p, 1 << 20) + s.remove("omb:bench:warm", True) + + print(f"\nprotocol={args.protocol} device={args.device!r}") + print(f"{'size':>8} {'put_ms':>9} {'put_GBps':>9} {'get_ms':>9} {'get_GBps':>9}") + for sz in sizes: + put_ms = []; get_ms = [] + for it in range(args.iters): + k = f"omb:bench:{sz}:{it}" + t0 = time.perf_counter(); s.put_from(k, p, sz, rc); put_ms.append((time.perf_counter()-t0)*1e3) + t0 = time.perf_counter(); s.get_into(k, p, sz); get_ms.append((time.perf_counter()-t0)*1e3) + s.remove(k, True) + pm = min(put_ms); gm = min(get_ms) + print(f"{sz>>20:>6}MB {pm:>9.1f} {sz/(pm/1e3)/1e9:>9.2f} {gm:>9.1f} {sz/(gm/1e3)/1e9:>9.2f}") + if args.register: + s.unregister_buffer(p) + s.close() + + +if __name__ == "__main__": + main() diff --git a/mooncake-integration/ollama/benchmarks/microbench_striped.py b/mooncake-integration/ollama/benchmarks/microbench_striped.py new file mode 100644 index 00000000..acc74d5b --- /dev/null +++ b/mooncake-integration/ollama/benchmarks/microbench_striped.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Verify striped (parallel, batched) RDMA transfer sustains full bandwidth for +large KV blobs by splitting into <=chunk slices and using batch_put_from / +batch_get_into. Compares single-object vs striped for the same total size.""" +import argparse, ctypes, os, time, mmap as _mmap +from mooncake.store import MooncakeDistributedStore, ReplicateConfig + + +def ptr_of(buf): return ctypes.addressof(ctypes.c_char.from_buffer(buf)) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--master", default=os.environ.get("OMB_STORE_MASTER", "127.0.0.1:52061")) + ap.add_argument("--protocol", default="rdma") + ap.add_argument("--device", default="mlx5_0") + ap.add_argument("--total-mb", type=int, default=2048) + ap.add_argument("--chunk-mb", type=int, default=64) + args = ap.parse_args() + + s = MooncakeDistributedStore() + assert s.setup({ + "local_hostname": "127.0.0.1", "metadata_server": "P2PHANDSHAKE", + "global_segment_size": 32 << 30, "local_buffer_size": 8 << 30, + "protocol": args.protocol, "rdma_devices": args.device, + "master_server_addr": args.master, + }) == 0 + rc = ReplicateConfig(); rc.replica_num = 1 + + total = args.total_mb << 20 + chunk = args.chunk_mb << 20 + buf = _mmap.mmap(-1, total) + buf[0:16] = os.urandom(16) + p = ptr_of(buf) + s.register_buffer(p, total) + s.put_from("warm", p, 1 << 20, rc); s.get_into("warm", p, 1 << 20); s.remove("warm", True) + + # single object + t0 = time.perf_counter(); s.put_from("big", p, total, rc); put1 = time.perf_counter() - t0 + t0 = time.perf_counter(); s.get_into("big", p, total); get1 = time.perf_counter() - t0 + s.remove("big", True) + print(f"single {args.total_mb}MB: put {put1*1e3:7.0f}ms {total/put1/1e9:5.2f}GB/s get {get1*1e3:7.0f}ms {total/get1/1e9:5.2f}GB/s") + + # striped + n = (total + chunk - 1) // chunk + keys = [f"stripe:{i}" for i in range(n)] + ptrs = [p + i * chunk for i in range(n)] + sizes = [min(chunk, total - i * chunk) for i in range(n)] + t0 = time.perf_counter(); s.batch_put_from(keys, ptrs, sizes, rc); putN = time.perf_counter() - t0 + t0 = time.perf_counter(); s.batch_get_into(keys, ptrs, sizes); getN = time.perf_counter() - t0 + s.batch_remove(keys) if hasattr(s, "batch_remove") else [s.remove(k, True) for k in keys] + print(f"striped {args.total_mb}MB/{args.chunk_mb}MB x{n}: put {putN*1e3:7.0f}ms {total/putN/1e9:5.2f}GB/s get {getN*1e3:7.0f}ms {total/getN/1e9:5.2f}GB/s") + s.unregister_buffer(p); s.close() + + +if __name__ == "__main__": + main() diff --git a/mooncake-integration/ollama/benchmarks/plot.py b/mooncake-integration/ollama/benchmarks/plot.py new file mode 100644 index 00000000..eea3d424 --- /dev/null +++ b/mooncake-integration/ollama/benchmarks/plot.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""Plot the benchmark matrix into publication-style figures (PNG).""" +import argparse, json, os, re +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +# Cohesive figure style shared by every chart. +PALETTE = {"baseline": "#c0392b", "mooncake": "#2e6fdb", "reuser": "#27ae60", + "accent": "#7a5fb8", "muted": "#9aa0a6"} +plt.rcParams.update({ + "figure.dpi": 140, + "font.size": 12, + "axes.titlesize": 15, + "axes.titleweight": "bold", + "axes.labelsize": 12, + "axes.spines.top": False, + "axes.spines.right": False, + "axes.grid": True, + "grid.color": "#e6e6e6", + "grid.linewidth": 0.8, + "axes.axisbelow": True, + "legend.frameon": False, + "figure.autolayout": True, +}) + + +def _human(name): + """A2_7b_30k_6ag -> '7B / 30k / 6 agents'.""" + m = re.search(r"(\d+\.?\d*)b_(\d+)k?_(\d+)ag", name, re.I) + if not m: + return name + size, ctx, ag = m.groups() + return f"{size}B / {ctx}k / {ag} agents" + + +def fig_ttft(cells, out): + names = [_human(c["name"]) for c in cells] + off = [c["ttft_off"] for c in cells] + on = [c["ttft_on"] for c in cells] + reuser = [c.get("reuser_ttft_on", c["ttft_on"]) for c in cells] + x = np.arange(len(names)); w = 0.27 + fig, ax = plt.subplots(figsize=(max(7, 2.0 * len(names)), 4.4)) + ax.bar(x - w, off, w, label="no sharing (baseline)", color=PALETTE["baseline"]) + ax.bar(x, on, w, label="Mooncake (all agents)", color=PALETTE["mooncake"]) + ax.bar(x + w, reuser, w, label="Mooncake (reusing agents only)", color=PALETTE["reuser"]) + for i, (a, b) in enumerate(zip(off, on)): + ax.text(i, max(a, b) * 1.02, f"-{100*(1-b/a):.0f}%", ha="center", fontsize=10, fontweight="bold", color="#222") + ax.set_ylabel("mean TTFT (ms)") + ax.set_title("Time-to-first-token: KV reuse via Mooncake") + ax.set_xticks(x); ax.set_xticklabels(names); ax.legend() + fig.savefig(out); print("wrote", out); plt.close(fig) + + +def fig_recompute(cells, out): + names = [_human(c["name"]) for c in cells] + off = [c["recompute_off_pct"] for c in cells] + on = [c["recompute_on_pct"] for c in cells] + x = np.arange(len(names)); w = 0.38 + fig, ax = plt.subplots(figsize=(max(7, 2.0 * len(names)), 4.4)) + ax.bar(x - w/2, off, w, label="baseline", color=PALETTE["baseline"]) + bars = ax.bar(x + w/2, on, w, label="Mooncake", color=PALETTE["mooncake"]) + for b, v in zip(bars, on): + ax.text(b.get_x() + b.get_width()/2, v + 2, f"{v:.0f}%", ha="center", fontsize=10, color="#222") + ax.set_ylabel("% of prompt tokens re-prefilled"); ax.set_ylim(0, 108) + ax.set_title("Redundant prefill eliminated"); ax.set_xticks(x) + ax.set_xticklabels(names); ax.legend() + fig.savefig(out); print("wrote", out); plt.close(fig) + + +def fig_throughput(cells, out): + names = [_human(c["name"]) for c in cells] + thru = [c["thru_x"] for c in cells] + x = np.arange(len(names)) + fig, ax = plt.subplots(figsize=(max(7, 2.0 * len(names)), 4.2)) + bars = ax.bar(x, thru, 0.5, color=PALETTE["accent"]) + ax.axhline(1.0, ls="--", color=PALETTE["muted"], label="parity") + for b, t in zip(bars, thru): + ax.text(b.get_x() + b.get_width()/2, t + 0.02, f"{t:.2f}x", ha="center", fontsize=11, fontweight="bold") + ax.set_ylabel("throughput speedup (x)"); ax.set_title("Aggregate swarm throughput") + ax.set_xticks(x); ax.set_xticklabels(names); ax.legend() + fig.savefig(out); print("wrote", out); plt.close(fig) + + +def fig_per_agent(swarm_json, out): + """Prompt tokens recomputed per agent: the pioneer pays the full prefill, + every later agent restores and recomputes almost nothing. This is the + 'only agent 0 prefills' story, shown directly rather than via TTFT (which + on the Stage-1 file path can make a reuser look slower than the pioneer).""" + d = json.load(open(swarm_json)) + pa = d["per_agent_cached"] + idx = [r["idx"] for r in pa] + recomputed = [r["prompt_n"] for r in pa] + total = [r["tokens"] for r in pa] + colors = [PALETTE["baseline"]] + [PALETTE["reuser"]] * (len(pa) - 1) + fig, ax = plt.subplots(figsize=(max(7.5, 1.0 * len(pa)), 4.6)) + # faint full-prompt reference so the saving is obvious + ax.bar(idx, total, color="#ececec", width=0.74, label="prompt size (tokens)") + bars = ax.bar(idx, recomputed, color=colors, width=0.74, label="tokens re-prefilled") + for i, (b, rc) in enumerate(zip(bars, recomputed)): + tag = f"{rc}" if rc < 1000 else f"{rc/1000:.1f}k" + ax.text(b.get_x() + b.get_width() / 2, rc + max(total) * 0.02, tag, + ha="center", fontsize=10, fontweight="bold", color="#222") + ax.annotate("pioneer pays\nthe full prefill", xy=(0, total[0]), xytext=(0.6, total[0] * 0.82), + fontsize=10, color=PALETTE["baseline"], + arrowprops=dict(arrowstyle="->", color=PALETTE["baseline"])) + saved = sum(total) - sum(recomputed) + ax.set_xlabel("agent index (0 = pioneer that prefills + stores)") + ax.set_ylabel("prompt tokens") + ax.set_xticks(idx) + ax.set_title(f"Only the pioneer prefills ({saved//1000}k tokens saved)") + ax.legend(loc="center right") + fig.savefig(out); print("wrote", out); plt.close(fig) + + +def fig_scaling(scaling_json, out): + """Baseline work grows linearly with the swarm; Mooncake stays almost flat. + scaling_json is a list of {agents, baseline_recompute_tokens, + cached_recompute_tokens, baseline_wall_s, cached_wall_s}.""" + rows = sorted(json.load(open(scaling_json)), key=lambda r: r["agents"]) + n = [r["agents"] for r in rows] + base_tok = [r["baseline_recompute_tokens"] / 1e3 for r in rows] + cache_tok = [r["cached_recompute_tokens"] / 1e3 for r in rows] + fig, ax = plt.subplots(figsize=(7.5, 4.6)) + ax.plot(n, base_tok, "o-", color=PALETTE["baseline"], lw=2.4, ms=7, + label="no sharing (re-prefill every agent)") + ax.plot(n, cache_tok, "o-", color=PALETTE["mooncake"], lw=2.4, ms=7, + label="Mooncake (prefill once, restore the rest)") + ax.fill_between(n, cache_tok, base_tok, color=PALETTE["mooncake"], alpha=0.10) + # endpoint callouts at the widest swarm + ax.annotate(f"{base_tok[-1]:.0f}k", xy=(n[-1], base_tok[-1]), xytext=(-4, 6), + textcoords="offset points", ha="right", fontsize=11, + fontweight="bold", color=PALETTE["baseline"]) + ax.annotate(f"{cache_tok[-1]:.0f}k (flat)", xy=(n[-1], cache_tok[-1]), xytext=(-4, 8), + textcoords="offset points", ha="right", fontsize=11, + fontweight="bold", color=PALETTE["mooncake"]) + ax.text(n[len(n)//2], (base_tok[-1] + cache_tok[0]) / 2, + f"{base_tok[-1]-cache_tok[-1]:.0f}k tokens\nnever re-prefilled", + ha="center", va="center", fontsize=10, color="#555", style="italic") + ax.set_xlabel("agents sharing the context") + ax.set_ylabel("prompt tokens prefilled (thousands)") + ax.set_title("Redundant prefill work vs swarm size (7B, 30k ctx)") + ax.set_xticks(n); ax.set_ylim(bottom=0); ax.legend(loc="upper left") + fig.savefig(out); print("wrote", out); plt.close(fig) + + +def fig_arbiter(out, points=None): + """Restore cost vs recompute cost per (model, context); the arbiter picks the + cheaper. Demonstrates the loss-free property: it restores only when restoring + is actually faster than recomputing. Values are the arbiter's own estimates + from measured online rates. + + points: list of {label, restore_ms, recompute_ms, decision}. Defaults to the + two measured regimes (7B restores, 1.5B declines).""" + if points is None: + points = [ + {"label": "7B / 30k ctx", "restore_ms": 2160, "recompute_ms": 4034, "decision": "RESTORE"}, + {"label": "1.5B / 8k ctx", "restore_ms": 697, "recompute_ms": 311, "decision": "RECOMPUTE"}, + ] + labels = [p["label"] for p in points] + restore = [p["restore_ms"] for p in points] + recompute = [p["recompute_ms"] for p in points] + x = np.arange(len(labels)); w = 0.36 + fig, ax = plt.subplots(figsize=(max(6.5, 2.6 * len(labels)), 4.4)) + ax.bar(x - w / 2, restore, w, label="restore from store", color=PALETTE["mooncake"]) + ax.bar(x + w / 2, recompute, w, label="recompute (prefill)", color=PALETTE["baseline"]) + for i, p in enumerate(points): + chosen = min(p["restore_ms"], p["recompute_ms"]) + ax.annotate(f"chooses\n{p['decision']}", + xy=(i, chosen), xytext=(i, chosen + max(restore + recompute) * 0.08), + ha="center", fontsize=10, fontweight="bold", color="#1a7d32") + ax.set_ylabel("estimated cost (ms)") + ax.set_title("Loss-free arbiter: restore only when it's cheaper") + ax.set_xticks(x); ax.set_xticklabels(labels); ax.legend(loc="upper left") + fig.savefig(out); print("wrote", out); plt.close(fig) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--matrix", default="run/matrix_results.json") + ap.add_argument("--per-agent", default="", help="a swarm cell json for the per-agent figure") + ap.add_argument("--scaling", default="", help="a scaling json for the agents-scaling figure") + ap.add_argument("--outdir", default="docs/figures") + args = ap.parse_args() + os.makedirs(args.outdir, exist_ok=True) + m = json.load(open(args.matrix)) + cells = m["cells"] + fig_ttft(cells, os.path.join(args.outdir, "ttft.png")) + fig_recompute(cells, os.path.join(args.outdir, "recompute.png")) + fig_throughput(cells, os.path.join(args.outdir, "throughput.png")) + if args.per_agent and os.path.exists(args.per_agent): + fig_per_agent(args.per_agent, os.path.join(args.outdir, "per_agent.png")) + if args.scaling and os.path.exists(args.scaling): + fig_scaling(args.scaling, os.path.join(args.outdir, "scaling_agents.png")) + fig_arbiter(os.path.join(args.outdir, "arbiter.png")) + + +if __name__ == "__main__": + main() diff --git a/mooncake-integration/ollama/benchmarks/run_matrix.py b/mooncake-integration/ollama/benchmarks/run_matrix.py new file mode 100644 index 00000000..66675538 --- /dev/null +++ b/mooncake-integration/ollama/benchmarks/run_matrix.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Run the benchmark matrix and aggregate results. + +Each cell shells out to agent_swarm.py (cache OFF baseline vs cache ON) for a +given (model, context, #agents, kv_type) on the currently-running stack, then +collects the JSON into a single matrix file consumed by plot.py. + +The store-backend axis (RDMA / TCP / local-file) is swept by restarting the +store layer between groups (re-run proxy_start.sh with a different protocol). +""" +import argparse, json, os, subprocess, sys, time + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def run_cell(py, cell, bridge, out_dir): + out = os.path.join(out_dir, f"cell_{cell['name']}.json") + # model_path may be a bare filename; resolve it against $MODELS_DIR so the + # checked-in config is portable across machines/checkouts. + model_path = cell["model_path"] + if not os.path.isabs(model_path) and not os.path.exists(model_path): + models_dir = os.environ.get("MODELS_DIR", "") + if models_dir: + cand = os.path.join(models_dir, os.path.basename(model_path)) + if os.path.exists(cand): + model_path = cand + cmd = [py, os.path.join(HERE, "agent_swarm.py"), + "--bridge", bridge, + "--llamas", cell["llamas"], + "--model-path", model_path, + "--kv-type", cell.get("kv_type", "f16"), + "--agents", str(cell["agents"]), + "--ctx-tokens", str(cell["ctx_tokens"]), + "--share-mode", cell.get("share_mode", "shared_prefix"), + "--slots-per-server", str(cell.get("slots_per_server", 4)), + "--concurrency", str(cell.get("concurrency", 4)), + "--n-predict", str(cell.get("n_predict", 8)), + "--namespace", cell["name"], + "--out", out] + print(f"\n===== cell {cell['name']} =====") + print(" ", " ".join(cmd)) + t0 = time.time() + r = subprocess.run(cmd, capture_output=True, text=True) + print(r.stdout[-1200:]) + if r.returncode != 0: + print("STDERR:", r.stderr[-1500:]) + return None + with open(out) as f: + data = json.load(f) + data["wall_total_s"] = round(time.time() - t0, 1) + data["store_backend"] = cell.get("store_backend", "rdma") + return data + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--config", required=True, help="JSON file with a list of cells") + ap.add_argument("--bridge", default="http://127.0.0.1:52052") + ap.add_argument("--out", default="run/matrix_results.json") + args = ap.parse_args() + + with open(args.config) as f: + cfg = json.load(f) + out_dir = os.path.dirname(os.path.abspath(args.out)) + os.makedirs(out_dir, exist_ok=True) + + results = [] + for cell in cfg["cells"]: + d = run_cell(sys.executable, cell, args.bridge, out_dir) + if d: + results.append({"name": cell["name"], **summ(d)}) + matrix = {"cells": results, "raw": cfg} + with open(args.out, "w") as f: + json.dump(matrix, f, indent=2) + + print("\n================ MATRIX SUMMARY ================") + hdr = f"{'cell':<22}{'backend':<8}{'agents':>7}{'TTFT off':>10}{'TTFT on':>10}{'red%':>7}{'thru x':>8}{'recompute%':>12}" + print(hdr); print("-" * len(hdr)) + for r in results: + print(f"{r['name']:<22}{r['backend']:<8}{r['agents']:>7}{r['ttft_off']:>10.0f}{r['ttft_on']:>10.0f}" + f"{r['ttft_red_pct']:>7.0f}{r['thru_x']:>8.2f}{r['recompute_on_pct']:>12.0f}") + print(f"\nsaved {args.out}") + + +def summ(d): + b, c = d["baseline"], d["cached"] + reuse = d["per_agent_cached"][1:] if len(d["per_agent_cached"]) > 1 else d["per_agent_cached"] + reuse_ttft = sum(r["ttft_ms"] for r in reuse) / max(len(reuse), 1) + return { + "backend": d.get("store_backend", "rdma"), + "agents": c["agents"], + "ttft_off": b["ttft_ms_mean"], + "ttft_on": c["ttft_ms_mean"], + "ttft_red_pct": d["ttft_reduction_pct"], + "reuser_ttft_on": round(reuse_ttft, 1), + "thru_x": d["throughput_speedup_x"], + "recompute_off_pct": round(b["recompute_ratio"] * 100, 1), + "recompute_on_pct": round(c["recompute_ratio"] * 100, 1), + "hit_agents": c["hit_agents"], + } + + +if __name__ == "__main__": + main() diff --git a/mooncake-integration/ollama/benchmarks/scaling.json b/mooncake-integration/ollama/benchmarks/scaling.json new file mode 100644 index 00000000..f07991ab --- /dev/null +++ b/mooncake-integration/ollama/benchmarks/scaling.json @@ -0,0 +1,42 @@ +[ + { + "agents": 1, + "baseline_recompute_tokens": 28942, + "cached_recompute_tokens": 28942, + "baseline_wall_s": 8.09, + "cached_wall_s": 22.694, + "throughput_x": 0.36 + }, + { + "agents": 2, + "baseline_recompute_tokens": 57884, + "cached_recompute_tokens": 28953, + "baseline_wall_s": 5.849, + "cached_wall_s": 6.269, + "throughput_x": 0.93 + }, + { + "agents": 4, + "baseline_recompute_tokens": 115768, + "cached_recompute_tokens": 28975, + "baseline_wall_s": 8.558, + "cached_wall_s": 8.594, + "throughput_x": 1.0 + }, + { + "agents": 6, + "baseline_recompute_tokens": 173653, + "cached_recompute_tokens": 28998, + "baseline_wall_s": 12.961, + "cached_wall_s": 9.519, + "throughput_x": 1.36 + }, + { + "agents": 8, + "baseline_recompute_tokens": 231535, + "cached_recompute_tokens": 29018, + "baseline_wall_s": 16.469, + "cached_wall_s": 10.878, + "throughput_x": 1.51 + } +] \ No newline at end of file diff --git a/mooncake-integration/ollama/benchmarks/smoke_e2e.py b/mooncake-integration/ollama/benchmarks/smoke_e2e.py new file mode 100644 index 00000000..8dd7b7dd --- /dev/null +++ b/mooncake-integration/ollama/benchmarks/smoke_e2e.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""End-to-end smoke test: cross-process / cross-GPU KV reuse via Mooncake. + +Agent A (on llama server #0, GPU X): prepare -> miss; /completion fully +prefills a long prompt; commit -> store the KV to Mooncake. + +Agent B (on llama server #1, GPU Y): prepare -> the sidecar fetches the KV from +Mooncake and restores it into B's slot; /completion now prefills ~0 tokens. + +We print the prompt-token count actually re-computed (timings.prompt_n) and the +prefill wall time for A vs B. B should be dramatically faster, proving the KV +crossed processes/GPUs through the store. +""" +import argparse, json, time, sys +import requests + + +def tokenize(llama, text, add_special=True): + r = requests.post(f"{llama}/tokenize", json={"content": text, "add_special": add_special}) + r.raise_for_status() + return r.json()["tokens"] + + +def completion(llama, tokens, slot, n_predict=8): + r = requests.post(f"{llama}/completion", json={ + "prompt": tokens, "id_slot": slot, "cache_prompt": True, + "n_predict": n_predict, "temperature": 0.0, + }) + r.raise_for_status() + return r.json() + + +def bridge_call(bridge, ep, fp, policy, tokens, target): + r = requests.post(f"{bridge}/v1/{ep}", json={ + "fp": fp, "policy": policy, "tokens": tokens, "target": target, + }) + r.raise_for_status() + return r.json() + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--bridge", default="http://127.0.0.1:52052") + ap.add_argument("--llama-a", default="http://127.0.0.1:52070") + ap.add_argument("--llama-b", default="http://127.0.0.1:52071") + ap.add_argument("--model-path", required=True) + ap.add_argument("--ctx-tokens", type=int, default=8000) + ap.add_argument("--namespace", default="smoke") + ap.add_argument("--block-size", type=int, default=256) + args = ap.parse_args() + + fp = {"model_path": args.model_path, "kv_type": "f16", "block_size": args.block_size} + policy = {"enable": True, "namespace": args.namespace, "read": True, "write": True, + "block_size": args.block_size, "replica_num": 1, "min_prefix_blocks": 1} + + # Build a long, code-like context prompt of ~ctx-tokens tokens. + unit = ("// module {i}: utility helpers for the data pipeline\n" + "func process_{i}(records []Record) (Result, error) {{\n" + " // validate, transform, and aggregate the {i}-th shard\n" + " return aggregate(transform(validate(records))), nil\n}}\n\n") + text = "You are reviewing a large Go repository. Here is the source:\n\n" + i = 0 + while len(tokenize(args.llama_a, text)) < args.ctx_tokens: + text += unit.format(i=i) + i += 1 + text += "\nSummarize the overall architecture of this repository." + tokens = tokenize(args.llama_a, text) + print(f"[setup] prompt = {len(tokens)} tokens, block_size={args.block_size} " + f"=> {len(tokens)//args.block_size} full blocks\n") + + # ---- Agent A: cold on server #0 ---- + ta = time.perf_counter() + pa = bridge_call(args.bridge, "prepare", fp, policy, tokens, + {"base_url": args.llama_a, "slot_id": 0}) + pa_wall = (time.perf_counter() - ta) * 1e3 + t0 = time.perf_counter() + ca = completion(args.llama_a, tokens, slot=0) + a_wall = (time.perf_counter() - t0) * 1e3 + co = bridge_call(args.bridge, "commit", fp, policy, tokens, + {"base_url": args.llama_a, "slot_id": 0}) + a_ttft = pa_wall + ca['timings']['prompt_ms'] + print("AGENT A (cold, server #0):") + print(f" prepare : hit={pa['hit']} decision={pa['decision']} wall={pa_wall:.1f}ms ({pa.get('reason','')})") + print(f" prefill : prompt_n={ca['timings']['prompt_n']} toks, " + f"prompt_ms={ca['timings']['prompt_ms']:.1f}, e2e_wall={a_wall:.1f}ms") + print(f" commit : stored={co['stored']} blocks={co['stored_blocks']} " + f"bytes={co['bytes']} put_ms={co['store_put_ms']:.1f} key=...{co['key'][-24:]}") + print(f" >> honest TTFT(A) = prepare {pa_wall:.1f} + prefill {ca['timings']['prompt_ms']:.1f} = {a_ttft:.1f}ms\n") + + # ---- Agent B: warm on server #1 (must restore from the store) ---- + tb = time.perf_counter() + pb = bridge_call(args.bridge, "prepare", fp, policy, tokens, + {"base_url": args.llama_b, "slot_id": 0}) + pb_wall = (time.perf_counter() - tb) * 1e3 + t0 = time.perf_counter() + cb = completion(args.llama_b, tokens, slot=0) + b_wall = (time.perf_counter() - t0) * 1e3 + b_ttft = pb_wall + cb['timings']['prompt_ms'] + print("AGENT B (warm, server #1 -- different process & GPU):") + print(f" prepare : hit={pb['hit']} decision={pb['decision']} restored={pb['restored']} " + f"restored_tokens={pb['restored_tokens']} wall={pb_wall:.1f}ms") + print(f" store_get_ms={pb['store_get_ms']:.1f} bytes={pb['bytes']} ({pb.get('reason','')})") + print(f" prefill : prompt_n={cb['timings']['prompt_n']} toks, " + f"prompt_ms={cb['timings']['prompt_ms']:.1f}, e2e_wall={b_wall:.1f}ms") + print(f" >> honest TTFT(B) = prepare {pb_wall:.1f} + prefill {cb['timings']['prompt_ms']:.1f} = {b_ttft:.1f}ms\n") + + # ---- verdict ---- + saved = ca['timings']['prompt_n'] - cb['timings']['prompt_n'] + ttft_red = 100.0 * (1 - b_ttft / max(a_ttft, 1e-9)) + print("=" * 70) + print(f" prefill tokens recomputed: A={ca['timings']['prompt_n']} -> B={cb['timings']['prompt_n']} " + f"(saved {saved} tokens, {100.0*saved/max(ca['timings']['prompt_n'],1):.1f}%)") + print(f" HONEST end-to-end TTFT: A={a_ttft:.1f}ms -> B={b_ttft:.1f}ms " + f"(reduced {ttft_red:.1f}%)") + print(f" (B breakdown: store_get {pb['store_get_ms']:.1f}ms + restore/overhead " + f"{pb_wall - pb['store_get_ms']:.1f}ms + prefill {cb['timings']['prompt_ms']:.1f}ms)") + print("=" * 70) + stats = requests.get(f"{args.bridge}/stats").json() + print(f"sidecar stats: hits={stats['hits']} misses={stats['misses']} " + f"saved_prefill_tokens={stats['saved_prefill_tokens']} " + f"learned_get_gbps={stats['learned_get_gbps']:.1f} learned_prefill_tps={stats['learned_prefill_tps']:.0f}") + ok = cb['timings']['prompt_n'] < ca['timings']['prompt_n'] * 0.3 and pb['restored'] + print("\nRESULT:", "PASS — KV reused across processes/GPUs via Mooncake" if ok else "FAIL") + sys.exit(0 if ok else 1) + + +if __name__ == "__main__": + main() diff --git a/mooncake-integration/ollama/benchmarks/workloads/matrix.json b/mooncake-integration/ollama/benchmarks/workloads/matrix.json new file mode 100644 index 00000000..9960dcaf --- /dev/null +++ b/mooncake-integration/ollama/benchmarks/workloads/matrix.json @@ -0,0 +1,40 @@ +{ + "cells": [ + { + "name": "A1_1p5b_8k_5ag", + "llamas": "http://127.0.0.1:52070,http://127.0.0.1:52071", + "model_path": "qwen2.5-coder-1.5b-instruct-q4_k_m.gguf", + "kv_type": "f16", + "agents": 5, + "ctx_tokens": 8000, + "share_mode": "shared_prefix", + "slots_per_server": 4, + "concurrency": 2, + "store_backend": "rdma" + }, + { + "name": "A2_7b_30k_6ag", + "llamas": "http://127.0.0.1:52072,http://127.0.0.1:52073", + "model_path": "qwen2.5-coder-7b-instruct-q4_k_m.gguf", + "kv_type": "q8_0", + "agents": 6, + "ctx_tokens": 30000, + "share_mode": "shared_prefix", + "slots_per_server": 4, + "concurrency": 2, + "store_backend": "rdma" + }, + { + "name": "A3_7b_16k_8ag", + "llamas": "http://127.0.0.1:52072,http://127.0.0.1:52073", + "model_path": "qwen2.5-coder-7b-instruct-q4_k_m.gguf", + "kv_type": "q8_0", + "agents": 8, + "ctx_tokens": 16000, + "share_mode": "shared_prefix", + "slots_per_server": 4, + "concurrency": 2, + "store_backend": "rdma" + } + ] +} \ No newline at end of file diff --git a/mooncake-integration/ollama/scripts/bridged_start.sh b/mooncake-integration/ollama/scripts/bridged_start.sh new file mode 100644 index 00000000..b2766517 --- /dev/null +++ b/mooncake-integration/ollama/scripts/bridged_start.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Build (if needed) and start the Go sidecar (bridged). +# Usage: bridged_start.sh [mooncake|local] +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" +BACKEND="${1:-mooncake}" + +if [ ! -x "$OMB_RUN/bridged" ] || [ "${OMB_REBUILD:-0}" = "1" ]; then + echo "[bridged] building" + (cd "$WS/bridge" && go build -o "$OMB_RUN/bridged" ./cmd/bridged) +fi + +if ss -ltn 2>/dev/null | grep -qE "[:.]$OMB_BRIDGE_GRPC_PORT\b"; then + echo "[bridged] already on $OMB_BRIDGE_GRPC_PORT"; exit 0 +fi + +nohup "$OMB_RUN/bridged" \ + -grpc-addr "127.0.0.1:$OMB_BRIDGE_GRPC_PORT" \ + -grpc-unix "$OMB_SOCK/bridged.sock" \ + -http-addr "127.0.0.1:$OMB_BRIDGE_HTTP_PORT" \ + -store-backend "$BACKEND" \ + -store-proxy-addr "127.0.0.1:$OMB_STORE_PROXY_PORT" \ + -local-store-dir "$OMB_RUN/store-local" \ + -slot-save-path "$OMB_SLOTS/" \ + -block-size "${OMB_BLOCK_SIZE:-256}" \ + -replica-num "${OMB_REPLICA_NUM:-1}" \ + -min-prefix-blocks "${OMB_MIN_PREFIX_BLOCKS:-1}" \ + -prior-get-gbps "${OMB_PRIOR_GBPS:-8}" \ + -prior-prefill-tps "${OMB_PRIOR_TPS:-8000}" \ + -arb-min-tokens "${OMB_ARB_MIN_TOKENS:-256}" \ + -arb-safety "${OMB_ARB_SAFETY:-0.9}" \ + > "$OMB_LOGS/bridged.log" 2>&1 & +echo $! > "$OMB_RUN/bridged.pid" +for _ in $(seq 1 40); do + curl -fsS "http://127.0.0.1:$OMB_BRIDGE_HTTP_PORT/healthz" >/dev/null 2>&1 && { + echo "[bridged] up (grpc $OMB_BRIDGE_GRPC_PORT, http $OMB_BRIDGE_HTTP_PORT, backend $BACKEND)"; exit 0; } + sleep 0.25 +done +echo "[bridged] FAILED"; tail -25 "$OMB_LOGS/bridged.log"; exit 1 diff --git a/mooncake-integration/ollama/scripts/demo.sh b/mooncake-integration/ollama/scripts/demo.sh new file mode 100644 index 00000000..53e5cb29 --- /dev/null +++ b/mooncake-integration/ollama/scripts/demo.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# 10-minute demo: bring up the stack and show a swarm of coding agents sharing +# one repo's KV cache through Mooncake. Usage: demo.sh [7b|1.5b] [agents] +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$HERE/env.sh" + +WHICH="${1:-7b}"; AGENTS="${2:-6}" +if [ "$WHICH" = "7b" ]; then + MODEL="$MODELS_DIR/qwen2.5-coder-7b-instruct-q4_k_m.gguf"; KV=q8_0; CTX=30000; GPUS=(4 5); PORTS=(52072 52073) +else + MODEL="$MODELS_DIR/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf"; KV=f16; CTX=16000; GPUS=(2 3); PORTS=(52070 52071) +fi + +# llama.cpp's -c is the TOTAL KV context shared across the -np parallel slots, so +# each slot must be sized for the full shared prompt plus a generation margin. +NP=4 +PERSLOT=$(( CTX + 2048 )) +TOTAL_CTX=$(( PERSLOT * NP )) + +echo "==> master"; bash "$HERE/master_start.sh" +echo "==> store proxy"; OMB_STORE_SEGMENT_GB=24 OMB_STORE_STAGING_MB="${OMB_STORE_STAGING_MB:-1280}" OMB_STORE_STAGING_COUNT="${OMB_STORE_STAGING_COUNT:-4}" bash "$HERE/proxy_start.sh" rdma +echo "==> 2x llama ($WHICH, kv=$KV, ${PERSLOT}tok/slot x$NP)"; +OMB_LLAMA_KVTYPE="$KV" bash "$HERE/llama_start.sh" d0 "${GPUS[0]}" "${PORTS[0]}" "$MODEL" "$TOTAL_CTX" "$NP" | tail -1 +OMB_LLAMA_KVTYPE="$KV" bash "$HERE/llama_start.sh" d1 "${GPUS[1]}" "${PORTS[1]}" "$MODEL" "$TOTAL_CTX" "$NP" | tail -1 +echo "==> sidecar"; OMB_PRIOR_GBPS=2 OMB_PRIOR_TPS=8000 bash "$HERE/bridged_start.sh" mooncake + +echo +echo "######################################################################" +echo "# $AGENTS coding agents analyse the SAME ~${CTX}-token repo context." +echo "# Agent 0 prefills it once and writes its KV to Mooncake; the rest" +echo "# restore that KV across processes & GPUs and skip the prefill." +echo "######################################################################" +echo +python "$WS/benchmarks/agent_swarm.py" \ + --llamas "http://127.0.0.1:${PORTS[0]},http://127.0.0.1:${PORTS[1]}" \ + --model-path "$MODEL" --kv-type "$KV" \ + --agents "$AGENTS" --ctx-tokens "$CTX" --share-mode shared_prefix \ + --slots-per-server 4 --concurrency "${DEMO_CONCURRENCY:-2}" --namespace demo \ + --out "$OMB_RUN/demo_result.json" + +# Scoreboard: turn the result JSON into a one-glance before/after summary. +python - "$OMB_RUN/demo_result.json" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +b, c = d["baseline"], d["cached"] +base, cached = b["prompt_tokens_recomputed"], c["prompt_tokens_recomputed"] +saved = base - cached +# Two distinct metrics: total prefill work avoided across the swarm, vs the +# fraction of *redundant* prefill removed (one pioneer prefill is unavoidable). +pioneer = base // c["agents"] if c["agents"] else base +redundant_base = max(base - pioneer, 1) +redundant_left = max(cached - pioneer, 0) +total_avoided = 100 * (1 - cached / max(base, 1)) +redundant_killed = 100 * (1 - redundant_left / redundant_base) +line = "=" * 64 +print("\n" + line); print(" SCOREBOARD"); print(line) +print(f" agents sharing context : {c['agents']}") +print(f" prefill tokens, no sharing : {base:>9,}") +print(f" prefill tokens, Mooncake : {cached:>9,} ({saved:,} saved)") +print(f" total prefill work avoided : {total_avoided:.0f}%") +print(f" redundant prefill killed : {redundant_killed:.0f}% (only the pioneer must prefill)") +print(f" mean TTFT : {b['ttft_ms_mean']:.0f} ms -> {c['ttft_ms_mean']:.0f} ms") +print(f" swarm throughput : {d['throughput_speedup_x']:.2f}x") +print(f" cache hits : {c['hit_agents']}/{c['agents']}") +print(line + "\n") +PY + +if [ "${DEMO_VERBOSE:-0}" = "1" ]; then + echo "sidecar live stats:" + curl -fsS "http://127.0.0.1:$OMB_BRIDGE_HTTP_PORT/stats" | python -m json.tool +fi +echo "Prometheus metrics: http://127.0.0.1:$OMB_BRIDGE_HTTP_PORT/metrics (set DEMO_VERBOSE=1 for raw stats)" +echo "Tear down with: bash scripts/stack_down.sh" diff --git a/mooncake-integration/ollama/scripts/env.sh b/mooncake-integration/ollama/scripts/env.sh new file mode 100644 index 00000000..0cbf0f82 --- /dev/null +++ b/mooncake-integration/ollama/scripts/env.sh @@ -0,0 +1,90 @@ +# shellcheck shell=bash +# Central environment for the Ollama x Mooncake KVCache Bus integration. +# +# Layout philosophy: +# * WS -> the *source* tree (this integration dir inside the Mooncake +# repo). Resolved from this file's location, so the checkout +# can live at any path without edits. Never written to by the +# build/run (keeps the git tree clean for a PR). +# * OMB_STATE -> all *generated* state: toolchains, venv, caches, llama.cpp / +# ollama checkouts, model weights, logs, run dirs. Defaults to +# a sibling ".omb-state" OUTSIDE the repo so heavy artifacts +# never pollute the tree and never land on '/'. Override with +# OMB_STATE_DIR to point at a large data volume. +# +# Source at the top of every script: source "/env.sh" + +# --- Source tree root (this integration dir) --- +export WS="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" + +# --- Generated-state root (kept OUTSIDE the source tree) --- +# Default: a ".omb-state" directory next to the top of the git repo if we can +# find it, else next to WS. Override via OMB_STATE_DIR for a big data volume. +if [ -z "${OMB_STATE_DIR:-}" ]; then + _repo_root="$(cd "$WS" && git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "${_repo_root:-}" ]; then + OMB_STATE_DIR="$(dirname "$_repo_root")/.omb-state" + else + OMB_STATE_DIR="$(dirname "$WS")/.omb-state" + fi +fi +export OMB_STATE="$OMB_STATE_DIR" + +# --- Keep ALL caches off '/' and out of the repo --- +export TMPDIR="$OMB_STATE/tmp" +export PIP_CACHE_DIR="$OMB_STATE/.cache/pip" +export XDG_CACHE_HOME="$OMB_STATE/.cache/xdg" +export HF_HOME="$OMB_STATE/.cache/hf" +export HUGGINGFACE_HUB_CACHE="$OMB_STATE/.cache/hf/hub" + +# --- Go toolchain (installed locally, never system-wide) --- +export GOROOT="$OMB_STATE/go" +export GOPATH="$OMB_STATE/.cache/gopath" +export GOMODCACHE="$OMB_STATE/.cache/gomod" +export GOCACHE="$OMB_STATE/.cache/go-build" +export GOBIN="$OMB_STATE/.cache/gopath/bin" +export GOFLAGS="-mod=mod" +export GOTOOLCHAIN="local" # never auto-download a different toolchain + +# --- Python venv --- +export VENV="$OMB_STATE/.venv" + +# --- CUDA / GPU. Restrict our processes to a subset of devices. --- +export CUDA_HOME="${CUDA_HOME:-/usr/local/cuda-12.4}" +export OMB_GPUS="${OMB_GPUS:-2,3,4,5,6,7}" + +# --- llama.cpp build/run --- +export LLAMA_DIR="$OMB_STATE/llama.cpp" +export LLAMA_BUILD="$LLAMA_DIR/build" +export MODELS_DIR="$OMB_STATE/models" +export OLLAMA_DIR="$OMB_STATE/ollama" + +# --- Runtime dirs --- +export OMB_RUN="$OMB_STATE/run" +export OMB_LOGS="$OMB_STATE/run/logs" +# Slot save files live on tmpfs (RAM) so the GPU<->host<->store path never +# touches a spinning/NVMe disk. Scoped + cleaned after use. +export OMB_SLOTS="${OMB_SLOTS_OVERRIDE:-/dev/shm/omb_slots}" +export OMB_SOCK="$OMB_STATE/run/sockets" +export OMB_STORE_DATA="$OMB_STATE/run/store" # mooncake local store spill + +# --- PATH --- +export PATH="$GOROOT/bin:$GOBIN:$VENV/bin:$CUDA_HOME/bin:$PATH" +export LD_LIBRARY_PATH="$CUDA_HOME/lib64:$LLAMA_BUILD/bin:${LD_LIBRARY_PATH:-}" + +# --- Default service ports (loopback only, high range to avoid clashes) --- +export OMB_BRIDGE_GRPC_PORT="${OMB_BRIDGE_GRPC_PORT:-52051}" # Go sidecar gRPC +export OMB_BRIDGE_HTTP_PORT="${OMB_BRIDGE_HTTP_PORT:-52052}" # Go sidecar metrics/HTTP +export OMB_STORE_PROXY_PORT="${OMB_STORE_PROXY_PORT:-52060}" # Python store proxy gRPC +export OMB_MASTER_PORT="${OMB_MASTER_PORT:-52061}" # mooncake master +export OMB_META_PORT="${OMB_META_PORT:-52062}" # mooncake metadata (http) +export OMB_LLAMA_PORT="${OMB_LLAMA_PORT:-52070}" # llama-server base port + +mkdir -p "$TMPDIR" "$OMB_LOGS" "$OMB_SLOTS" "$OMB_SOCK" "$OMB_STORE_DATA" \ + "$PIP_CACHE_DIR" "$GOPATH" "$GOMODCACHE" "$GOCACHE" "$HF_HOME" "$XDG_CACHE_HOME" 2>/dev/null + +# Helper: activate python venv if present +if [ -f "$VENV/bin/activate" ]; then + # shellcheck disable=SC1091 + source "$VENV/bin/activate" +fi diff --git a/mooncake-integration/ollama/scripts/gen_protos.sh b/mooncake-integration/ollama/scripts/gen_protos.sh new file mode 100644 index 00000000..9df9d78a --- /dev/null +++ b/mooncake-integration/ollama/scripts/gen_protos.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Generate gRPC stubs for both protos, into the Go module and the Python proxy. +# The generated stubs are checked in, so this is only needed after editing a +# .proto. Requires protoc + the Go protoc plugins (installed by setup_go.sh). +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" +cd "$WS" + +GO_MOD="github.com/mooncake-ai/ollama-mooncake-bridge" +GO_OUT="bridge" + +# Wait for Go protoc plugins if a background install is still finishing. +for _ in $(seq 1 60); do + [ -x "$GOBIN/protoc-gen-go" ] && [ -x "$GOBIN/protoc-gen-go-grpc" ] && break + sleep 2 +done + +echo "[proto] Go stubs (storeproxy + bridge)" +protoc -I proto -I bridge/api \ + --plugin=protoc-gen-go="$GOBIN/protoc-gen-go" \ + --plugin=protoc-gen-go-grpc="$GOBIN/protoc-gen-go-grpc" \ + --go_out="$GO_OUT" --go_opt=module="$GO_MOD" \ + --go-grpc_out="$GO_OUT" --go-grpc_opt=module="$GO_MOD" \ + proto/storeproxy.proto bridge/api/bridge.proto + +echo "[proto] Python stubs (storeproxy) -> store-proxy/gen" +mkdir -p store-proxy/gen +python -m grpc_tools.protoc -I proto \ + --python_out=store-proxy/gen \ + --grpc_python_out=store-proxy/gen \ + proto/storeproxy.proto +# make 'gen' a package and fix the absolute import grpc_tools emits +touch store-proxy/gen/__init__.py +sed -i 's/^import storeproxy_pb2 as/from . import storeproxy_pb2 as/' \ + store-proxy/gen/storeproxy_pb2_grpc.py 2>/dev/null || true + +echo "[proto] done" +find "$GO_OUT/internal" -name '*.pb.go' 2>/dev/null +ls store-proxy/gen diff --git a/mooncake-integration/ollama/scripts/llama_start.sh b/mooncake-integration/ollama/scripts/llama_start.sh new file mode 100644 index 00000000..fb24aef2 --- /dev/null +++ b/mooncake-integration/ollama/scripts/llama_start.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Start one llama.cpp server instance ("agent worker"). +# Usage: llama_start.sh [model_gguf] [ctx] [np] +# auto-pick only scans the devices listed in $OMB_GPUS. +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" + +NAME="${1:?name}"; GPU="${2:-auto}"; PORT="${3:?port}" +MODEL="${4:-$MODELS_DIR/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf}" +CTX="${5:-40960}"; NP="${6:-1}" + +pick_free_gpu() { + # pick a GPU from $OMB_GPUS that is currently near-idle (< 2 GiB used) + local used line idx mem + for idx in ${OMB_GPUS//,/ }; do + mem=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits -i "$idx" 2>/dev/null | tr -d ' ') + if [ -n "$mem" ] && [ "$mem" -lt 2048 ]; then echo "$idx"; return 0; fi + done + echo "${OMB_GPUS%%,*}" # fallback: first allowed gpu +} + +if [ "$GPU" = "auto" ]; then GPU="$(pick_free_gpu)"; fi + +if ss -ltn 2>/dev/null | grep -q "127.0.0.1:$PORT "; then + echo "[llama:$NAME] already on $PORT"; exit 0 +fi + +LOG="$OMB_LOGS/llama_${NAME}.log" +KVTYPE="${OMB_LLAMA_KVTYPE:-f16}" # f16 | q8_0 | q4_0 (smaller KV => faster store transfer) +EXTRA=() +if [ "$KVTYPE" != "f16" ]; then + # quantized KV cache requires flash attention in llama.cpp + EXTRA+=(--cache-type-k "$KVTYPE" --cache-type-v "$KVTYPE" -fa on) +elif [ "${OMB_LLAMA_FA:-0}" = "1" ]; then + EXTRA+=(-fa on) +fi +echo "[llama:$NAME] gpu=$GPU port=$PORT ctx=$CTX np=$NP kv=$KVTYPE model=$(basename "$MODEL")" +CUDA_VISIBLE_DEVICES="$GPU" nohup "$LLAMA_BUILD/bin/llama-server" \ + -m "$MODEL" \ + --host 127.0.0.1 --port "$PORT" \ + -ngl 99 -c "$CTX" -np "$NP" -t 8 \ + --slot-save-path "$OMB_SLOTS/" \ + --no-webui \ + "${EXTRA[@]}" \ + > "$LOG" 2>&1 & +echo $! > "$OMB_RUN/llama_${NAME}.pid" + +# wait for health +for _ in $(seq 1 240); do + if curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then + echo "[llama:$NAME] healthy on $PORT (gpu $GPU)"; exit 0 + fi + sleep 0.5 +done +echo "[llama:$NAME] FAILED to become healthy"; tail -30 "$LOG"; exit 1 diff --git a/mooncake-integration/ollama/scripts/master_start.sh b/mooncake-integration/ollama/scripts/master_start.sh new file mode 100644 index 00000000..107ef502 --- /dev/null +++ b/mooncake-integration/ollama/scripts/master_start.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Start the Mooncake master (idempotent). Logs + glog under run/logs. +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" + +if ss -ltn 2>/dev/null | grep -q ":$OMB_MASTER_PORT "; then + echo "[master] already listening on $OMB_MASTER_PORT" + exit 0 +fi +nohup "$VENV/bin/mooncake_master" \ + -port "$OMB_MASTER_PORT" \ + -metrics_port "$((OMB_MASTER_PORT+2))" \ + -enable_metric_reporting=true \ + -default_kv_lease_ttl="${OMB_KV_LEASE_TTL:-30000}" \ + -default_kv_soft_pin_ttl="${OMB_KV_SOFT_PIN_TTL:-600000}" \ + -log_dir="$OMB_LOGS" \ + > "$OMB_LOGS/master.log" 2>&1 & +echo $! > "$OMB_RUN/master.pid" +for _ in $(seq 1 30); do + ss -ltn 2>/dev/null | grep -q ":$OMB_MASTER_PORT " && { echo "[master] up on $OMB_MASTER_PORT (pid $(cat "$OMB_RUN/master.pid"))"; exit 0; } + sleep 0.3 +done +echo "[master] FAILED to start"; tail -20 "$OMB_LOGS/master.log"; exit 1 diff --git a/mooncake-integration/ollama/scripts/master_stop.sh b/mooncake-integration/ollama/scripts/master_stop.sh new file mode 100644 index 00000000..e74f1b72 --- /dev/null +++ b/mooncake-integration/ollama/scripts/master_stop.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Stop ONLY this workspace's processes. We scope every match to $WS so we never +# touch any co-located workspace sharing this machine. +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" +# master is bound to our unique port; safe to match on that. +pkill -f "mooncake_master -port $OMB_MASTER_PORT" 2>/dev/null || true +# everything else: match only command lines that mention our source path. +pkill -f "$WS/benchmarks/microbench" 2>/dev/null || true +pkill -f "$WS/store-proxy" 2>/dev/null || true +rm -f "$OMB_RUN/master.pid" +echo "[master] stopped (workspace-scoped)" diff --git a/mooncake-integration/ollama/scripts/proxy_start.sh b/mooncake-integration/ollama/scripts/proxy_start.sh new file mode 100644 index 00000000..60cec550 --- /dev/null +++ b/mooncake-integration/ollama/scripts/proxy_start.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Start the Mooncake store proxy (gRPC) wrapping the real distributed store. +# Usage: proxy_start.sh [tcp|rdma] (default: $OMB_STORE_PROTOCOL or rdma) +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" + +PROTO="${1:-${OMB_STORE_PROTOCOL:-rdma}}" +DEV="${OMB_STORE_DEVICE:-}" +if [ "$PROTO" = "rdma" ] && [ -z "$DEV" ]; then DEV="mlx5_0"; fi + +if ss -ltn 2>/dev/null | grep -qE "[:.]$OMB_STORE_PROXY_PORT\b"; then + echo "[proxy] already listening on $OMB_STORE_PROXY_PORT"; exit 0 +fi + +nohup python "$WS/store-proxy/store_proxy.py" \ + --listen "127.0.0.1:$OMB_STORE_PROXY_PORT" \ + --backend mooncake \ + --protocol "$PROTO" --device "$DEV" \ + --master "127.0.0.1:$OMB_MASTER_PORT" \ + --metadata "P2PHANDSHAKE" \ + --global-segment-size "$(( ${OMB_STORE_SEGMENT_GB:-32} << 30 ))" \ + --local-buffer-size "$(( ${OMB_STORE_BUFFER_GB:-8} << 30 ))" \ + --stripe-mb "${OMB_STORE_STRIPE_MB:-64}" \ + --warmup \ + > "$OMB_LOGS/store_proxy.log" 2>&1 & +echo $! > "$OMB_RUN/store_proxy.pid" +echo "[proxy] starting (proto=$PROTO dev=$DEV) pid $(cat "$OMB_RUN/store_proxy.pid")" +# RDMA memory registration of the segment+buffer can take ~1-2 min the first time. +for _ in $(seq 1 360); do + ss -ltn 2>/dev/null | grep -qE "[:.]$OMB_STORE_PROXY_PORT\b" && { echo "[proxy] up on $OMB_STORE_PROXY_PORT"; exit 0; } + kill -0 "$(cat "$OMB_RUN/store_proxy.pid")" 2>/dev/null || { echo "[proxy] process died"; tail -25 "$OMB_LOGS/store_proxy.log"; exit 1; } + sleep 0.5 +done +echo "[proxy] FAILED"; tail -25 "$OMB_LOGS/store_proxy.log"; exit 1 diff --git a/mooncake-integration/ollama/scripts/setup_go.sh b/mooncake-integration/ollama/scripts/setup_go.sh new file mode 100644 index 00000000..7b1cec0f --- /dev/null +++ b/mooncake-integration/ollama/scripts/setup_go.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Install a local Go toolchain under $OMB_STATE/go (never touches the system or +# the source tree). +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" + +if [ -x "$GOROOT/bin/go" ]; then + echo "[go] already installed: $("$GOROOT/bin/go" version)" +else + mkdir -p "$OMB_STATE" + VER="$(curl -fsSL https://go.dev/VERSION?m=text 2>/dev/null | head -1 || echo go1.23.4)" + [ -z "$VER" ] && VER="go1.23.4" + TARBALL="${VER}.linux-amd64.tar.gz" + echo "[go] downloading $TARBALL" + curl -fL --retry 3 -o "$TMPDIR/$TARBALL" "https://go.dev/dl/${TARBALL}" + rm -rf "$GOROOT" + # tarball unpacks to a 'go/' dir; GOROOT is exactly $OMB_STATE/go + tar -C "$OMB_STATE" -xzf "$TMPDIR/$TARBALL" + rm -f "$TMPDIR/$TARBALL" + "$GOROOT/bin/go" version + echo "[go] done" +fi + +# protoc plugins for regenerating gRPC stubs (the checked-in stubs already build; +# these are only needed if you edit a .proto and run scripts/gen_protos.sh). +if [ ! -x "$GOBIN/protoc-gen-go" ] || [ ! -x "$GOBIN/protoc-gen-go-grpc" ]; then + echo "[go] installing protoc-gen-go / protoc-gen-go-grpc" + "$GOROOT/bin/go" install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2 || true + "$GOROOT/bin/go" install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.4.0 || true +fi diff --git a/mooncake-integration/ollama/scripts/setup_llama.sh b/mooncake-integration/ollama/scripts/setup_llama.sh new file mode 100644 index 00000000..66a1faf2 --- /dev/null +++ b/mooncake-integration/ollama/scripts/setup_llama.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Clone + build llama.cpp with CUDA (H200 = sm_90) and SHARED libs (needed for cgo Stage-2). +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" + +if [ ! -d "$LLAMA_DIR/.git" ]; then + echo "[llama] cloning" + git clone --depth 1 https://github.com/ggml-org/llama.cpp "$LLAMA_DIR" +fi +cd "$LLAMA_DIR" +echo "[llama] HEAD: $(git rev-parse --short HEAD)" + +cmake -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DGGML_CUDA=ON \ + -DCMAKE_CUDA_ARCHITECTURES=90 \ + -DBUILD_SHARED_LIBS=ON \ + -DLLAMA_CURL=OFF \ + -DLLAMA_BUILD_TESTS=OFF \ + -DLLAMA_BUILD_EXAMPLES=ON \ + -DLLAMA_BUILD_SERVER=ON + +# Limit build parallelism to a fixed job count rather than all available cores. +cmake --build build --target llama-server llama-cli -j 24 +echo "[llama] server: $LLAMA_BUILD/bin/llama-server" +ls -la "$LLAMA_BUILD/bin" | head diff --git a/mooncake-integration/ollama/scripts/setup_models.sh b/mooncake-integration/ollama/scripts/setup_models.sh new file mode 100644 index 00000000..394c1eff --- /dev/null +++ b/mooncake-integration/ollama/scripts/setup_models.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Download small GGUF coder models (weights only; KV is computed at runtime). +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" +cd "$MODELS_DIR" + +dl() { # name url + local out="$1" url="$2" + if [ -f "$out" ] && [ "$(stat -c%s "$out")" -gt 100000000 ]; then + echo "[model] have $out ($(du -h "$out" | cut -f1))"; return 0 + fi + echo "[model] downloading $out" + curl -fL --retry 4 --retry-delay 3 -o "$out.part" "$url" + mv "$out.part" "$out" + echo "[model] done $out ($(du -h "$out" | cut -f1))" +} + +# 1.5B for fast iteration, 7B for the main results (bigger KV => bigger transfer win). +dl qwen2.5-coder-1.5b-instruct-q4_k_m.gguf \ + "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf" + +if [ "${1:-}" = "--with-7b" ]; then + dl qwen2.5-coder-7b-instruct-q4_k_m.gguf \ + "https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-GGUF/resolve/main/qwen2.5-coder-7b-instruct-q4_k_m.gguf" +fi +ls -la "$MODELS_DIR" diff --git a/mooncake-integration/ollama/scripts/setup_py.sh b/mooncake-integration/ollama/scripts/setup_py.sh new file mode 100644 index 00000000..b0a83770 --- /dev/null +++ b/mooncake-integration/ollama/scripts/setup_py.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Create a venv on /data2 and install Python deps (mooncake + grpc + bench tooling). +set -euo pipefail +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" + +if [ ! -d "$VENV" ]; then + echo "[py] creating venv at $VENV" + python3 -m venv "$VENV" +fi +# shellcheck disable=SC1091 +source "$VENV/bin/activate" + +python -m pip install --upgrade pip wheel setuptools >/dev/null + +echo "[py] installing core deps" +pip install \ + "mooncake-transfer-engine==0.3.11.post1" \ + "grpcio==1.62.3" "grpcio-tools==1.62.3" "protobuf>=4.25,<5" \ + "numpy" "requests" "aiohttp" "rich" "matplotlib" "pyyaml" "prometheus-client" + +echo "[py] verifying mooncake imports" +python - <<'PY' +import importlib, sys +ok = True +for m in ("mooncake.engine", "mooncake.store"): + try: + mod = importlib.import_module(m) + print(f" OK {m} -> {getattr(mod,'__file__','?')}") + except Exception as e: + ok = False + print(f" ERR {m}: {e}") +try: + from mooncake.store import MooncakeDistributedStore, ReplicateConfig # noqa + print(" OK MooncakeDistributedStore + ReplicateConfig") +except Exception as e: + print(f" WARN MooncakeDistributedStore/ReplicateConfig: {e}") +try: + from mooncake.engine import TransferEngine + print(" OK TransferEngine") +except Exception as e: + print(f" WARN TransferEngine: {e}") +sys.exit(0 if ok else 1) +PY +echo "[py] done" diff --git a/mooncake-integration/ollama/scripts/stack_down.sh b/mooncake-integration/ollama/scripts/stack_down.sh new file mode 100644 index 00000000..60cf3d5a --- /dev/null +++ b/mooncake-integration/ollama/scripts/stack_down.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Tear down ONLY this workspace's services. Workspace-scoped so any co-located +# environment on this machine is never touched. +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh" + +# kill by recorded PIDs first +for pf in "$OMB_RUN"/llama_*.pid "$OMB_RUN"/bridged.pid "$OMB_RUN"/store_proxy.pid; do + [ -f "$pf" ] || continue + pid=$(cat "$pf" 2>/dev/null || true) + [ -n "${pid:-}" ] && kill "$pid" 2>/dev/null || true + rm -f "$pf" +done +# belt-and-suspenders: match only command lines mentioning our state/source paths +pkill -f "$OMB_RUN/bridged" 2>/dev/null || true +pkill -f "$WS/store-proxy" 2>/dev/null || true +pkill -f "slot-save-path $OMB_SLOTS" 2>/dev/null || true +# master is on our unique port +pkill -f "mooncake_master -port $OMB_MASTER_PORT" 2>/dev/null || true +rm -f "$OMB_RUN/master.pid" +echo "[stack] down (workspace-scoped)" diff --git a/mooncake-integration/ollama/scripts/stack_up.sh b/mooncake-integration/ollama/scripts/stack_up.sh new file mode 100644 index 00000000..be5a2711 --- /dev/null +++ b/mooncake-integration/ollama/scripts/stack_up.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Bring up the full stack: master + store-proxy + N llama servers + sidecar. +# Usage: stack_up.sh [num_servers=2] [tcp|rdma=rdma] [model_gguf] [ctx=40960] +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$HERE/env.sh" + +N="${1:-2}"; PROTO="${2:-rdma}" +MODEL="${3:-$MODELS_DIR/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf}" +CTX="${4:-40960}" + +echo "==> master"; bash "$HERE/master_start.sh" +echo "==> store proxy"; OMB_STORE_PROTOCOL="$PROTO" bash "$HERE/proxy_start.sh" "$PROTO" +echo "==> $N llama servers"; +for i in $(seq 0 $((N-1))); do + bash "$HERE/llama_start.sh" "a$i" auto "$((OMB_LLAMA_PORT + i))" "$MODEL" "$CTX" 1 +done +echo "==> sidecar"; bash "$HERE/bridged_start.sh" mooncake + +echo +echo "stack is up:" +echo " master 127.0.0.1:$OMB_MASTER_PORT" +echo " store proxy 127.0.0.1:$OMB_STORE_PROXY_PORT (proto=$PROTO)" +for i in $(seq 0 $((N-1))); do echo " llama a$i 127.0.0.1:$((OMB_LLAMA_PORT + i))"; done +echo " sidecar gRPC 127.0.0.1:$OMB_BRIDGE_GRPC_PORT" +echo " sidecar HTTP 127.0.0.1:$OMB_BRIDGE_HTTP_PORT (/metrics /stats /healthz)" +echo +curl -fsS "http://127.0.0.1:$OMB_BRIDGE_HTTP_PORT/healthz" && echo -- 2.34.1 From fd7bbf720afc376f0cc35a6a15b9cc1763a0b077 Mon Sep 17 00:00:00 2001 From: zbtrs2 Date: Mon, 29 Jun 2026 01:52:29 +0800 Subject: [PATCH 8/9] ollama-kvcache: add deployment, observability and docs deploy packages the topology as docker-compose (master, store proxy, sidecar, patched Ollama, Prometheus, Grafana) with the RDMA/GPUDirect host passthrough it needs, a Prometheus scrape config and a Grafana dashboard whose primary panel integrates the live count of avoided prefill tokens. docs/REPORT.md is the full write-up: design, what is real versus simulated, the measured store bandwidth, the multi-agent results, the adaptive-arbiter behaviour, the Stage-2 microbenchmark and the honest limitations. The README is the entry point and quickstart, and the figures and result JSONs are the measured data behind the report. --- mooncake-integration/ollama/.gitignore | 12 + mooncake-integration/ollama/README.md | 173 ++++++++++++ .../ollama/deploy/docker-compose.yml | 68 +++++ .../ollama/deploy/grafana_dashboard.json | 77 +++++ .../ollama/deploy/mooncake_store.yaml | 24 ++ .../ollama/deploy/prometheus.yml | 17 ++ mooncake-integration/ollama/docs/REPORT.md | 262 ++++++++++++++++++ .../ollama/docs/demo_result.json | 138 +++++++++ .../ollama/docs/figures/arbiter.png | Bin 0 -> 56894 bytes .../ollama/docs/figures/architecture.dot | 36 +++ .../ollama/docs/figures/architecture.png | Bin 0 -> 94121 bytes .../ollama/docs/figures/per_agent.png | Bin 0 -> 61778 bytes .../ollama/docs/figures/recompute.png | Bin 0 -> 45210 bytes .../ollama/docs/figures/scaling_agents.png | Bin 0 -> 78721 bytes .../ollama/docs/figures/stage2_path.dot | 28 ++ .../ollama/docs/figures/stage2_path.png | Bin 0 -> 41481 bytes .../ollama/docs/figures/throughput.png | Bin 0 -> 43026 bytes .../ollama/docs/figures/ttft.png | Bin 0 -> 60722 bytes .../ollama/docs/matrix_results.json | 83 ++++++ .../ollama/docs/stage2_kvbench.txt | 15 + 20 files changed, 933 insertions(+) create mode 100644 mooncake-integration/ollama/.gitignore create mode 100644 mooncake-integration/ollama/README.md create mode 100644 mooncake-integration/ollama/deploy/docker-compose.yml create mode 100644 mooncake-integration/ollama/deploy/grafana_dashboard.json create mode 100644 mooncake-integration/ollama/deploy/mooncake_store.yaml create mode 100644 mooncake-integration/ollama/deploy/prometheus.yml create mode 100644 mooncake-integration/ollama/docs/REPORT.md create mode 100644 mooncake-integration/ollama/docs/demo_result.json create mode 100644 mooncake-integration/ollama/docs/figures/arbiter.png create mode 100644 mooncake-integration/ollama/docs/figures/architecture.dot create mode 100644 mooncake-integration/ollama/docs/figures/architecture.png create mode 100644 mooncake-integration/ollama/docs/figures/per_agent.png create mode 100644 mooncake-integration/ollama/docs/figures/recompute.png create mode 100644 mooncake-integration/ollama/docs/figures/scaling_agents.png create mode 100644 mooncake-integration/ollama/docs/figures/stage2_path.dot create mode 100644 mooncake-integration/ollama/docs/figures/stage2_path.png create mode 100644 mooncake-integration/ollama/docs/figures/throughput.png create mode 100644 mooncake-integration/ollama/docs/figures/ttft.png create mode 100644 mooncake-integration/ollama/docs/matrix_results.json create mode 100644 mooncake-integration/ollama/docs/stage2_kvbench.txt diff --git a/mooncake-integration/ollama/.gitignore b/mooncake-integration/ollama/.gitignore new file mode 100644 index 00000000..8dd38df0 --- /dev/null +++ b/mooncake-integration/ollama/.gitignore @@ -0,0 +1,12 @@ +# Generated state lives OUTSIDE this tree (see scripts/env.sh -> $OMB_STATE). +# These patterns guard against accidental in-tree artifacts only. +__pycache__/ +*.pyc +*.pyo + +# the Stage-2 microbench binary, if built in-place +/bridge/cbridge/omb_kvbench + +# editor / OS +.DS_Store +*.swp diff --git a/mooncake-integration/ollama/README.md b/mooncake-integration/ollama/README.md new file mode 100644 index 00000000..6d79d350 --- /dev/null +++ b/mooncake-integration/ollama/README.md @@ -0,0 +1,173 @@ +# Ollama × Mooncake — global KV-cache reuse for local agent swarms + +> **6 coding agents. One shared 30k-token repo context. 1 prefill. 5 RDMA restores. 0 redundant prefill.** +> +> `100% of redundant prefill eliminated` · `prefill work stays flat as the swarm grows` · `−69% TTFT for an uncontended cross-GPU restore, −34% swarm mean at 7B` · measured on 8×H200 + +Let a swarm of local coding agents share one long-prefix KV cache. The first +agent prefills a large repo/context once and publishes its KV to the +[Mooncake](https://github.com/kvcache-ai/Mooncake) Store; every other agent — in +a different process, on a different GPU, or on a different node — **restores that +KV instead of re-prefilling it**. + +![Redundant prefill work vs swarm size](docs/figures/scaling_agents.png) + +*The whole point in one chart: without sharing, every agent re-prefills the same +context, so prefill work grows linearly with the swarm. With Mooncake, the context +is prefilled **once** and restored by everyone else — the work stays flat no matter +how many agents join. Real measurement, 7B model, 30k-token context, 1→8 agents.* + +Ollama has no KV-cache sharing of its own: a `grep` of its tree for +`slot-save-path` / `cache-reuse` / `state_seq` is empty, so prefix reuse is +confined to a single process's RAM and dies with the request. Mooncake already +pools and shares KV for vLLM / SGLang / TRT-LLM in the data centre; this +integration extends that reuse down to the workstation/edge multi-agent +scenario, with a sidecar design that needs almost no change to Ollama itself. + +Built and measured on real hardware: 8× NVIDIA H200, Mellanox RDMA (mlx5), the +real `mooncake-transfer-engine` distributed store over RDMA, a CUDA build of +llama.cpp, and Qwen2.5-Coder 1.5B / 7B GGUF models. + +## Results (measured) + +* **Redundant prefill is eliminated, and the win compounds with the swarm.** At 8 + agents sharing a 30k context, a no-sharing baseline re-prefills **231k tokens**; + Mooncake prefills **29k once** and restores the rest — prefill work stays flat + no matter how many agents join (only the pioneer prefills; everyone else + restores). This is a property of the mechanism, not of timing, and holds in + every run. +* **Throughput / TTFT track store bandwidth.** A single uncontended cross-GPU + restore cuts **time-to-first-token by ~69%** (5.99 s → 1.83 s); across a + 6-agent swarm the **mean TTFT drops ~34%** at 7B/30k (and ~33% at 7B/16k, 1.25× + throughput). The gain grows with model size and context length, and the single + store client + single NIC serialize concurrent restores — so the swarm mean + trails the single-reuser number until the store is scaled out. +* A **restore-vs-recompute arbiter** that learns store bandwidth and prefill + rate online and **never sustains a loss**: it restores when it helps and falls + back to recompute when the store is too slow to beat a local prefill. +* **35–44 GB/s** striped zero-copy KV transfer over RDMA (uncontended). +* Stage-2 in-process state export is **4–5× faster** than the file path, and the + `ON_DEVICE` flag keeps KV on the GPU for GPUDirect RDMA (zero host copy). + +Full methodology and figures: **[docs/REPORT.md](docs/REPORT.md)**. + +## What's new here + +| Contribution | What it is | Why it matters | +|---|---|---| +| **Cross-process KV bus** | a KV cache shared across processes, GPUs and nodes via the Mooncake Store | brings data-centre KV pooling to local agent swarms; Ollama had none | +| **Content-addressed chained keys** | model fingerprint + forward-chained per-block hash | exact longest-prefix matching; KV is never mixed across models/tokenizers | +| **Radix prefix index** | cross-process analogue of RadixAttention over remote KV snapshots | O(blocks) longest-prefix lookup + per-prefix hotness for replication | +| **Restore-vs-recompute arbiter** | learns store bandwidth and prefill rate online, picks the cheaper | makes sharing **loss-free** — safe to enable unconditionally | +| **Striped RDMA + Stage-2 ON_DEVICE** | pre-registered staging pool; cgo `state_seq_*_ext` keeps KV on the GPU | sustains 35–44 GB/s; targets GPUDirect with zero host copy | + +## Architecture (3 decoupled processes) + +![Architecture](docs/figures/architecture.png) + +``` +agents / patched Ollama ─► bridge (Go sidecar) ─► store-proxy (Python) ─► Mooncake Store + key + radix index + arbiter 1 warm store client, RDMA / GPUDirect + 3-stage Lookup/Prepare/Commit striped batch RDMA, DRAM pool, replicas + │ pre-registered staging + llama.cpp /slots (Stage 1) + cgo _ext ON_DEVICE (Stage 2) +``` + +* **`bridge/`** (Go) — the brain. Pure Go, no heavy deps. Serves a gRPC + `KVCacheBus` (Lookup/Prepare/Commit) **and** an HTTP/JSON gateway with a + Prometheus `/metrics` endpoint. +* **`store-proxy/`** (Python) — owns the single warm + `mooncake.store.MooncakeDistributedStore` handle (the official client) and + exposes it over gRPC. KV blobs are passed as **file paths**, so a multi-GiB + blob is copied at most once and never streams through Go. +* **llama.cpp server** — unmodified for Stage 1; driven via its `/slots` + save/restore endpoints. + +## Repo layout + +``` +bridge/ Go sidecar (the core deliverable) + internal/cachekey/ GGUF parser + chained block-hash cache keys + internal/prefixindex/ cross-process radix tree of KV prefixes + internal/arbiter/ restore-vs-recompute cost arbiter (online learning) + internal/orchestrator/ 3-stage Lookup → Prepare(restore) → Commit(save) + internal/store/ pluggable backend: mooncake (proxy) | local file + internal/llamabridge/ Stage-1 /slots HTTP client + internal/seqstate/ Stage-2 cgo binding (llama_state_seq_*_ext, ON_DEVICE) + internal/server/ gRPC KVCacheBus + HTTP/JSON + /metrics + api/bridge.proto agent ↔ sidecar contract + cbridge/omb_kvbench.cpp Stage-2 libllama microbenchmark +store-proxy/ Python gRPC wrapper over MooncakeDistributedStore +proto/storeproxy.proto sidecar ↔ store-proxy contract +ollama-patches/ diffs to ollama/ollama (+ README, integration notes) +benchmarks/ agent_swarm.py, run_matrix.py, plot.py, micro-benchmarks +deploy/ docker-compose, prometheus.yml, grafana dashboard +scripts/ env.sh + setup/start/stop/demo scripts (state kept off-tree) +docs/ REPORT.md, figures/, result JSONs +``` + +`scripts/env.sh` keeps **all** build/runtime state (Go toolchain, venv, llama.cpp, +models, logs) under `$OMB_STATE` (default: a `.omb-state/` dir *outside* the repo), +so the source tree stays clean and nothing is written under `/`. + +## Quickstart + +```bash +# 1. one-time setup (local Go, venv + mooncake, llama.cpp CUDA build, models) +bash scripts/setup_go.sh +bash scripts/setup_py.sh +bash scripts/setup_llama.sh +bash scripts/setup_models.sh --with-7b + +# 2. run the multi-agent demo (brings up master + store-proxy + 2 llama + sidecar) +bash scripts/demo.sh 7b 6 + +# 3. (optional) the full benchmark matrix + figures +python benchmarks/run_matrix.py --config benchmarks/workloads/matrix.json --out "$OMB_STATE/run/matrix_results.json" +python benchmarks/plot.py --matrix "$OMB_STATE/run/matrix_results.json" \ + --per-agent "$OMB_STATE/run/demo_result.json" --scaling benchmarks/scaling.json + +# 4. (optional) Stage-2 KV-state microbenchmark +bash bridge/cbridge/build_kvbench.sh +CUDA_VISIBLE_DEVICES=0 "$OMB_STATE/run/omb_kvbench" "$OMB_STATE/models/qwen2.5-coder-7b-instruct-q4_k_m.gguf" 16000 + +# tear everything down (only this integration's processes) +bash scripts/stack_down.sh +``` + +The demo ends on a one-glance scoreboard (real run, 6 agents / 30k ctx, concurrency 2): + +``` +================================================================ + SCOREBOARD +================================================================ + agents sharing context : 6 + prefill tokens, no sharing : 173,653 + prefill tokens, Mooncake : 28,998 (144,655 saved) + total prefill work avoided : 83% + redundant prefill killed : 100% (only the pioneer must prefill) + mean TTFT : 5868 ms -> 3898 ms + swarm throughput : 1.01x + cache hits : 5/6 +================================================================ +``` + +*Two honest numbers: 83% of **all** swarm prefill work is avoided, and 100% of the +**redundant** prefill is gone — the one pioneer prefill is unavoidable. A single +uncontended cross-GPU restore cuts TTFT ~69% (5.99 s → 1.83 s); the swarm mean +trails it because one store client + one NIC serialize concurrent restores.* + +Observability: the sidecar exposes Prometheus at `http://127.0.0.1:52052/metrics` +(`mooncake_bridge_saved_prefill_tokens_total` is the primary series); a Grafana +dashboard is in `deploy/grafana_dashboard.json`. + +## Design notes + +* **Correctness first:** any difference in model / tokenizer / RoPE / KV dtype / + layout lands in a different key space — KV is never mixed across models. The + chained block hash makes longest-prefix matching exact. +* **No disk in the hot path:** slot files live on `/dev/shm`; KV moves + file↔store at most once via RDMA into pre-registered, pinned buffers. +* **Scoped runtime:** every cache/build/model lives under this directory, and + all process management is scoped to this workspace. diff --git a/mooncake-integration/ollama/deploy/docker-compose.yml b/mooncake-integration/ollama/deploy/docker-compose.yml new file mode 100644 index 00000000..b68ea0e9 --- /dev/null +++ b/mooncake-integration/ollama/deploy/docker-compose.yml @@ -0,0 +1,68 @@ +# Ollama × Mooncake KVCache Bus — containerised deployment. +# +# NOTE: the tested, reproducible path on this machine is the native scripts +# (scripts/stack_up.sh). This compose file packages the same topology for +# portability. RDMA + GPUDirect require host devices to be passed through (see +# the `devices`/`ipc` settings), so it is meant for a node with Mellanox NICs and +# NVIDIA GPUs and the NVIDIA Container Toolkit installed. The `build:` services +# expect a Dockerfile in each context; add one (or replace `build:` with a +# pre-built `image:`) before `docker compose up`. Ports match scripts/env.sh. +version: "3.9" + +x-mooncake-env: &mc-env + MOONCAKE_MASTER: "127.0.0.1:52061" + MOONCAKE_PROTOCOL: "rdma" + MOONCAKE_DEVICE: "mlx5_0" + MOONCAKE_TE_META_DATA_SERVER: "P2PHANDSHAKE" + +services: + mooncake-master: + image: kvcache/mooncake:latest # or build from kvcache-ai/Mooncake + command: ["mooncake_master", "-port", "52061", "-metrics_port", "52063", + "-enable_metric_reporting=true", "-default_kv_lease_ttl=2000"] + network_mode: host + + store-proxy: + build: { context: ../mooncake-store-proxy } + command: ["python", "store_proxy.py", "--listen", "127.0.0.1:52060", + "--backend", "mooncake", "--protocol", "rdma", "--device", "mlx5_0", + "--master", "127.0.0.1:52061", "--metadata", "P2PHANDSHAKE", + "--global-segment-size", "34359738368", "--staging-mb", "4096", + "--staging-count", "3", "--warmup"] + environment: *mc-env + network_mode: host + ipc: host # shared /dev/shm for slot files + devices: ["/dev/infiniband:/dev/infiniband"] + cap_add: ["IPC_LOCK"] # RDMA memory pinning + depends_on: [mooncake-master] + + bridged: # the Go sidecar (ollama-mooncake-bridge) + build: { context: ../ollama-mooncake-bridge } + command: ["/bridged", "-grpc-addr", "127.0.0.1:52051", "-http-addr", "127.0.0.1:52052", + "-store-backend", "mooncake", "-store-proxy-addr", "127.0.0.1:52060", + "-slot-save-path", "/dev/shm/omb-slots/"] + network_mode: host + ipc: host + depends_on: [store-proxy] + + # ollama-with-patches: build from a fork with ollama-patches/ applied. + ollama: + image: ollama-mooncake:latest # ollama built with ollama-patches/* + environment: + OLLAMA_MOONCAKE_BRIDGE_URL: "http://127.0.0.1:52052" + OLLAMA_MOONCAKE_SLOT_SAVE_PATH: "/dev/shm/omb-slots" + network_mode: host + ipc: host + runtime: nvidia + depends_on: [bridged] + + prometheus: + image: prom/prometheus:latest + command: ["--config.file=/etc/prometheus/prometheus.yml"] + volumes: ["./prometheus.yml:/etc/prometheus/prometheus.yml:ro"] + network_mode: host + + grafana: + image: grafana/grafana:latest + network_mode: host + depends_on: [prometheus] diff --git a/mooncake-integration/ollama/deploy/grafana_dashboard.json b/mooncake-integration/ollama/deploy/grafana_dashboard.json new file mode 100644 index 00000000..fd3d10f4 --- /dev/null +++ b/mooncake-integration/ollama/deploy/grafana_dashboard.json @@ -0,0 +1,77 @@ +{ + "title": "Ollama × Mooncake — Local Agent Swarm KVCache Bus", + "uid": "omb-kvcache", + "schemaVersion": 39, + "time": { "from": "now-15m", "to": "now" }, + "refresh": "2s", + "panels": [ + { + "type": "stat", "title": "Prefill tokens saved (cumulative)", "gridPos": {"h":7,"w":6,"x":0,"y":0}, + "options": {"colorMode": "background", "graphMode": "area", "textMode": "value"}, + "fieldConfig": {"defaults": {"unit": "short", "color": {"mode": "fixed", "fixedColor": "green"}}}, + "targets": [{"expr": "mooncake_bridge_saved_prefill_tokens_total", "legendFormat": "saved tokens"}] + }, + { + "type": "stat", "title": "KV restored from store", "gridPos": {"h":7,"w":6,"x":6,"y":0}, + "options": {"colorMode": "background", "textMode": "value"}, + "fieldConfig": {"defaults": {"unit": "bytes", "color": {"mode": "fixed", "fixedColor": "blue"}}}, + "targets": [{"expr": "mooncake_bridge_bytes_get_total", "legendFormat": "bytes restored"}] + }, + { + "type": "stat", "title": "Cache hit ratio", "gridPos": {"h":7,"w":6,"x":12,"y":0}, + "options": {"colorMode": "background"}, + "fieldConfig": {"defaults": {"unit": "percentunit", "color": {"mode": "fixed", "fixedColor": "purple"}}}, + "targets": [{"expr": "mooncake_bridge_hits_total / clamp_min(mooncake_bridge_hits_total + mooncake_bridge_misses_total + mooncake_bridge_recompute_chosen_total, 1)", "legendFormat": "hit ratio"}] + }, + { + "type": "stat", "title": "Restores performed", "gridPos": {"h":7,"w":6,"x":18,"y":0}, + "options": {"colorMode": "background", "textMode": "value"}, + "fieldConfig": {"defaults": {"unit": "short", "color": {"mode": "fixed", "fixedColor": "green"}}}, + "targets": [{"expr": "mooncake_bridge_restore_total", "legendFormat": "restores"}] + }, + { + "type": "stat", "title": "Arbiter: learned restore BW / prefill rate", "gridPos": {"h":7,"w":24,"x":0,"y":7}, + "targets": [ + {"expr": "mooncake_bridge_learned_get_gbps", "legendFormat": "restore GB/s"}, + {"expr": "mooncake_bridge_learned_prefill_tps", "legendFormat": "prefill tok/s"} + ] + }, + { + "type": "timeseries", "title": "Saved prefill tokens / sec", "gridPos": {"h":8,"w":12,"x":0,"y":14}, + "targets": [{"expr": "rate(mooncake_bridge_saved_prefill_tokens_total[1m])", "legendFormat": "tokens/s saved"}] + }, + { + "type": "timeseries", "title": "Decisions/sec (hit vs miss vs recompute-chosen)", "gridPos": {"h":8,"w":12,"x":12,"y":14}, + "targets": [ + {"expr": "rate(mooncake_bridge_hits_total[1m])", "legendFormat": "restore (hit)"}, + {"expr": "rate(mooncake_bridge_misses_total[1m])", "legendFormat": "miss"}, + {"expr": "rate(mooncake_bridge_recompute_chosen_total[1m])", "legendFormat": "recompute (arbiter)"} + ] + }, + { + "type": "timeseries", "title": "Store traffic (bytes/s)", "gridPos": {"h":8,"w":12,"x":0,"y":22}, + "fieldConfig": {"defaults": {"unit": "Bps"}}, + "targets": [ + {"expr": "rate(mooncake_bridge_bytes_get_total[1m])", "legendFormat": "get"}, + {"expr": "rate(mooncake_bridge_bytes_put_total[1m])", "legendFormat": "put"} + ] + }, + { + "type": "timeseries", "title": "Restore latency p50/p90 (ms)", "gridPos": {"h":8,"w":12,"x":12,"y":22}, + "fieldConfig": {"defaults": {"unit": "ms"}}, + "targets": [ + {"expr": "histogram_quantile(0.5, rate(mooncake_bridge_restore_latency_ms_bucket[1m]))", "legendFormat": "p50"}, + {"expr": "histogram_quantile(0.9, rate(mooncake_bridge_restore_latency_ms_bucket[1m]))", "legendFormat": "p90"} + ] + }, + { + "type": "stat", "title": "KV prefix snapshots in radix index", "gridPos": {"h":6,"w":12,"x":0,"y":30}, + "targets": [{"expr": "mooncake_bridge_index_snapshots", "legendFormat": "snapshots"}] + }, + { + "type": "stat", "title": "Index bytes", "gridPos": {"h":6,"w":12,"x":12,"y":30}, + "fieldConfig": {"defaults": {"unit": "bytes"}}, + "targets": [{"expr": "mooncake_bridge_index_bytes", "legendFormat": "bytes"}] + } + ] +} diff --git a/mooncake-integration/ollama/deploy/mooncake_store.yaml b/mooncake-integration/ollama/deploy/mooncake_store.yaml new file mode 100644 index 00000000..a6489080 --- /dev/null +++ b/mooncake-integration/ollama/deploy/mooncake_store.yaml @@ -0,0 +1,24 @@ +# Mooncake Store configuration consumed by the store proxy / clients. +# (See mooncake.mooncake_config.MooncakeConfig — loaded via MOONCAKE_CONFIG_PATH.) +# +# This documents the knobs we tuned for the local-agent-swarm scenario. + +local_hostname: "127.0.0.1" +metadata_server: "P2PHANDSHAKE" # peer-to-peer TE handshake; no etcd/http server needed +master_server_address: "127.0.0.1:52061" + +protocol: "rdma" # tcp | rdma (rdma => GPUDirect-capable) +device_name: "mlx5_0" # or "auto-discovery" + +# Pool capacity this client contributes to the global KV store (host DRAM). +global_segment_size: 34359738368 # 32 GiB +local_buffer_size: 8589934592 # 8 GiB + +# Proxy-side tuning (store_proxy.py): +# stripe_mb: 64 # split KV blobs into <=64 MiB chunks for parallel batch transfer +# staging_mb: 4096 # size of each pre-registered (pinned) RDMA staging buffer +# staging_count: 3 # number of staging buffers == concurrency of zero-copy transfers +# +# Replication (ReplicateConfig, per Put): +# replica_num: 1 # raise for hot shared prefixes (read fan-out) +# with_soft_pin: true # keep hot prefixes resident under memory pressure diff --git a/mooncake-integration/ollama/deploy/prometheus.yml b/mooncake-integration/ollama/deploy/prometheus.yml new file mode 100644 index 00000000..f2c549f3 --- /dev/null +++ b/mooncake-integration/ollama/deploy/prometheus.yml @@ -0,0 +1,17 @@ +# Prometheus scrape config for the Ollama x Mooncake KVCache Bus. +# Run: prometheus --config.file=deploy/prometheus.yml --storage.tsdb.path=run/prometheus +global: + scrape_interval: 2s + evaluation_interval: 5s + +scrape_configs: + - job_name: ollama-mooncake-bridge + metrics_path: /metrics + static_configs: + - targets: ["127.0.0.1:52052"] # sidecar HTTP gateway (OMB_BRIDGE_HTTP_PORT) + labels: { component: sidecar } + + - job_name: mooncake-master + static_configs: + - targets: ["127.0.0.1:52063"] # mooncake_master -metrics_port (OMB_MASTER_PORT+2) + labels: { component: master } diff --git a/mooncake-integration/ollama/docs/REPORT.md b/mooncake-integration/ollama/docs/REPORT.md new file mode 100644 index 00000000..5cca7863 --- /dev/null +++ b/mooncake-integration/ollama/docs/REPORT.md @@ -0,0 +1,262 @@ +# Local agent swarm with a global KV cache: bringing Mooncake to Ollama + +> Integrating a new inference framework (Ollama) into the Mooncake PD / KVCache +> ecosystem. +> +> **One sentence:** multiple coding agents on Ollama / llama.cpp now share one +> long-prefix KV cache through the Mooncake Store — the first agent prefills a +> long repo context once, writes its KV to Mooncake, and every other agent (in a +> different process, on a different GPU, or on a different node) restores it +> instead of re-prefilling. + +Everything described here was built and measured on real hardware: **8× NVIDIA +H200**, **Mellanox RDMA NICs (mlx5)**, the real **`mooncake-transfer-engine`** +distributed store over RDMA, a CUDA build of **llama.cpp**, and +**Qwen2.5-Coder 1.5B / 7B** GGUF models. + +--- + +## 1. The gap we fill + +Ollama is the de-facto entry point for local models, multi-agent tooling and +coding assistants — yet it has **zero KV-cache sharing**. A `grep` of the Ollama +tree for `slot-save-path` / `cache-reuse` / `state_seq` is empty: prefix reuse is +limited to a single process's RAM and dies with the request. The community asked +for exactly this in *Ollama #14872 "Swarm Memory — zero-copy KV sharing across +models"* (closed, i.e. not built upstream). + +Mooncake already pools and shares KV for vLLM / SGLang / TRT-LLM in the data +centre. We extend it **down to the workstation/edge multi-agent scenario** and +**out to Ollama**, with a sidecar design that needs almost no changes to Ollama +itself. + +## 2. Architecture + +![Architecture](figures/architecture.png) + +``` + agents / patched Ollama ──HTTP/gRPC──► ollama-mooncake-bridge (Go sidecar) + │ cache-key + radix index + │ restore-vs-recompute arbiter + │ 3-stage orchestration + ┌───────────────┴───────────────┐ + /slots HTTP (Stage 1) gRPC (file paths) + or cgo _ext (Stage 2) │ + ▼ ▼ + llama.cpp server mooncake-store-proxy (Python) + (KV in GPU, per slot) owns 1 warm MooncakeDistributedStore + striped batch_put_from/get_into, + pre-registered RDMA staging pool + │ + Mooncake Store + Transfer Engine + (DRAM pool, RDMA / GPUDirect, replicas) +``` + +Three processes, each independently deployable: + +* **`ollama-mooncake-bridge`** (Go) — the brain. Pure Go, no heavy deps. Serves a + gRPC `KVCacheBus` (Lookup/Prepare/Commit) **and** an HTTP/JSON gateway with a + Prometheus `/metrics` endpoint. +* **`mooncake-store-proxy`** (Python) — owns the single warm + `mooncake.store.MooncakeDistributedStore` handle (the official client) and + exposes it over gRPC. KV blobs are passed as **file paths**, so a multi-GiB + blob is copied at most once and never streams through Go. +* **llama.cpp server** — unmodified for Stage 1; driven via its `/slots` + save/restore endpoints. + +## 3. Technical contributions + +1. **Safe, content-addressed cache keys with chained block hashing.** Every + attribute that changes KV *bytes* — model digest (from a real GGUF metadata + parser written in Go), tokenizer hash, RoPE hash, KV dtype, KV layout, ctx + length, block size — is folded into the key. Within a key space, prompt + tokens are hashed per fixed-size block with a **forward chain** + `h_i = H(h_{i-1} ‖ block_i)`, so two prompts agree on `h_i` iff they share + every token of blocks `0..i`. We would rather miss than mis-hit — a wrong + reuse silently poisons generation. + +2. **A radix tree of KV prefixes shared across processes/GPUs/nodes.** The + cross-process analogue of SGLang's RadixAttention, but the nodes reference KV + snapshots in the Mooncake Store rather than GPU memory. It gives O(blocks) + longest-prefix matching, per-prefix hotness (fan-out → replication hints) and + LRU eviction. Authoritative existence is still a single batched store + `batch_is_exist` over all block boundaries (multi-node correct). + +3. **A restore-vs-recompute cost arbiter.** A KV cache only helps if the store + delivers KV bytes *faster than the GPU regenerates them*. The break-even + bandwidth is `B* = (KV bytes/token) × (prefill tokens/s)`. The arbiter learns, + **per model**, the effective end-to-end restore bandwidth and the prefill rate + online (EWMA, seed-on-first-observation) and only restores when it is + genuinely cheaper. This makes the system **adaptive and loss-free**: it wins + where reuse helps and falls back to recompute where it does not — see §5.3. + +4. **Striped, pre-registered zero-copy RDMA transfer.** Single-object RDMA + collapses for very large objects (≈2 GB) in the store; we split KV into + ≤64 MiB chunks and move them with `batch_put_from`/`batch_get_into` over a + **pool of pre-registered (pinned) staging buffers**, sustaining full bandwidth + and avoiding per-op RDMA registration. Slot files live on `/dev/shm` (tmpfs) + so the host side never touches disk. + +5. **Stage-2 on-device path.** A cgo binding (`llama_state_seq_get/set_data_ext` + with `LLAMA_STATE_SEQ_FLAGS_ON_DEVICE`) keeps KV in device buffers for + GPUDirect RDMA, eliminating the host double-copy of llama.cpp issue #8915. + +## 4. What is real vs. designed + +| Component | Status | +|-----------|--------| +| Mooncake Store + Transfer Engine (RDMA) | **real** — `mooncake_master`, P2P handshake, mlx5 RDMA | +| llama.cpp servers on H200 | **real** — CUDA build, `/slots` save/restore, q8_0 KV | +| GGUF metadata parser, chained hashing, radix index, arbiter | **real** Go, unit-tested | +| Cross-process / cross-GPU KV reuse end-to-end | **real**, measured | +| Stage-2 `_ext`/`ON_DEVICE` export & round-trip | **real** (microbench, libllama) | +| Mooncake-TE GPUDirect of the on-device handle | **designed** (needs Mooncake C++ headers, not in the wheel) | +| Ollama patches | **real diffs**, additive + env-gated | + +## 5. Results + +### 5.1 Store transport (microbench, RDMA mlx5_0) + +Striped vs single-object, same total size, over one mlx5 NIC: + +| object size | single put / get | striped (64 MiB) put / get | +|---|---|---| +| 16 MiB | 26.5 / 5.0 GB/s | 22.8 / 28.7 GB/s | +| 128 MiB | 34.3 / 38.7 GB/s | **43.4 / 44.6 GB/s** | +| 1 GiB | 34.1 / 16.7 GB/s | 41.7 / 14.5 GB/s | +| 2 GiB | 17.5 / 12.3 GB/s | 20.6 / 10.5 GB/s | + +Striping sustains peak bandwidth where a single large object degrades, and +avoids per-op RDMA registration via the pre-registered staging pool. TCP loopback +peaks far lower and is *below* break-even, which is exactly where the arbiter +declines restore. + +### 5.2 Multi-agent swarm + +7B model, q8_0 KV, agents sharing one long repo context, real Mooncake RDMA, +**cross-process and cross-GPU** (round-robin over 2 H200s). Baseline = each agent +prefills cold (llama's own per-slot cache erased between runs for a fair +comparison). Honest TTFT = prepare(lookup + store-fetch + GPU-load) + prefill. + +A single reuser, uncontended (the `smoke_e2e` two-agent cross-GPU test): + +| stage | agent A (cold) | agent B (restore) | +|---|---|---| +| prepare (store fetch + GPU load) | 0.1 s | 1.82 s | +| prefill | 5.88 s (30 045 tok) | 0.01 s (1 tok) | +| **honest TTFT** | **5.99 s** | **1.83 s (−69 %)** | + +The swarm, mean over all agents (benchmark matrix, concurrency 2): + +| workload | mean TTFT (no share → Mooncake) | redundant prefill eliminated | swarm throughput | +|---|---|---|---| +| 7B, 30 k ctx, 6 agents | 5868 → 3898 ms (**−34 %**) | 100 % → 17 % of tokens (agent-0 only) | 1.01× | +| 7B, 16 k ctx, 8 agents | 3184 → 2120 ms (**−33 %**) | 100 % → 13 % | **1.25×** | +| 1.5B, 8 k ctx, 5 agents | 883 → 918 ms (≈ par) | (arbiter near break-even) | 1.00× | + +The per-reuser win is large (−69 %); the swarm *mean* is lower because the single +store client and single NIC serialize concurrent restores, so at high concurrency +the restore path contends with itself. The mechanism-invariant result — **only the +pioneer prefills, every other agent reuses (83 % of all prefill work eliminated)** +— holds in every run regardless of fabric contention. Scaling the store across +multiple clients / NICs (a deployment concern, not an algorithmic one) lifts the +swarm mean toward the single-reuser number; the Stage-2 on-device path (§5.4) +removes the file/​GPU-load term entirely. + +![Only the pioneer prefills](figures/per_agent.png) + +Per agent, the picture is stark: agent 0 prefills the full 28.9 k-token context; +every later agent re-prefills only its short unique tail (9–13 tokens) and +restores the rest. + +### 5.2.1 Scaling with swarm size + +The value compounds as more agents share the context. Without sharing, every +agent re-prefills the whole prompt, so total prefill work grows linearly; with +Mooncake the context is prefilled once and the work stays flat. Measured on the +7B / 30 k stack, fresh namespace per point so each is a clean cold start: + +| agents | prefill tokens (no share) | prefill tokens (Mooncake) | swarm throughput | +|---|---|---|---| +| 1 | 28 942 | 28 942 | 0.36× (pays the store write, nothing to reuse yet) | +| 2 | 57 884 | 28 953 | 0.93× | +| 4 | 115 768 | 28 975 | 1.00× | +| 6 | 173 653 | 28 998 | 1.36× | +| 8 | 231 535 | **29 018** | **1.51×** | + +![Redundant prefill work vs swarm size](figures/scaling_agents.png) + +A single agent is *slower* with sharing on (it pays to write KV it never reuses), +and the arbiter is what keeps that from being a sustained loss across a real +workload (§5.3). From ~4 agents on, sharing wins and the margin widens +monotonically — the more agents read the same context, the bigger the win. + +### 5.3 The adaptive arbiter (loss-free) + +On the tiny **1.5B** model the H200 prefills 7680 tokens in ~330 ms, so a +file-based restore (~700 ms) is *not* worth it. Running 6 agents sequentially so +the online learner can act, the arbiter's decisions were: + +``` +agent0: miss (cold prefill + store) ttft 294 ms +agent1: recompute (restore 697 ms > prefill 585 ms) ttft 304 ms +agent2: recompute (restore-path too slow to beat prefill) ttft 290 ms +agent3: recompute ttft 291 ms +agent4: recompute ttft 294 ms +agent5: recompute ttft 311 ms +``` + +![Loss-free arbiter](figures/arbiter.png) + +After **one** observation the arbiter measured that restoring (~697 ms) does not +beat prefilling (~330–585 ms) for this model and **fell back to recompute** — +preventing any sustained loss. Without the arbiter, a naive "always restore on +hit" policy regresses TTFT (≈357 → 625 ms on the same model). On the 7B model the +same logic keeps choosing restore. This is what makes KV sharing safe to enable +unconditionally. + +### 5.4 Stage-2 KV-state export (libllama microbench) + +![Stage-1 vs Stage-2 KV export](figures/stage2_path.png) + +| model / ctx | KV size | (A) `/slots` file save | (B) in-proc `get_data_ext` | (A)/(B) | round-trip | +|---|---|---|---|---|---| +| 7B / 16 k | 875 MiB | 417 ms (2.2 GB/s) | 91 ms (10.1 GB/s) | **4.6×** | PASS | +| 1.5B / 8 k | 219 MiB | 105 ms (2.2 GB/s) | 19 ms (11.9 GB/s) | **5.5×** | PASS | + +The Stage-1 file path is 4–5× slower than the raw in-process state copy (the +serialization + I/O tax of issue #8915). `ON_DEVICE` reports a tiny 0.1–0.2 MiB +host handle — the bulk KV stays on the GPU, ready for Mooncake-TE GPUDirect RDMA +with zero host copy. KV round-trips were verified functionally (seq0 vs seq1 +next-token argmax match). + +## 6. Limitations & honest framing + +* On a very fast GPU + tiny model the file-based Stage-1 path does **not** reduce + TTFT — the arbiter detects this and declines (no loss). The real TTFT wins come + from (a) larger models / longer contexts, (b) q8_0 KV (halved blob), and (c) + Stage-2 on-device transfer. +* The store-fetch path in Stage 1 is bounded by llama.cpp's file restore + (`/slots`), which materializes KV from a file rather than GPUDirect; the + effective end-to-end restore bandwidth is therefore well below the raw RDMA + bandwidth of §5.1. Stage 2 targets exactly this gap. +* Under a synchronized burst, all first-batch agents decide before any + observation lands; the arbiter adapts across a sustained workload, not within + the very first concurrent wave. +* Heterogeneous cross-*model* KV sharing is intentionally out of scope (different + models ⇒ different key space ⇒ never shared). Same-model multi-agent is the + target and is fully working. + +## 7. Reproduce + +```bash +bash scripts/setup_go.sh && bash scripts/setup_py.sh # local Go + venv + mooncake +bash scripts/setup_llama.sh && bash scripts/setup_models.sh --with-7b +bash scripts/demo.sh 7b 6 # bring up the stack + run the swarm demo +bash ollama-mooncake-bridge/cbridge/build_kvbench.sh # Stage-2 microbench +bash scripts/stack_down.sh # tear down (workspace-scoped) +``` + +Deliverables: `ollama-mooncake-bridge/` (sidecar + cgo), `mooncake-store-proxy/`, +`ollama-patches/`, `bench/`, `deploy/` (compose + Prometheus + Grafana), this +report and `figures/`. diff --git a/mooncake-integration/ollama/docs/demo_result.json b/mooncake-integration/ollama/docs/demo_result.json new file mode 100644 index 00000000..85e40646 --- /dev/null +++ b/mooncake-integration/ollama/docs/demo_result.json @@ -0,0 +1,138 @@ +{ + "config": { + "bridge": "http://127.0.0.1:52052", + "llamas": "http://127.0.0.1:52072,http://127.0.0.1:52073", + "model_path": "/data2/wangzhe/gitlink/mooncake1/.omb-state/models/qwen2.5-coder-7b-instruct-q4_k_m.gguf", + "agents": 6, + "ctx_tokens": 30000, + "share_mode": "shared_prefix", + "n_predict": 8, + "block_size": 256, + "kv_type": "q8_0", + "namespace": "canon2", + "concurrency": 2, + "slots_per_server": 4, + "replica_num": 1, + "out": "/data2/wangzhe/gitlink/mooncake1/.omb-state/run/demo_result.json" + }, + "baseline": { + "label": "cache_off", + "agents": 6, + "wall_s": 23.111, + "ttft_ms_mean": 5684.5, + "ttft_ms_median": 5685.4, + "ttft_ms_p90": 5687.4, + "prompt_tokens_recomputed": 173653, + "prompt_tokens_total": 173653, + "recompute_ratio": 1.0, + "hit_agents": 0, + "throughput_agents_per_s": 0.26 + }, + "cached": { + "label": "cache_on", + "agents": 6, + "wall_s": 20.186, + "ttft_ms_mean": 3966.9, + "ttft_ms_median": 3653.9, + "ttft_ms_p90": 3934.2, + "prompt_tokens_recomputed": 28998, + "prompt_tokens_total": 173653, + "recompute_ratio": 0.167, + "hit_agents": 5, + "throughput_agents_per_s": 0.297 + }, + "ttft_reduction_pct": 30.2, + "throughput_speedup_x": 1.14, + "per_agent_cached": [ + { + "idx": 0, + "llama": "http://127.0.0.1:52072", + "tokens": 28942, + "hit": false, + "decision": "miss", + "restored": false, + "store_get_ms": 0, + "bytes": 0, + "prepare_ms": 68.9809100003913, + "prompt_n": 28942, + "prompt_ms": 5587.763, + "comp_wall_ms": 5798.791166977026, + "ttft_ms": 5656.743910000391 + }, + { + "idx": 1, + "llama": "http://127.0.0.1:52073", + "tokens": 28942, + "hit": true, + "decision": "restore", + "restored": true, + "store_get_ms": 1988.8248469796963, + "bytes": 882366232, + "prepare_ms": 3607.2255889885128, + "prompt_n": 11, + "prompt_ms": 16.065, + "comp_wall_ms": 382.30138598009944, + "ttft_ms": 3623.290588988513 + }, + { + "idx": 2, + "llama": "http://127.0.0.1:52072", + "tokens": 28944, + "hit": true, + "decision": "restore", + "restored": true, + "store_get_ms": 1912.454207020346, + "bytes": 882366232, + "prepare_ms": 3500.9051900124177, + "prompt_n": 13, + "prompt_ms": 16.111, + "comp_wall_ms": 306.38355697738007, + "ttft_ms": 3517.0161900124176 + }, + { + "idx": 3, + "llama": "http://127.0.0.1:52073", + "tokens": 28940, + "hit": true, + "decision": "restore", + "restored": true, + "store_get_ms": 1712.3380179982632, + "bytes": 882366232, + "prepare_ms": 3297.939590003807, + "prompt_n": 9, + "prompt_ms": 87.499, + "comp_wall_ms": 220.71896900888532, + "ttft_ms": 3385.438590003807 + }, + { + "idx": 4, + "llama": "http://127.0.0.1:52072", + "tokens": 28942, + "hit": true, + "decision": "restore", + "restored": true, + "store_get_ms": 1611.3499579951167, + "bytes": 882366232, + "prepare_ms": 3589.8023130139336, + "prompt_n": 11, + "prompt_ms": 94.8, + "comp_wall_ms": 3008.9015809935518, + "ttft_ms": 3684.602313013934 + }, + { + "idx": 5, + "llama": "http://127.0.0.1:52073", + "tokens": 28943, + "hit": true, + "decision": "restore", + "restored": true, + "store_get_ms": 2498.859048995655, + "bytes": 882366232, + "prepare_ms": 3919.863100978546, + "prompt_n": 12, + "prompt_ms": 14.367, + "comp_wall_ms": 287.0978229911998, + "ttft_ms": 3934.230100978546 + } + ] +} \ No newline at end of file diff --git a/mooncake-integration/ollama/docs/figures/arbiter.png b/mooncake-integration/ollama/docs/figures/arbiter.png new file mode 100644 index 0000000000000000000000000000000000000000..5b49a6cd74521e2e38e9a951522e011ce8baf0ee GIT binary patch literal 56894 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 orch [label="HTTP / gRPC"]; + orch -> key [style=dashed, arrowhead=none]; + orch -> radix [style=dashed, arrowhead=none]; + orch -> arbiter [style=dashed, arrowhead=none]; + orch -> llama [label="restore / save KV slot"]; + orch -> proxy [label="gRPC (file paths)"]; + proxy -> store [label="put / get KV blobs", color="#b0413e", penwidth=2]; + llama -> store [label="GPUDirect RDMA (Stage 2)", style=dashed, color="#1a7d32"]; +} diff --git a/mooncake-integration/ollama/docs/figures/architecture.png b/mooncake-integration/ollama/docs/figures/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..e7b7bf480450d386d8a01bd2a21252deeeedf254 GIT binary patch literal 94121 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 cEMvdMJoggd#UQ2xohla65J5g+ny&|_1sz7yS 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 MtS82N`~8pq0X>;Dpa1{> literal 0 HcmV?d00001 diff --git a/mooncake-integration/ollama/docs/figures/recompute.png b/mooncake-integration/ollama/docs/figures/recompute.png new file mode 100644 index 0000000000000000000000000000000000000000..021947f168d0fa22b27ce93d0aac0bed0b7a8d0e GIT binary patch literal 45210 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

JgrfUjW-B!5T~&4Z~_HO2Tu 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 literal 0 HcmV?d00001 diff --git a/mooncake-integration/ollama/docs/figures/scaling_agents.png b/mooncake-integration/ollama/docs/figures/scaling_agents.png new file mode 100644 index 0000000000000000000000000000000000000000..0126067eb1fd5f36054f4dfaf2945e8959dd99e7 GIT binary patch literal 78721 zcmdqJWn5MJ_C34}KuV;MmXvM@0TrdBq`M^*K^iGR8l)s71d$F&=?zFXBA}EaBHfMj zf3Ewx=idABdHtNvN6$IL-g~X@j4{U;bA>)qmc_@R#6h7@`0{cO)KI9)lql52fGe2r zm3$LjE%=w1v$T%0y1k{do2jD(O3BpO;iB&dv@_ zVmv%{|M>}Sdq*oC7MWpPcol31IbA0diZlfIbD?eY?t9b)6iWWVJq`EN)t@dpq~qsU zn+MtT3il{gnO3zPjq23vuMQa2o4b6hG<>4Xr>%S2;hQFjKAwigUDcMm8hWbRCuh@c z8;Pd#M(f$!HR+=}n>Pil22$lMC7q<&D9q^tu1Q_Ff&9gGfxU+t^{@X#{%WKnvHs6D zqDT|LLu|cprCN3wgjW@a2Pac${I8n%i6t>6Gi_B?1-f`B^w3ZLN#JyV#X@02w0R;LoEA~o48 zlC;(KzdH8UCcoA?S=H3n|M{9J8#wPd-*;W(qaLYwXQcGqyC^er^R2xV?X2wV8~pr4 z#|Im-a0p*ZP4A2Qo}uxoKCQ+FQ_4a}_`Q4gZuS7qmea8AlVa~9qdc~HQ~=udoYt_; zx%oAf@S_qV2@CSzYuo3NUVrJw6f_uWOpr*?K2!_?yL*Ir@b#};;&Iymsc9SQpU`R)iDdsg^ydTw!XWcM3Zr;B#_ z)z8{@mxfhq9W9s>idI$M`^AYF8yhdKtT5ibO=UlYzGq@W-{^DNBmX8oJ`e+!>`_0a zR8LG^-pwgNLqo$K4W8r^6BB3;2&pl&hm$VJ>uXYRE$!`r>rNP}KWd57CB0`9clP#3 z74;ilxYS-8dj9;mi0dka{fjSY5_8;%e-DXKLqkIjr$;X67kgoe&$edTDfr9+MkE6t zhG%5l`0?|nsl7dcgoFeWD=VX*APM~1pKK*%IYh3x7NnOh7kSVtnUElP?`aB6HL8^XI2Wd#!NI%$-b(jF(nMOF9cQ z3*!vR%ccD=)t1S4jQaAccGrL29?4h7+W6J9kcEqf_dr?ss)nX!Z%shJwHp|h{81*B zmMj#HAuhLneX{5!8`$_(pn3mioeReBE$cxPKBeGIL6r(SzOKjzr~o+QBy zjHJ0egOmbi`(KUHj~_D`7#PeB)QbdNr3tg>O~}ytsP{NaRLDM7%F73}60MXg=}D z#{KlDQfhMc&!5&O#8sXL&ze7N?|gqZ;G7^xV&!$)PG_>jLNTFES zr`Vv@->Bh5hT~ih^3BRhA*e_LwKsZJI$l$a)jVem!n^K|iBI{ty&c2S(h?~}jg688 z8u|X;KkA$RZV61?Pmy}O=S)pS6`rr2+tS~UUsqQ*I65l3Hc?sTb?7)nr!(z)?6EkQ zf%dp}@1A2+}(8NT-;Cgzl=6HUOyVgO=FDUqZV;M!3!YK z^gbsClaZ0JSsaq~Wju%W2k+zk(#p!f;$rSkm6fJXo`es)yNwhk8ch9sdHH&^YjgZ z=X!R&+&O;?DC99ijGXN_76C1%OTCkm(|DHro5$-- zzgvSV!pV8Ib0){fvFGy(3iKBFx_zHQOf)+>IbrPR)i`8K@53RbEG;iT?5_q`!pX;1 zHh5|ZoQ7qqb2gThB*8J>aY(Qt7w*VV3G4iS;JZ|31((1OpNJwu~& zRMH0&s-c`2)g5g7s<=uc)>TOdRWy4_nBt9($|m%+J{~r=) zb_H$dcKiEl`hoj9(4^cC-Cw+TsC0ht%Xep}+GaTDjU^n3qLPx*x>LDq6fI7?Xp^x0 z)RlEy-8&f>8L@K}6%|R4zgCS9*1ZD*^F<5rs|#}i5VJ-eP;xsJ$Z2T$<^RmiW@ze>w#^D8fTROuiLrCngs| zNX9zV<(-_KCPhX@{y5HuFQCP_jT`$ESa%OpWB%OWpzENx?Kpd-qO|m;vj-O!SC#Fz z0<$$n>Hai{6zxc-g#l8IFPeo~TicLy{JmXW*Az>-c5(+#W%Tqcy8HVrT$V?E$Cp8( zpg&Dk+b?7f4-cD*ii)B?LGAFLJUcmbQWmrMVs_X7y?=aSVtYvF$@hrttSl0bNjOf9 zztwt+tmivXAHHxMMjZy>Q@QUHXnpKy^g3)|n*?m1VrAzHcFOv*x`Q2XEwj`0!y6KMOvvzwpG!h{pWts=?wmbU1$ki5@&b9bMh- z$~DjZRYHSXVS+jtZ?U}bWDtuu~mOVdAG6I0&4+=eOz zP>wUtrr~Nc`R1I!&eiqS%20N=)MU|z4=oTX8jH8OkMeK&1;I&dZ6B>w{}QpOdiIlD zrz(o>j^og`665s5d8qrTf`CUYZ{50ulODXY@P(YSY4CZBOIk0LwVa%s3EYs`?0m!e zTlV#H%p_g?=lp5L3j^;wY~WlulYIA^=9EdM4sxu?EUOjLBMr^x#K1r$J@^yz)vcRX5JT57Kn12HK3`ulOYdM7HMBxPor zXDkH~&>7ACp}c;bO&fLVT*c#xV3ARS-fyqA=9Kb50KWru(hRC)7o~J{b^QG&HweBZPl- zbjZwO4gL6GVEOcEpKiy*L|xZ$F2e^&7%4nYkAz&;e{`pC3*O>KiF@wjH8wV)p9j2- zh?xHK=aRa*`U4pm$AU)}J_B4R1%LoGG*Q5N3hm*q`L}&7UqeIV8U;lNWFkE$r(*L* z4#OGS7zEfs_u8EAr)r+R3iob7^G~Did85gmg^-XCfN)G`sP6l#*Ps2UxiQN75TmxP zu4Q6^5++7oKE8VZ9B2OgdA=|}S581NYSxW2cDDdwrQEWMe)HxP zw6yJluK)<>3bj7^Cna4+nQTrsLk!$?cNgI{tZS3k2bAu-@?Gh-U0h-faXU2shXvQt z?)avSw*48*xW5SPrp|Ru&pK_!c6P>+9}^QZBr;OQ+B%P^ALd(k%@{wlDueC!B5eAS zaiufc&Dz>p3Nh!Kwxh*dHt7=|^@Wi!Xl=4u9Ci?DXFzC(pnrk8Yxwamu@yd7WC!`O5X{*Hdkv^KWi#`2jTK7Z4z%ql>co zlD3t+MiN+bm4KL-`1$7ay{X-k`!g3lH=Un_Ul*|49D=OOfUd~P%ljEZ%Hd$$aNy*Z z?>WtDN}-mQ7cPbAyf~blo!wlgfugULvfJJI>sPwluaV*5*0+4-Bm+;NVa5*7-+`m0 z5`MOsb`D6_!&@)qH3;8&;uD*F+8}RyJi8`QpRBAb*3r?CoPxq1#i)>w z=!#DsUxc~zK285rd9lA5dI{?)N$t5`U!S7$;-H^f|0R#kCXb!@(z3GVL>}YK#jlLw z9@_zLWWFwtp;DXd>s{BRAV)-%a;u+?`xhJ5x3soi%9MFM!&z8R5Qa<+^9^!LL>YiE zQ=8b3G0MOob;Nx4r>bk#E3Z3Q(s2+{# zKiL~Y*|^6ihXSlWp?G;1a{%Dfm6GyjQc_ckHL2_-kd4JJwnF*^g^QGTs25jMBC>sCcg0blNNpWv4px7&ktKQi66?N*4S* z$F|1dvN6@*F&bJEHUlr}F`58azC_GdON)%^j*mFc#)V6mn>{x+5^tAn8)e=Q7EU~< z1PmQ894EU-alr&iR&182+d8%V*;tvm=!%%*Y>0{EOi7a&OyGUrzQrdF{QUV&+s9@h zxw=}oWWE_DMP|*tY{N@{5Q`vn(H?e_RdbrxHAjFoc?&R@^%uKl!Dg}&_BZ;ZTXvMJ zO3f_yA?G=Pr*LRMK0J&ora*yls?`Q2dy|M;nk_9Ytqx@?JOwUl6$*Z;hm?{76e#wj zb13f3=GZTxqASKe17wRnJUcr>m#2z6_a9AVXi80`ZeBTqX<$A(e!tql*f`w~?H-e8 z8aMOr7>8A;RoM9N|M^93Vy z2_W>6l;jkooSg+rya2*+Yo$p|id*!MB=ySOw!%Gr?&M?++0U)z5nfoz4>VGu(<#a9 z(o(m+jGi5H$6FJwul~o{dar{3XErx=14r~*2w!~!$_Pev696KAp~nbn@pZe-F}3Hh zHX&q$&ky6>ci=CK(wa`T!wo$4S4N|%ksCLsx9@ou$O*GB$IQ`@k;lMnO=IA<0dL|? zPzWGKt&n{WK0tM3gXh7+c{+&~al*z-L}id(+uJYB&l;xhNAh37hZ>8vwSlTg`n2&H z5fOv@n>TM-VTzxfAnTx58OeXsBD`Ev1LVvYUzOwB!&@-)-?ABEy5)1?j{NyvZlPf0 zgEUm}EDTB88&ih1x2>i3)__G)|pGy*Yq7_5?fkU&Pt-BKn5s~#F3?|IO_;`3C{ojz9-JPkLDT6)^ zEc)~A?c1(_RMA!l%GUY5q*mm+wFI~rUMa%%1HJbrer-cqbpJ5289j&oE3(AT&wu&c za*m)a1pD32(8W7}|H=(yNErAf??JjCJr9tfud_3_8vXGjS2)rrUWbHCXIFgs6h|q3 zb1dM(%~Nv9j9cgB6Gi0nkS*ww5jbMZuM`|F0IMDU4yMEDZxZZQm!YGh%UAjm00}5z z9IBbG7MSP*v{$O2)E6_Tw%1NP#h=eS$Ul9?CbgJU3<->b)f+~2xW>gOUVARu6K zz8)x{uU}(?e+~^10c7ojgqStbwpsaBNN#U^4+aAYDykis)h=V2+wANQOR-+qXja^I zTjv}LFN>a@Ht9;Et1)R>H_0cK@4w zytH3POCE;Qdj)g}Y28y(gbFyfjHD92YHMq8Tp(sOKyA?Ib=}X;&wmH9ywvyHr}li{ ziXj2BkiW9@-Bx{~Y-n~@5L8QnLT#Vrk!hb*WlIrgR#jE0CC29x_?6EBw;^J_3~oic8iBsl{6snLqwT%G zcjVjrf`V-baOdnoq3)b)UfMX4fX>YHo)5Mc z-6fNO4k#vosG9opVQK*qkZa899x&EW8()E5Ve+Hqx%F7-4d5{N9B1X4CAGpBgoVkW zJzTkRWm-{~pC4#$a;DQiv)Y8~OH!S=d4BIpOL>iaxlp{2HVkuEv)+$QJu0g~b@Bt~ zfjdu5O#F(JLyN>iqP+|xo2OIg>+r6Dz$wCn6WN={(>gR1`(_;`WD5SrS2ohvM(Bcy zGGYVgNwZb(Sy@^8((f4mo(o`NV!AR_4|A_7!y$*1v@`}_r(obzrcdsipAf^u>$t$` zEJ3nor1vkPpbpwI%&9Z|DZ;><3T<5AhJgSuA~E`~`&dYe%))yvVV{?2gPiHckga3f zY_A{M(b16?E;;{Q2C2-{=La4WmKN-*_uexYb69jYH~SrIdqOuyoh05dH#c{8cbCh- zz`*zojJ0FK33iMoqx5%i-LT;$Js&nUHtWf%k0$-OABaL9P~3V-VJ|T=p?27`f5~HV zs8E~7ND}bOUm$+UTvoJ3N{o%?6SGv(B~1YyTmPuxD`^$*sI>n@8$~P8Qfk`z_q!-ru8lPsU`w{H%3 zusA=%{aF(B0{^BNur7aBN}6C?W~{KhtEWeKcCw=~@=-r=SkyrE^>u+D?KcmBy@l8e z1pRNCpY(T{(WV7Om!}P97=-=ifGo7BX3E#naLX^ihy}y}rT5Y9?|vTlJ8CTOy{BWP ze!>#KpKAB!L-9d74~ey(f>JYGQT2#6(Bp8M8HjG9S*y?6oJ>p)mMa-a@ZLx-z99v`$qsYV2_thHZFO(Q%Kwzjqu0+x6k z5!YV~$BQ7(cH?Y?!9kdV-Q zZ#n*PK;}ti1L`yIUVP4rj|O@R0jH$CdnelPgB*|E`pJ__n1?}ok%b9{Nh04E+AQ3f z`J(%VhVRsOE_B^?3oIBTpcc6Xa6Axr%Pg{QhdT>jVRB>ZD59xlb4S< zVy-U=l?`KGgwRNBTG?Cv_UA<6()L9nU_Q`R!!XzYfA_`7zrOx910$nozs1?%0^RQN zH(c3BYBnu(MhLIGf`V;ZA#v}cPkkv)jh+WrUttj>PzFGW;PCnT8>{=KMj&AwZE9+2 zm}W;CQn2k=KgQMwz8-T|cv5L0cH{9=Z|V!J8aD91VNp|(~f(i6Sw*RH(;MPPd-wehrOAi}Y68ehVYEM!2GrlM|*UVcQY8?U(CoYh2iZsX&AY&l-R{>7i5-u-*zV z5b-l?oMrYMJ9uPeX1;<~Qz&*pxo^#2Y^*$czCRhutl)ln6^?jjy7|>#fZC6_Ih;>( zb8^gp_4B{*#lL4Jtr%zrXxNSuqfIaN9uB3yM*Ezfx*5?40f_ZKb&b1hqx2AG>X*VR zbf@$L?;ngPZy+7qd6v@e_;$d_Os}o=%X5tM6+*%AS8~HC07lH`bWa=fy>}JqfWHHQ zKSJ0$>0+A#Xz8GLZWdR5)UP&!I*v1^pK8?z)9@|`(SymIK!4qZ{*L2KKzPs`Zwv}0 zdtdZ4vtP3Ewd`xl;`6||X-pnZhjScjV1;w3% zgN4`U+z+gaW?}Z)`cMs&c{8x!B%IH?32`nqxNm*XC_Dy1etTdJY%x$$f)lHr|LO4O zF$`P&Q7cfg{2Zn<%P(nMm47lI^{FMjC}1e>ta<(?%rGeI10s;|d^)^cXFK@bZ}w#1 z97B|qHTYlHk6okd+3F`JIMs$ETf6@Sg3fWJ#lO`9y>WB zZEMXH3ehqku1QAHT!J;L4Ee)Nloz%!_G#)ej;op($B zImzmkL_U_WvUEX&u>_#tF${B%9=nhY)TA;H1gD%K1C5Mt-!-CRWQYmbQ7^r(A{{|@ zhZZulBU>RcD=ipErO$C}>X#R4>+79-s;jEVR>wS{Eu_+K17N~PF^`~?&|5EeTOPrN z(uDrwINL>tM=M^VbNU!EK1z74`4tvu;_pr9CE3|=U}&CKB7ZUO3EY8!(hHx%IdY>n zmAC9BOc0{V1`z(@uO{CNzxd3m`DuiQENdkKbw znGsu^s;AeH!aAVfD<^jaN+oifA|fJpjg9H@^YcT(!djrLWLNbI0XFW6Ve}B51XZwQ zeEd43L697il!?iEsWpCNM1l|Nl+)hU_8t-kj1@HC(Aorv@yh<7ftJtF5-Ye~qIZJn zxw!D5TS>vd^9U&OfZ*UA+hjRL4PpiWcfhA0jQjiS>_8}7n*|h!LK#)T&_?n!3&|*N zJ-wM<@DqgmU7!!!zC%GXK!Ow`9BCL}2GZ~Fj6KcCDhSdl(7*=G{5OV(7EE3*HqJzYCVgs&B57cI z1nl$0ix=O2_;3**8e(?>d&S`3;9V)Hi~j!p?u;yX`T5P`<4K8$HTP9*E)d!HVcw8h zNCs)%*~LZlAOWTwz=Y&**@2)^!iUNk4F7e&&c{bcb?fP8(COai=3)W+4Tt>s^Jf$& zejr4Uog8b&?JNSt@pmaV-FXGfa{7$P7VVtR^D!A zvz6e^6`wIgvU33CLfHYXg91im>z{Q(P$iJLYDyp^I2fxVlBV#{A=XvuklESCMlVl* zApj>!8)8n&Iu!x?+S4C3F~Tz)P*$&9yN0s2x7X3v2OAF!z<6oM1vnM=FFOMR1CLZy ze~YhJT3Ka2`&qZSzn?IIv$MTDbMg}#2WJL@1Tg_FEMRePa3D?2$rmaP3mK;l{f>R* zBfC7%hPujx8yPLc-m)AP%TsmgOt>#dng5(Q@0{hb2CiC>iHOuDV%8rB!?zU`P8q%V zd3n6xiJ01jVzs^9*VAJLR~I;+pPAX}?Ck753~X*+ue$Y#1H@yqH=AHp*`qYz8seU$ zi;9SJ30-{-%rtJl0&t9AXxh#Ju5@Rcn6lmj^eX>JaqtAxMk;|v(4h17`Pg0<$fZU` zMviL>lHNyG=Giv3IB#-rgv034C^s59=%eK_rA1Hvo&G~%KyqZr)YFTGlo$a~ZPuIpPWH)Ii!ONGBl%%^T zh9dy{W&R{kJH0URWe!hs|Yfz6kjXN9|HGL+;uz;uo_yk)Vdun_~80- zL3a=2o&@Pj{s{UM|C1+AjFLezQy2nwssMD10WVTg(y1LOAB4xxf{EM=`U}_gS$+sY z{ZS%wT2=pT*VWG;l8Fps;!(T=hhVc@v0y<#0s2<`T_dLmg+%TQz%6Qt--d=-V4(2O zyqK-3uHFjW!Xh%?2x==dvV1x7sl+%y*yb$(*!;uJK&?Tkj^Sv6*1Fl@JfH%$8z{K! zUz0C}!JrGqF7wc+Q0`vnSG@L9;zi6~gun+^9(%IiX1hwgx9`=IO@cG|O z(`G{2tgj;@ah`&yv5!iZ40UuUN_2Cgp|kBYnyf4@v!BE_h=BMS`ed{?e%xU`Y|0p7 z2<~nm2o&8#*+by1qQ2vks?MmB3|~bi`F2W4NAO7aSGra`9ao$6d>tN+3+Ux?$`Sia z^)H_{_4my?C=inz3}lWu1hnE55}x~UC(o>`I_tkI%+H%mfA;ggui6AQRA@-Eid5iW zNQx*{PXl}RP0i;0@ViEp<^Gu}C=b6-;9&mDcE6#J^b!Gz(bU!!2h?qA8=K~y9vnnD zguYw`OdBYvzd@+--Ng?DC}PuCaAwhM=f8! zmMK2l1vLd1#CbozOIR)6zmp;4sDQwIpm!Yr1yKX5oMG}34WxUi`<5BnV+RusNE$2_ zi0>3M2bd0!J^_p^0ytnO35Kk=!OdN)NVnQ^2}*kN)mos?TUVU!P4pV?%=hEsn}F2F zKu?c?xNe6iN4&ppQ&M1HrU7UU?81HML|exjO-MBZ0p9wnaKT$(dIO}x$+mJ59~|pj zSy|=VsRQ$T!)d(MX<-KLvQDid^U?0oD}*~t<`4N|1DzX5H5i(2f5^-X-+NCUCLalCvms!<`3!spoGSD$ zKd`2)uC2k~e;XlvfQ5iqswof0M=b&^43s58)Zzh8001nwtc(wo?+oyaL)j9vpQ1+0 z+CZgmg1`d|MMYnauvy!csYQ9n4KC+ee;SL6pMm3gnDvX{hMt~Y?2238TRsubeNM1T z5CG6<-bI+#GhPUX%eToAN8y;BAMM&8x&s1I!o$O7H!48Ggw$6B62y6Jf)pm!7Qn_^ zFn%f|OZE)qtFwZsn;J@2GZI0db%L7?7Y@w9(Gk(j9Y7a@Fw1>#gn|O`diob)GBe#@ z9hdH@00%3W>hK_X1E6obw;9eUEid=a$zg$RYGQ961<`E=KdE+1T&P>Y0x8T3{s)ah zEhWo^d@$|)aT&3LpMuEV9=b}w`*Jj~4Oq+HP{*Fox`D#4{L%>OIWO-{196$ZXV5jN zkRJ%feus5t<5z^3bjWwG|6JERadC6K;Qh(GXHWBLCR8gm!U3YOR z!3C!r-3-292yu0@(751#wnCyAhDSzbD@}iWvUxLNYR2IY!@lhHIiNtZW`B%_ocQ@y z%FD~$8C5xr8&gL=EjIZFRWtqHLJ5fia8ub==k&flc<_K8CXc&DolS7*(I+F{ zzA-Q0|96{3uBNWeNBADGR9xUc?|n@wj9HiixBK8#%*iIVTDrK@d?!JdqT=0WmY9MO zO7A!3FBB?Em`sG+L7z@$V#QD%T{AY4l(kf2C|6gfSJQk|7sKcLuCN6yO{G({gi`fO zW)@vqlB;v0>b`LhO-2^hr$QH#6o;Cfm1^yy#-tTet@Wg%crh~bamV{J_5;pi<8$m{ zr^J?m`=dm^c+NEimoNW&^(NX(9@o=jtmh

u6$*F=el`9#~b~4e8R3rF!zjvt{R&N?~x#%IpE0r7d=kj&xj#Iozzt+c` z(l{UPwz`=Bc0j#^@%DZ<^S=-0T5&*%wrCU8w*uC;+)uYSXDuJoA3IStK9Hg35S8aI zQCmnWT^{_{HkxGoOa?QpFEu1C+S7yh0fUH2QoGIkz1jPIK^ur$C(bZH3g~idWWPX5 z?xEV(!7}EonfEL4(P4qby4w<6e<`98sk9Y*$ny&_dOrUKzph~elgQD7_zSHj!d{#Y z3JM%Q?(Fm~g#2tU+^#Q3lPvPwORQCVrbU)EkmNsGU%kVUm-l{o7b9;ZJJ(<(Yb8Eh z=G{NLV`?*v?G5dj*salrd`A5J7DwWX^;+6D(?_vfe>ZP7{w1T+2J24Mi^RMJa_t`Ak20rp_}G0z1M$t#ne7DqP_g0zrF&`?77>? zO`_(*-0{2--J@R53I(^XCo(rF(;G0OTqZL%=5Hh=((Vz(tR(arSZ%*D5l`kN(UlMN zDAdrF-D55^oygP8nCt(g*ji`ht5db2vB>}6e~*}tEsAdXcTIEG$?gTfu-K!u3UkXV z;F-Vovx|ZSKl!uy+AVrxr_eSk?dI|djKpISXTm67+E=MO=@(G!fMJX)ug^<KMrc?jXD2s5sD^?abUa>#Tq5$s>hDHE3J+zq*WBGdCZNoke!oDpsB>wy}`*! zrJLW4c7C24IlY9=H%UtJ=)F$a{A8H>t)Ml-K^?ox3O?x?Izvh-nqi<(ftWMq8^lE3IXT?Ijx3LM-(?4jTH?F|-ps4Zq&{4j3Er0HHVl=#}N=}rl zp3dQhdz1JFRn~uY0!b9~+5`f4-r@4HBtbz46h1rN?C_=IY3SWO9OS)qPj8I4EnNbe zREcX*toCke8=mN|#+lwi@~(%9)12!0>I4H_#OeOr47QV)%9Kh@3J=!Cm~=WQn~LAm z;+^d2VaFVP{jF+)0T zj$5+G>mAQ$9l}`>xbewN9)(p76cV8VT*o_BMs*{sQL|j-^u4!eqT}O25~;N2KQc03 zkIIdB(Ergh!_Hj42z{>b-^IrVY(<{?!8x_eYWL}8^nd6-&Stv7%6Q3EtZuNp0MA>P zKg7E zs4Bk@8){8wIGEJ7*3tAY&cxMlagHMA-P0tVSc}f_z))()!rQd#?TC#+CNr8ew zj@0vb&)rwWnH7$>`Aj3u{{QTCs(wzn@A=&Rkudu9IuAB~$efo_XZ@eE8sR*uDe8&B zEFyODZ~xsYo!y-uG(!KVDcB5%kRKY~-Q${5i|5hE{Lh+bOlzWcjC-w3rBpBa$0tf- zBk2US&>q(Rp6U%pceh>o&u`6e`GVX!uf{9;^|1ZVcLTfRRb)?Uho$vyo&B(x&B`TD z+q`}6zZT~2P6KS8kOaCh#8W}mSh_FX6i~*>6*swa=WCytA zTId52=fx(?)A(yra%nUB713|bvI8Efoqf$ChHb>l2(}02d$7!0RuDvp@%DUPIQsxt zQ2>B^0nER7+9k9wRG2(@Qn>md=DY^BAf{pA0oQCHf8Ce7%C=R%edWp^iuZ-mV8PBRVW>7Ir>T!s7s>tT+gDZBvJj&D z!ot>X-!#YDAC0NpIlbIL0X$mK>IaRRpg{o4ya*-;K$XunjYa^^UIrriZBi0je!y(g zGRRVhI)akJ7o@m;{daD9>ryzIbMplux$Kgf3`s&~2(-`fyUvl2n zlqIjbI>4+^z8wWhm!enrYbmqO*a60eiNnS6@W~R^#4;SCd&nGAvk-fy`Vt;k5y*IQqdiRUG(E^*O z@$lAq&7F1pwx9+XL-GuWJE?J{N^bC`yf0B$NwBL|2V&<604ohaBJ!o$b^Jv$o!OrR+Yiu{QUh@|1Xhypbd{zR}gy^%j6D6DmY{o_RAA3Umz-UB4a*WPAqP6J*Z~ zRtAAe35HP{X4+S8T*2W0xgB`xAW6dW79>5g_>-a?XP$OWO9IS2P+P4X9hksCAT|@^ zYGdRs16K+rk%wHPVWb=;&z?mhi$G;o+sM4gp;dGjrfm}|D=fqYcl|md1OUgy2!8Es z?i6shKw!7RY9frS2&Vy5*A_U^4Qd@RK+AD6nsbA>(s^}^Fd-oUDEOD)wgQW67*L2f zIbVF&7iMQofG*kG-3@>>Y#3j^LcxJ$*7S;qzqCxxZmDkmAYnI#FafLJ!A4@k4ur7A zl^Im)>Ajy@OWPRvOAjcMdDn;NEHBA$#5E1KEuA+^Oh#z9gNNra{LygbbC+!LYAzOZ zwbG{i7JZ&ucKi5bJFq-_z#M2KR4M!nFWk7C{QPbxCjBtsC&@9Hp>#kF)&(oF{Wyr| z2=|Ej685+FApqvr*9Tycf&yh}Z~w&z{pe}RIWT3IFE5zL$jJ2d_J(`Bc)<_W7|fTt z`ua{oveMG>L&t}Qd_Y!OnwhmMfB)DEHazB?9OZXC`r6LKgd;fcxpvXHc_r;Czz6jmGp7GKF~|C^?zZ$LFPOl8tO5lZy9@Qr9Mp7ENF+Q|p;|W1lcB88+1%+P z&{DcMQvT}5NJ~G@f!j>Fq{H7a29h)L@dJJR6p#xAz`x(UHx3{sAvu{{{`_#qDknD= zP9EG>*z-x?5r$or78EZm=?|bk&^m2NERGucS^!ZFYjtlxCl7}gsQ`te9Gs(;Ha6X; zQrMcnN#MABI|Y`ElP;Km4KdM^Zg24`+zdFUJ`k4Rx>VF?w|u^Ibxm_60{spaHEwwb z_BMn{1D`N#q+Nw|7v@W!Vb{XF*XMX&soV(F_v1$@@{UPh(nW8Cd^rUVd@1OXas%Ex zJUj!>!N-%pNlZ@8y7L2ASyp=qP_3;{Z-H&)@uRK)nA&{9k_Cl-BNz zwxMSCg3ix98jr{=$luAHJo;E%JOIjl8@NrGcM!KE5I+>Cswbn|1CCH|%;BK$P)~q> z3l9yA=?jmDcnpdN9tt!bGjM@8#xujsN=i=VM?L-1@ftlX1LQIE!An+pXNexrN?>hF zY~Tp74Rp79f)Hjk4jOV5NW%jPnKjhlcu|y<4F)Vw0sEOoy6r2#tyzFLZ2^RTe4m1f z%0m1{`YnZAi0nded9u>)zWV@5z8*yr%4x1eOa9;bc1zu2*aL_Hj^&H&bWS2gJ%Zyv67(^{z z6e6l*I3b8=6*LY`*rX7Ufc-BbIsHa2QTtk02UXa=555H}eNalcYcYX+f-Or6ln(IX zA`3j`9?dssXlPi7LdT$ii|lNtA4A^)pN(g%2i+Z?#{j@AQ98hyd8}}*{^;WY?aRi- zh7Se)wyeS301{GoLJJ#>&6n1(H{);SaCwXL2C#W&&qKXQI%v7{`--cII(66e3XfI~ zmyLVoutv(IBq#{eSOr+7S0+c>dtS}W&pdhR9FySf_JdCTVB>C<+DU@M#COfhXOEL$OA4{H1+Xf*R#u<%G4NiBL2CE_rR0!I+j4glK#*f2-r1=xIYOuWbWFCLLK zArO2+&Ct$OtDhNm3vRiV7}k>^541?+HY_#oygK0cZ`nxd{(V9sB7Zp23;^{wi9$e; zf{c9^HfWfJL3wq6{n4?B2?lQNt6&Fc0~M%GUkfe<*z#!&D)K~c=r@2xT4ARdd0NbX zBNTjpu&owomqY@vgLUytu)g6WHgFWg3y)Hc^A8)ntm?!z_c)yU_Jy<()BD z9$uo!BTwLK@VJi;unC;{DX^@x^d(4nh$R{{{lFdq6vQ}rDz_a~8z5&s&;6%}QUH%t zK{no!LBm|a<3e1iVo)oK9YlAYOjdERc0DTX151pYqGFsP?74_jm^-+*NW;DG=+)KH zk(Vd*;QgWnj~((X0jM$XtO}NKatv4u!2l$4 zANEec%E2_O|H;}9QiLA78Sp5n-<)sK(jp;UE>63V=@E5$fnu-r>M|m3fwuu@dZttS zkOt5T;b;9|RtDYUa$@2f02>6-BAO3KQJJtqh1?KO9xu}Kh|sSu5_hd&&4WVfYz)V|P#(fk$J} zwbu-UN0jusLLf}tfLM^Mu)FIm?ezWn@YXqyJRA@Ee9njiPY3-a zL~B3W>TPmw>OJbw0ToHAT0Z2j#Y1cB4*x8>0~lqQF1!P&e#aIgq`kBA63VEFTkmrb zNV>p3#Oqc8ACa_!`vN?OL&G~VQ8D}eSMP3T*X6-Uf|L7V^nv3!^uC8m2hbf+Kh{85 zYlc%OHEYK~N&>j$68mn$)0JTL77s9_a&&%Hq=)I~5GZ=cmM!8s1Ian@UzA39Ma9=5 zT>)^fboFJvhtn_tPa*OMsS$7oGO@5cv^@$bxrfMS)11+6_&9`gDG2Dp#A#o-v|Zc@ z9o}dS-{t_BfZ)L$XrI$$w-WWZzqHYJK0^j)c_HWXwZ4k}x9$5rX9j#O3b$4K;j~v7 zmQVF$vX2{K5&s$njJr6>+K1yk2%b6AcnSrIEHM2A`h?u zBNWVLv6eW8DYzt7R6fUR!bs7A(Mbnd@kEs!F7hOg#YJFqq2c4d<+ljyv%E6&ovoy$ z$Ds7b3^@Y`w)q}C$*kc?x<-$0`{Lu+WX0bW71iB72PKg-IqGjaEeg@x!KQ7L3=hBB zGWxNNWrh<=rbF~bYac-f=k~D2-7t90<2&9dkc_lb4=*S)51WCg~K?amNG z%e6n%@fYzPKJdeA$+R=f9`lepvcB2#Dpl^Xo&GV=O6I+!Hi98AiY6w@oDAi4g_l4A z10BKV_1D4c(H81&XqKgK~R5_La!ig)eag`goRxMS*S?^qjkPI0Gb4z%5Cdww2$ zwwOQ@vArsX)W?Ue*#_!MRt`2kK1Q^5f5S}}3BOi8(MHNY5E*`niR56Lq^)Fd9O&BWj3*bu#e7n1b3NY(Us<0xP|fU7TsdI}NUlx={lChU~m~Yi^E) z-f^pk9*sKxbcf1ro~V@bUsY6Ae#Af1W<1P_CReA)F`w|$XYpSD+`?90i>=;!a)!OS z#rVEe@zTJmP1klx{bAUpTF?{{QaPplPT?I7R9!EAFnhRWZWZvyP?s*|Vu&3>f%^4(1pnGTCqW zXk8{x@8?_ZVpbNCQAkHwDvq+HV$U8uf8sT=Uc^a>4G3?JQPT_Yb<+TRt4m*udX@2| z+s0Hu`7pjX1d8(jC%r(aw(jsph=(art*WlZvP^D}{lo*Ay*NBOPN3SFP1VZ}$o-Rs ztnF^t77D-R=&$$m{#@5yut(c(hzp3EaJ_>gqyXs|1+UCV=%Ay~sf(;>( zU=R5I*#`0eoht=-mQLHS`z8`Qy9$a{09?~|ak)@;2XBF$2|JTX@mSooeSN=E#gu$j zz6&cNHU#N%qr+sHwARi}Yi+8z&r^K!)z@c331tX+tLNBHcdcd1 zp48UzA_wyD^wb533ZNPYX+rsSZvZ^y0W^RPkq`2PzJUQf9Ere_j|1dr!}=!RwJpxe zupgxYxo833h(L*iLTCfU+pAlq!8JnRrFPcBakwRt^c3e$duL}DK&Ny_F<@R+j(16! zEu&I>g$ja;0 zs}R_KLB!#7P&ju_YilbNbU;mR?s=;TvQ+8&_heY12dXrg93pP5**%3zsL56f)_6=h zC|VX{g-TcluYrdWsTOX9IxluFV0_Q6fu2YP|47V!tQP=^ydch^9-j%aW8A~9=>L$N zm%M^PNo6G`3L@&%P!$O>t!}Knn?Bc)$X6#zHZ%#?4oEa`J^|$xn)B|ROf%?2BDxb z#33Ng<4<(Me!2PXr+-TDbiLo{pyAKME|#d%bCe1v5@+FPuE1F)KnSm33X$>G!P|XX(ZX9IiH;)dffyg`O&AEq4+S=M4(Cren41~VO$VeSj zc|=Zw4~kc_P%3TN_Vvuz?R`82*fKuQzbncwsj>SheC<%Z8n)A5GD(`QA*;6QH;39SF^#ImJKJ zSb2P& z7L6~C>hiC5o)I!$3CA(xWtUDaPBn5sH4;wJ74&tA2}3BIA)L7fDitlb`!I>My?+2Q z5rPtwX%|u>C?XO{QkABr9dA%7AFt8j!q*bSbo{!78Ml zR)SO^w`{gLX@!E>d3_|GVL9`V%Pe5__O`YSpI)rIbLY;@eIHiBzh|B16dBpha&&h4 zf$I~|5&_j>>r#jZg24ENmZq{Y3o(KG8({_Fw75uVALQhD*dAc(*}OGG?()N;_x6U+ zb&|*8g#I`ocX_u={o4YS8&PF(CdgbOObdHLfuX<$qaL!E7%>f2e8vy}xh=I35fP36 zsb~?zB_@UvMI4%>oXs847v&b6P0tPdaDKpV4=@vzp2X)+z=EWT%)tNvx0s{);>8uK zAP@zru9Tmx5SzMgdBRQfL1hkXN`Ye=_;qd(2N;OdP{I5{ExE=KDP9y);I^kI>J zBpcvD6r#&a{Hp57TY?rsNlit0p^0}IF2)X~8dq6`v`?Hd786Ta+y2R5t>OdI009WC zrNJ0p6Mx0X8l?d?QY9#mH(j1eAVojr!OboEVK&lByq;NjgOx-fh0i5G%VL;lAayK< z52SgHsi^aelX1d>fBzhw!p;sC3touON}xAPlrr81VmT!DQRq3hVX@lBlpyKE1}&!F z1VWT^Di%xtubh>>q1`BD_K!V8%Iz+BdVi0=$-Qi~+k=jB%^Q3sV^W+P*%_B>9M_T(Qm~i3hs`{)g`|_rTl?0ioM&^Af(d#HFmuzQ%j< z`X7gP?lP9c{I!?%XUkK${*Ml5eA$FMAQI_*oQVHMmu2zyRLveu4KlgUB4U53Em3&a z$VmTegfNJ3Lva3keRhY*C-G)+TACLf3AnoL{!}zPYLw>u4|h?{l$&3-?%B044l&F9 zLw}0|IC#|k)o>+3l?dM*86Aa(9f=Vj1(f-|xC5KnXQWS-{>tv%xiOi-{}C07F7^BU9(*80kY` zPW%bbsjKfGoXF0Z!-0yb91jndd5MK2u!5e7&|hZ}@I@wxJCS^IW2I#Lnx@NtD4f6G zZ1i<0vxj(z$t)r^BhTh4)Z;v5sA+ZQOVeQZ;vSHmol=oALwVgl&{?ZFU;2Nzg>NmS zR_F{pn{WJBadl8evkt8Ts}(%z<1>^L>Y?EZMlJH}4)z3`J~I#`(s}@FL)mS$WjfZ; zzk3m>7WgsSrOA3J8()b1%N%KTvA>RZ{WtI5Uya>{9q+JXM=&fuY#w;9;842+DTLxT z0Pg7cUx?Z9KQDhY5Uf}PvtK`|%r0hLnV484rc~T#my@WYkD5(r`t)!B-3~~!wcx)2 zC0sp3YL_nmxnYdBh8q&?ZMt#ef3H>*XzNwAqU>u+@F&g^%~zMZdrPy&CYtQ)8`ZL2 z#t#_JJsO^}U}pyqI?}?^IjCMiqCL`AzC61!Kq5wi>^j8D|Kk$JOv&cz7iwc+$ zvjXpeq{(@Pq1=K6)5r&Q$&#+&`5Rl=r%WLOUHA*2FvkBp@ap6VDwwlpG;qer1AGLB zLNN6tqj?+^VR zt=`jJ{uo)r9W_?UF=Ato&N?FP*L(%@+`>IP`ZZ>LMyhfjHRI34^GdE2)!Otj=)?t! zj_o`UE$RZ%VT#fFP_r$FOKHX3O;#!3Rmf>;7c|rwn^q!33V{-V77PFH-=bZyJJHGl zWDl|sTB|7bSn#+2Dx7B=Pjp1eX#~eBVj$pHQhubR@EHXa`2nIv(SSEJm^)7ry2gpR zGF3`DlB|nCv_ko-+^i=IGWMvhdgh_Mx~5etQhF?h`0U>W2*xb`6_R`1Ry^s1rMxNc z!nlc3-LXrxeAU=yoX?h75n^)T%dY79yz47}FI*Jrt*}<%)P^HSD<6RLvM*GGOsqPP zayZ*4TS#C@QI7--4pu-M8xMMl+=BPEdWAY{QK{Z37=;NB5n6y}3!AR4xq|YZDowQG zK5S2S0Nw~`-31+ejVkXW;JG`%sJ?G-Fa%iqZ&)`%J8l3vqudQ*j17F`RV06IDn$l$l{V$T!hPSF1ws0J0CUX-#F@@w% zK{;MWK9gS|{T?lXAikQp#OWG-5d0jXSo~IvaS2$xA?~P1ZiCxGz<0Oa_(CpE0<0;f z`Zez!&#NB!R(4VEl?I2&L*11|IX1bOU*N^~`i5OBfkJ3V6x=9*@d0HoXb@8w41xA) z69t9xq%&vcxjtTm%uONL$P=jrJCmEI!!_3%eT8s{j?fJ0Z!2MgG<6%mzR*vlV)_Gr z>}l^NuKwkG!Rp15x-XydHrie4?LKABUHvN^cL;`yVefh@PqZOErLI;!SM`AJ>e4Mf zak52=6j#Y=s%9mcTKdDVyBj7_P$rTXr*$0Kns!iiD2GPGm0xfJL5Y$_gxfH*@sZiI zb$3T%2g8rw;`1$J&-x_Qadic9#}ArQ1}a0yDn-ofZl1hN)h9|kRHHDsGDIpt0A9+M zm}AFEVH>^Sw#!;{xO8A&h&?U_{0#Em=O$xFl)l1*6FX2JMTP_TOE~Unt^q7>pjA%D zS9M*yuaysd8z9kbIJ47yA-yUFI!TJ5|42Z<2H${G2=5|M+a_>B2|58G47k@u_2Vdf zL21F1h~wyJI|2cP8RAj2Z%E7p_Y3sVc~#F8EvSIJn#hbOEP+*ZS$(s1YAzc!TD_W$ z*?MP}Y!BDV%F5Lqf1ERvy2en{f&xkI?eKQ||6>fPjj_y?m!E<}I0U6T7#48Fnu5B_ zEOj9_;7<{o&p37HQ=q^l?-sP}?zlHkM~FCv1I@B+LS%$txZp7<`*@pu<}@0FMC z<2konr&YffDDjJj2i#C=4v&)IyPMmW9p|N) zdMGEEVlppi-K%HZV;>{i(<-Sgv1CfDB7d^D(*_Oo;L&;$kJfqt1i7#{QulDU9)!-Npm@Vq< zmzumT!?YNuS~~>2kk>+{=Jh%1`7czRHU5{(khmg1$OkMXkm$`DF$V_+N}owPg_0o) zE4sk>qFG8XXju#cr6?_814F}hNcRZ1A$gkJkx|3mhsi2if^k$nfj$fRVjuYG5El>{ zLw~@jWFR=-8-`afYv)99gCWZ<7+aEl(KZ@Bvh9?6d7(W2xyh7+6Sgj5$GB07TbXdj~s?HnjO`XE43Ul`kS z@?Mc3AO>~XlH1qh01TCd?v|D!#8*WV20pZ<_4R^ycmfBhuMI~3z=!@wpmV?zX8 zL=SK85YmxZ{++>Qy}ube5YGQpp}@cr2AHpY~py(3jZQ)fnD^3$jD4yLs!@CwM8KdFcW|0Fc?<(ZPzo zoQdt|^RobKhrTqe7v6Ihdkv@;6^vkFbO3j#z@|}$7QMvXIVJFXcKMUlV0}Q-3Z{83Bj%1=3fRG2YXC{I=QV4?~pmE@uEO2gVJ%BWrHBAsj*TAv4pcfY5!xv?v3z&rDc| z_B?jhF)*xdfjITKtE-Yx$K%@`0S(-NISu|R^1xIlKZH2^d1#Gi+Z?f0}ZuzWoXmr3C{KqR-7T}~f2qHyw0gkQ3$V#f0 zA&xGsMNdDmr@LF7xr)Zi*KgatuOv?I82~6dw_+)h#X8#6AI;XUUF(d3awoGHuY3y> z^!7{Pk|6&I;C~7lKy1aBSpiOg*v=G02@R&Msw$Fn@3+>AIjX?Ae(Cb%3mB4&n+f5_ zabeydhs(YobbbcdtXGkmt1Mn@t@#5I>$>XdC>DD#ak}Qa?cAxj^c1|McH$)^_(Dcm z-n!WHFVe2*Dv$1r>Q(CO0Y`w3*#3?6BNdpT3gTI<*e8Ifzs+KIIrNh z0o&Vmw&TlsL&LE7TW*C0j1CxTs-YqA2U^!X*eq$zmdfBNRGi6N}M4p(siwX2f?dC1O5#@ zf|#T4*f-pXih@EIRFVDH75kTd>*`XxTL)Nb2Xh*9LR6QhoqmIO{Dy?wZ_lAx^#)~Z z8nN17B~IQRDalDm$E}ruV{7gF<5szlJlM@Ja*xp8x60FQ)2!nSBR~M+wN+q|M_|2y|r&% zNiGNITCK8j>9HfxeS<@thY!wMcp|{`sm0F>wHb*o#XkYtiGv{px=+Q(9@USd!~_{R z20jxg(r(7A8Ts&Yne5CPtvjcGQT(lc>d9U4GBLKyRZgOh`0!tL9MC&h{XK1B2@(SJ z3N1T*0m)^dAc|0P21Kv8hkNi);O_7;BY%EPnv&VbDU^LbV$gf%o7`5T>(IEI2u1B8(0#dQEVU*GS?HfO6 zqpeL^*lb8NqLy4k^<4kz<3W*Zgbal>r_y0m&Be>B?{Ra;Gv#Q(95M8#o&fA^@9BvG zMwmg~m!qIefhjhJu}3Ec^0Cea4g%uQT%3Y(3oKbwX&XQP9+(#BaQEIl)rIpN`2r{Y zEJ28qOCf9=J$p`u_&t{!t3kBQ+xc7yG;Na$u5ND0z4wxGI?{gZAW#n=ILN27Q{aU% z=kgX0nD`-L8-I3oaWVb9It9i-pP!$9-gAY@xd`ba#p${s@~_B$hFUS8g8_b94Tqtn z&M@_x55gu7v-z>VZ~V_N75FJo1!ZGnBmb&QGEQ~t9WD?6sT##lo|^pY1TEk-Q5Qp9 zJL<3hAOBN#^me0pj@_ zkN*b?Fbm0Yf-6pKGE~6J><7Do>{_r*!q1+SRGa{lAsv~-2&_iQ-Y=|1^+slk;xWIN zPgds7V#n!zP!m>&L9`v6CLn9z3AV`l!mtR(fPS9=^zyaJo^u^E({|eP|{4d@JHMU);au18$?_@2X6>y^A&Wyirf7JqIWc zwFr6<)L8TxY;0&yhq*GNqoWh#1Xm|5@PW8dQgxkY9|RVCPbNCRl`?I{`zOeK+mH#J#@mBnA_&<~761|AD$&^`bK_6JLg|(MLCC+c z+XZl@+Q8BN(DMi0T~qO__Rh}bzU|dF(8wZ@KG4IAZ=-`_0K1stva;;@I{-F676?Wy za(!uFk+W(duWlbg!xbL}2Zx3X@JW1Lx#9lMeMc?W#KDmL7GwYmfZ3VF{EP>%l9J)xFJH3ICJwIsJvPS1 zpglYl{&OV@I|eT;TpnfpY2}tQNBJAkUPnG%%*>SIPCqizmr*vT7}}>%5hvz9O`wu5 zVP3Jl#vU(QX}-b;J;O80V&^DaX&Bojm=G6NJnE-di%1F#Yr{i!IRjm*TuWU<;y$Ds z9jGIqe%C|dLf;4wS4ypsfdRv`w6tVDesHY$5xD5G09N3`kkE-j1X>sQ2WeoNA|a#| zj0TVGas0r7%WBW?P)u>pHkx(^VEjwkZ5%Y3_vaNMO@f=eu8RyyP=nj@MTaED;RlIjUs8)d!)?0v_gPPcgKqr^k z8N2S?wne#j^qd_MK=xz=ahOgPBOh4W<&K0asNf^!_sMKpT0TY|$DjXBk1}8eejaj= ze5|bZ&X2Vl8hJmy*;MPneQqYoH37FSRT|qmA2>oI z38^GbhUo5DY;f7H=i`4&1ocd%03MVq5xR}M>%4YC3hmMhh2g~y^!M?o##sQU3pswA zA2cO`tkEsrxcec3f*e*I5EJK3TEWT}%2&?qKJKYRzqWw zt>n6K;i>`2J20tad>aqu6k2kLWAFqNYE;dGMzc^3c0*a(3ocoq$ZK znk^433i~*gsw>~8GGvV9`Ozq&Q+QQeZ7Dj-IJ3wOH^bFga$3fWljGTvCttSQyJguh zxS}ZY-2DNLJ4YwJ{Xl_RH9UMb``00dE*2*2IU5plYT2S%t$@D<<+;?h)@g0RVX@PZ z>d2(qN|m=|wp4B`DD1G0SIIf>Y@Xj^(JnF3`(~Zr#^bD4E?97TN2flQ(1i^_CG}z# zR&Z_%I+`e+GerV<%6!+I`S5JD-L_fg2G2Kww1uPUFRqtXNE|3~`)FK#xNQ2ZuYH`T z2(8*X{g!B`G-a)eL?GY?R6*%E($Dmaj1t#Z^Nxc#5mew%o?UG>e_NXE_zj;28nQSL zDRwIcct#d+Kr}(j05A5Ltwn)quHX0fHq0$i^9*aWxOL}F88#$(uzncQ5Z)PfQedlbcrnVsG!!hpRV*?-2+6JrllZ9>4)RU{_ia*zZ9$^A9!CQ zViZ_;>c>ai&Shm~WlQAyppB4 zKp7Eelgoj45A*}IL?x^(y{^s1$uoH>sXorG^x9ez67r?VJ*t`4UD; z4O&|<-tFSUn+rQy|Deobzryw7{i99aPm~bI4>AW4WUxHSf%Rz(qHd2yr9B(sT(J1z zLFzj?GQu$6GHl=V4uu2n!N>`J4fLDYo?rFZqPxJeT>xmqS{t>xi8Co2jz5Qns z?Lz!x)URC;=NaRW6J~k!Fx4S0)_jY|m9mChD_&?m%W$|Vs9gxlyL~$ZMJ8j91`g2o zhfcN--_c}BJeMwPx0XCC10j)NP&ZYApP-=jm%wDGl<*InOz=~_=-ntiA-OEU*C(XO zcHbDtb!hQrefouywiw}>Qh9ZU;Ot(NxTZ&=0Gm2{?afK$LxW$_Q4>I0;AKQwlsIjh z^=Vo{IDWehEc=Ni4WWdk%W79W;=Lo2fyk_wYmK}+cAo}k7eOYv^(+EFx4gyusqPFaCz6Smoc62meDTWM5Bh@eh{eYTy01?rx zCwr;Q_%1;OQ|E&C8--R=>GS6Th~**J^LWNsity;EqXC!vE~_98>Uwe?qwWAEjU5BX zV&Xyc1!_nWaf8}X|Iwh(Dd+>W1=uLXbB_^|Etqyml-^me^Mt)~{3z>Eu;9=}VF&3A zMGL7}2nDMULksqp&FqXj_pV(P9rjn73N=nyMFp7|oyMb~W{XB*3;HZCJg#6S&P(KQ zu;@%rk)jynraZsl-GtjCs;ovTnStsU4%fIuum;DzeLsZfUDQUBM~5F-O%pc`L(90~ z=LJ}CHmpn0{YrS@Fu*Aug0>5YKVZ9K_=XVbQesB{mIALI(CAT|px&<+*%ve9m5)a{ ziYNr|oBkevAJdmmea!%rujMzSgr0Xe$~y8^qiI*bxggQKiRRhDUG+Nc?x?&Pu?xwAeNP6kBuo{lwMgvF0X9 z)q}G!3KREUzHC)c!8#c>7>pvEZ2DjrDyWLZxo^9E9h%s@C*P}>HdcvTD2yl`sXQlQ zBhwlEl8ygUQAw+f=*hr_lCQ0EB4cU|(DK~B&hg z1`T0*T92Lq?I`qcf@`na6-f~X1CXZ&My_FrqH>!;>=?wF7l34i)o8+$AX?P`4v1v{ zV^Xr1#f8Z8UcAW`2K5dEW#fnY5aMATTME#a;wvO>(L?Ny(R`x86;LA_Lkdpo0Ne)> z%b@b4Szh!tAO}w1bi<)UlpDfV@c}OMEmJf*9Yno2kYX^92kXwKuZ;zpClU$VNANPK z97gs)$pY5Gyn?-XFW(`VO&cuKB4q!bd-D#QMGyRjl~EJ(;3!6yfJ~EVcz`0tVm-l4 z*Mrwf06ij1($HayS~-DV%LHE7jR7b7ws)ki?0CGwG!->4>VP~ffhkj_$Y^WN!j~gY zJy@aBh+bgzUcu`SeJcTc5f(z^Hnh2d!1l%BB~s)#%m>t$bR8_%hK>w%K7j3)m?`ci1h|WEcM$UcEjA45@Zh<;Ot zD9{-D#|J)=UWU*yuF)G=4BP0r$kW=#udmtS<)w*^qtm^Kdw1{7FBP^@0o9Z>u1Tt( zKsYv3N8ctO@bNFlW-Kh50v{oKkdKAh$QESKN``xKj)n6%OFs18$>R-W4$2StZ*OX| ze*E!Ms7VGXA6X|hJIoAN(Zs4zwFon6`WndQ$QOk%TkK*_at^8UGr1Km>R%R3FgWZi>cQmuMDv;`>d8Up zlkbAx&@d9$Ap$YPGh`{`E0dzU&Tj(~l!)X|!WaBTZJOLjZM#tfC?(at4b)Q#};a z6i87Jr(5KhlJNoT9m%HwprFXs+vIo%MH44tHnr?HIXQ{Uc3mnPg9ZrJDMf1xtz%F) ziqW?*z3Q$cn>~b9+72L4$W()gs|-&04p1hmevy+p5f&Q1%Nj6_d~iQ2326`LbNW3c zMVG^B+Jem|QdqjS@3en;fMFLm4$DfE;K%+Yc>rp|nydGRh)#>f?Z9CWxQR>+^nuJr zu`o)px2X7mhOF<@1n^M}B)3o*JVCAlN-CY`fC7oh*A7?&eD03y9QAg843hMO}{k2L##;elyok5_h=Fc@R3U+v(O_zcw;Ub~S%^YpFxS^_lE5jKePA zK7k`bY&idw>{~GAmMp16nnBDsV#MNomxB~U)Iy9BhGFGYq}fS)Yzi>Ij7AMW$~+{= zWuSa`fQ<{1=_GUC)Km|I4xd_twv3wuxWT>o9HSU#xSqdik z1Zfya>P}LGA~eF+H>rLssc-dJ=ABt56OtRXRHd>m^tK&i_HZamhV61+qGqoF^v%T$jG~<4p zuwdne-V52APZV zYpu;U?R77o>cQ!0yna1rUr}-(LpJyfbuxF~ECzpD$96CV=^R9c9;Xf*Jb3D%ZT~H# z{pBsJ|F~zyTO9K9jUP;p1edYDGwkb5-$fgFPydvhnzi_SdS-?>-nD@*xM-X2>=Htl z6LW+j;|aiCNPwoi(yRb-V!Eh4E-ftvGh1015SPlt56D5y%Gje`yc;uT&YXk^MFj;D zOzU+Xe4dOMg!*pix*y9(2X|p|c-BGxPIIggCh?1ALT1MQ_2lZDR{PZO-M23z-9?-q zGHq+juI$(`^}Xt!jBkHtr(Zw%>G}EEONrT6JDqmUUVa9?PC_dBbNM+xSx^7-7~DG& zxen(PUWJ||3)LOa=`eH>v27|+2)V*gZ;9*|FMr$JxY@B)C|dPQ;L`uz$f(>@b$_ip zoZ>ISrihu%PjAkbzEc<&YJ7ILdfbE*AS6&juEjJCkX#7}vlc|fa7Z9SQ;2a=P8z{6 zhlzuc(;{h4>8VV5R@TOB=Rq~a<9Eyz$`(o1c*mACxDEU|c4EG)DChPYp_VH9pPmv> zSuz9GY_cv*)BSx%^<1_t@}75kSbS>f>+P4K$^z>yhEBP3SpyJW{iSIeEYdTy?PDC^ z)zRS$8_}3trMO$O`&uR zPae+myy4@RjSz^L?5NTy%N`jSFy3PU&0*_?LnbVOH% zNL4uTi^Wc+TFVQY_`RQh9jz^jQQ)J|az^qwFQo?$`U2p>tmOy!a3fvp5{PH=0qa1+ zh*B2Pm8B-wL6BD@f#tmv?{v06T6ed`PKhQ9O&z z(@YE8WW+PxSc)t-afV zL?a!-2c-V?AoM4s&Oogvrmw zH<;osL-B%W0weX{$j=(*mK?$yruaMv7+ValpX0~{9Z(mFsoU%1H4#ul2pD7U;6OvB zF1zAoQtvT-kD*o$I(rh*|*1d4dKn@OU84#Ux14Sauo~(X2F)ncI z&40ZN(^%vuCa?f7;6!H8H59@7-XRU5!V5eGJl9O{M#8`nCczDolP9pkS64$96hn71 za0wu?oFGs2gLO_*9q`|lV?Z?uZmX;Av;X-HR#kX;^=b5J?>M+G84aG+ee!(U4ul#X zXXOi3GM&3f0v1yad-lYjdSGE_ZX6bc3>-^drF5V#O#TE&K8eBOyuFSmWcNZr=l{>E zQP3Bjd@tRbG+$W+9TgF6+|_1LBSpwA$V&1>$BMcyW@i^ejH9_7&@djV66btPLCn9K zi;F(i`Kj$=&LE85qK8f^rMNcEzvl3I@*dL}HdzH7Adp~6K)dC6gmb{iK}oC55py|E zbl2YU`R^J$(YQuwj(6m6v$ao9Neu_MwZ%6T*u}W!nwWAgM9D-Wn-e~uxQRt6X%BoZ zO{WwvCj2SbVTMFJ;K^-fp^M}e3Y2y?75uXK90X=U7J>Xw_6NA0#2gxMtpPJ@Hn2~8 z01P9Z_0Kab6nTSE*6IK5$?F8&Q+XgH+=YFNevYy)OZfC}a}mJiT9~G1AoC;ERYyk$ z!$1T<`8NVoD2{U(80U4YR1Wa>C%d%&_tw_Um9iJ(E4)*|P`Z?3uSruj8!Fnho* z{n;JB7$jFii%bw(80Q1&gaLAk%f%c2FJC%wfy(mbYQ;AXFMnKNRBHyVv|eJC&325j zKZtJo1>i;hZRMSt4=!Fget5B3ao;i{m#xD!R`##=iSTV3H>O_+y1wkcQ!PJdb5P0j z&wo~z6kn927bcfu6M!}ty%V&tK_#z9IfPu9AQv2$h9Fqx!RHTRzj^=jo&*&f;AuLs zgvmS7+&{ebYTX@#vuuC!8mkAO$6~^tG$I2z%qzT#m0TW0%-!@6peH7R1_PS%zM!`GCKIDUE|DNt zl>p8}{&Ezl(8dq^|1RZUS;i7BR^&eN^XJ{VI}^O>cS4M~85RB8?$1Ak2GRICJ{6%(7(<`PQud?~~4to+7qz&nze%MEFwgW?*ND zM&db8t`be3tR=wcRIi}Q23_ea$fwfopp64q#N0^ZzhhWIe#vs){;qYi5B{3Uqb@jb zb|V%t$nPYsy*b$2B0oluunm-xMQn|^kt1=8HmMA000J;{EWc=PDT$ei8@fwmiKRzLjZlr;6- zEvKxnh2}bos|0lghmWz9%$HI17#HLWPFi8`)8Yc(+chcka}NJF`@Q8yEQ;mD?y}Vf z3eLqw#B%13wY*;*)wKAAKL=KA)lG3|nO?H*?iK7#|ZLO8G<<>Yb~J3GjB2z=N5 z^t-WYedMcmuNz9YHHKtt3&c+i2g&OakvF%|RuEXwlbIKViN;>05x90}QYUsLc%ip;`YwbCVoD^ z&+@$lh6zmyH8fUEjC(^dYS5c7`3HNoadSi2yZ6*H42mWaq>|E~gCE<>z`!pX*KPT` zJ@Vb=q@a>ty2bTpSlEP;%(Fr`n7C@E$e5VKhkSGgnafO4&c{ArdnB0OzDY#hN=jX; zNS)LAxw^efW^`kBx-8%ynC^o}a`K_bzhB7N$`X<+ou>HQrC7(n%3^j%cv+bN2Kq=z z`}eU#M@QEe1qNPTGx>PA(-KEVBb$phimJ;i?slJtp`vd>G8%-33=H1k3xL05b@{AP zG&-Lh<9w4hzTTK_Ct$f?zDD{$P)W0$sJiz1W4L!KVfZicyt8qW^wwe)*_xWb-37^E0d) z;?lD3UQ@O|&35JDVNE=+;vAFuVkw@<8~JUXc0S_4ia?p&gZq*=`Bpw{AAd6MW^YRf z?xn}JtKjWA88+9))+d0Bo=h-$pH~i zV+Uc3Lqq4}-H6-d>9H8LxhoBNIrM&>y5@$9`mLKvMeYaK!e@Lg&Yi}_A@@?cIyiiM z+`nM(+49X8?aIT;Tl;khfL*Bh`WiRc>8d8L*&s_-i+AV@ZC(BK*)`)4w^h;JqD&AM zWAu5thvc}5M9ev^HEPm~j;HPSQd!|7cD~2++qHeCsw_XS6Ccq}(WE}zQn|my`#E*P z1r3FOLihdpMJOr`b3!WdiLct^3u7tJaU3Wkj6Z08c6y;Zo1V2QXU&$3Yg(I*zIk6X z`{46~KF@sFG^b57*vq5iusiy-ck1Z9;K5JyVXD&Qxh1IOK~86wVe-L$kRMiDJdPhD zaztj#a6})5`bB6Y1tTwMFP~wTm}xn=k_wNt-eZydpmwDoLrLyy*AHcPm(R^S&(!LU zI#=$cE4SgwpsI4|3f_cbWeDhO-4V2W3Pw7P0bbL=_$V?3{^gCjjb5(o4e>w_zB0!?0^YDP%)j%HJH4Yv$z+*+urjs?_WQh8I%#~UgQv$ zvaLH^Q%>%Eg32xoonEyHL7rj2DgJMJf%I*>W%}e!cqY>MN#y2HF+>D*wMOA(s|=@uIe441<8K9Bvywx&%|>UVbE;1N65 zi`C08C$cUS=LXEkmV9#}<|Aji%H5%yx)$Mkf5J16&HEbL z^$g+PntqfHh27V8bMkGpJ=(ePY|VSMUwucLuly)=u9#{3&{wiXoBgXzi_EXS&t~|ZiyUq~l{u=YCbX)PbtH<0giuM_J9z6B>-gNin zrM?5RF!5twI`V?u0)?E!h1&XS0`GYQR*`q_2~A-!G&Fpua*RG0mkuKRMd>Mg-}o#g zh0MQY{YuvrDVOk^I=Qi5M&HsD`ZH!^u8WIesGbKKz>PdjP%+`2lQp?mzfLAvo-cdt1|Tt{g2?X(NUYdVN`t}{Hx=aW+$ z-I1kLBfl6jh@-xP7bl+ExFJluCpXBU@>C65_=)$HEFpnzY*wocEGNVe3BjzvV9SiWZ{W@-F)L+WPir6Pl!D|S1;g{UM2tE3@cqOW2bW0JjiNwj|MoCgD zkK7?ZvQfKVD8mIhX3+inOVHxxHmzeeuKp9sWH2o*X@?l|avZK3^5 zzlVxxb2M-Zxh7b1{=fDG8x=2RAD##Qm1c4ownPbk$xBR|d%BHX&hk;qA zm$*4hUOm_8_v7NjKMFZfwUg2eO`g~TC(x^*$umBmQ+9el+%37um==VEvhiBos@M;0 z9o@Q-@5ax(jw|U+vEB9i&-8`1Lo4(V5+qE2tdM}%sjq}ny6xKi)+djCJU#|U?_++~ zxzYvCwF!YuI&22^1AL1ku|6GO*u*6u5TO(;w)~)qOWo;=9|=*o-71F1Vx!bmxx8xw zQhd*c!e4!QtNzP%FWqvtXU~dVKK)+OA5UwP%a&Zl270R}H^j|3F)^QI{iy4`{G3k-1h& z2ZSQ*?tn)O;o@NM#h*K8`&|R6JVvQ0L30-J`RVAfFnYiD<{7UwYWU2_qJQMv>wDJQ ze;50{+;e(#Wag!1a&j+4L#{8AO|I2IEl5W+=0cL58?%qes{*PG;yJ+50SVD+q*we~ zBqs088j6uv$mk1rwBJ#u$F!6`7*v;5+T z9&CgR*VYLda2Zw!xb!3B3pyAvY>znwkm7(a%1so7MAMuT=N0gf@eZF972E$#1VpWI zoM=lnj=wAsJ(g z)>=Ptmvc`kya}0Hvt=h(f~Et&s6P^-G-4<1EBbRVDfT?9ec;7odCl#}(2LbnGN804 zE*Bv=47nH)V;NOv1+_)r?mTbxrGP#|_^I=Wk(bu8M7|&6EDL76=03T|?1q(hky#Ke z_#*{)v_f08rAx)ZBUr&u-qj1`H{>W`AV7&iO8n%|hkytSVVnsThu^gbEVpjYPl^gM zyB(PwzrNK=sdGIk4a&G2+s-48J(z>X2)_qj^QX?v60BsHb{LHOKD8?21+ZX- z0g@Vy*{AvLU|&Unr~@L_iOKcU$Te!h9oZrlq=j_dU`R}Cp=Bxl+9IhRkj7x|l`Q{J z5Tf>Ygf&=Z*Vm#$KctxA%8MO#1(Q8Rz@3tt*5I{SuILSN_#rLG%R6tc64%Y|O9h`z zvkPx2ZT3JoH@}wtE4`$usyphrEEg+^+5H=yexza0%%yjKI3hU8xW<&v#{y_iqhBZnjQ-xw)bai*r} z^reb*ui@sWv%mXV{Hmup|tR9Bn)XsSP4FB84vxK`!Kk+u+lnmuIbixvpsr3JrQ z50f(`!XrD*k^^TD5}2UNmtDiURK+baHYn}4Swx%VV3L(L_f2|_tP1vKrN3O0)dVqqE4Q>(sufC@9cog2KWKbDEZcCY0a2#&4OJT#BEK!iRH> zpCo$v`wA6uSNIfJL=}W;8MyNS%s87gHU4JLBC&PTcIvj4)#7snoLADg)z({IRG4L>y15XyL#frXh~DzMZ0|VmL7xf#OHaiuG&h z1Hu(bf{Y_PJZ@I3gmDI=V`^#|R+&7`I@gsJPPUpaw`b(LzIh!}ztU%Ny4kZd5`j@@ z#dH&nSUN69XaSQCI~UgSmqisWIRxe^27okc(H5p zx^BMxo)7N(bE_Vg3`PmPX_h8kdx41Um4lh>Pt?RT1%+NX#%Hgu#1fKzUgha2{X3X@wpfg;5)L8c0~?9g3<=NK#Qw?T(+^+MVT4fIF{>}+jRzR%wlv*u&C%-&x=qxEt0 zEe?n%`I5YS*T}P-eBzOj#)`4L&(5171y~$q{kKN#YsjvU{CO*=rifgyuwv-|-EQZd#3^=r(WwW&IQ zAUQdKKd>?NNCQE}%V=>s^7HLZ@&h3Z5SV`n)&eAl)EZ1QRiIWFNJHJ9MzI^s|0K?s zajc5pO)${oP>#O*o-w|#kD*)QmgJ^PbDxGn0f3D<21C=_9P%D|JobIL|F8FlD7A#X z9bIH>Y=+5H+A;8{`t%~VQ|qp8LtEuLkDzIeh2vq9Acn!T^>-?X0;CYfSr}qweWXhe9~%;_HZVBLkn}aIm<^>?1md6r&}(%~&6i+IBG~^{P*ddq z$}01>O)9b+h#9tN$sJiX9QH5OP@9?O9^=HaM0~KkQLrHuPPDB7zqSwevF2{BHb_8I zF8M)1wCA^<50CwrZ~m=h?C$rf$7+qOp6h9>D)030j_i~L|J3SOSrAP>21%aXS0w~<(uEDL! z<$C6dst?V)fEc$>!{#W~Yn;|DC7M%ePf5=uR+JNqgr|N3P>kZq_BfY>fw=*=rXoD- zsSDfH3@Kw~s*XqLeF|S>#U~cNic3sl=5-ys1TSNWg}~)F>;i^G$r=k=i7PlL(_(=_ zAaZED<0qIo@-)wtj-h>@Dhy=3{_HNB$QMvjz5XDhj_cl^^I@+>l7amSnID(kQs4J< z$p?E-!8Rx-iQdb*cdr86A@VzgJnm@_N?Bx6wtM$yE8CB_1vavVb1!h{ZLOkXOvp-i z)J?PZErmez+mNrepV{|V7j!yU;%vR}9uZn5S`MeRqHYrI#UMO>resw5^Oa>eQP1W6 zwwDzu05c<`?qhhMZkHKhG|$tWRvbtzH>UWVKZFgt;Z5uB1?wj4Y`a4HzIYnJQ^ zn7mpXt`>RVO1#H|1SvkF3V{e?bsHI^59<=riT4b3?uH1ycl1lhmNj*(CVwe|N9>61 zlXkWw4!(nI=fXaYq^KWo+x37IIr}+4`}$z_Ycbn+a& z)lV`CMI&&RH)oD(7qI<&63|*X`MB12Tz&NF`$6gN0H#3|i}D_tV@s~A2!$M1aXj{B zWoF8aBj^(w1J)GrX=!{qi$eE-ecQQiD<8IC2S`UGhRV#!qK-6Gf?+dk2?RE32D6i# z&e0n%24M|L6;TvdW~2GS@YGZC{tFc$6Lj%n*R&NyXzdbT5F7SL=3w`&3%_-Q#7|Tm zD3WQf($%Ro-KMS?{=QhqW7evWc2?BIeki$#A(5dUK@p6EVaFws&Ne8)nG#X&s0k8cN(Ln7{?y;KkSFBDSk2#g~|6Jx7D9(H`0Z6Z-AA}ZS6yswDpef4)- zHsKVpp;(|ci+hs!{QDBIm ztOtJW!sz-Fklx+d_u+5e^NcU4y$_W+`6}>Dw1n4vbSW;odz2DTxtFtn(c*hZLHe!@ zAJ{OwO-INZ!0KaVAn2yyY{l@SFyF8tXojs13z!2*1a9W81|Ogw)=D2QUpyCV_S3_u zy{#mK>;Wl}M|5l8no0&kpaI(B%aI}b?+4^GMfM;Ru2E=SX{aZ z8#CEGF6ieVQV3{jm+z~=3^Y>-)eh#?@va*03$awdms#-=92&3tOJFIEE-Eb&aDG!4 zy0YpFxBmg15iQ6DF3g_;rCL>51up0VH9JQYGz28@h#j)%c78^A18K6A)x%P0PP zn2<#?9w+JR2NhYi{{4oNVlWzj0d5XFi~arkAXmQ9ydB3OJwAvS@yW>-r8Hk6Ia0hn z7f;7IbyhL5bQqe$rF1!Ytip)}IpyVtXDu$5fvQoTZ88n|enB*Wp8$HsyJ10;z^Rg7 zHwMOjXc4RoL2EOBn+m8rFyLc@xp55SBjDzWK3@DVJCaMk?46ad_RpQ7tl{P!606we zsD2ef+w;IlC*6?Cm**b&-J@90Uon%YBKWSWuqSE6I}!=%s}oDVD;q7RYWFyX)EuyK0TZdr zv1vc-p~ivRQV{`%9MrDI(F~ZS<-y=3jljgV(fwmbN8I`I0+=mEOaY|WBm)FM;E6W2 zKd^rSwZQjbF6o7%2S&UqaL&713zs`-ibKfnfrDF9Hfp8P6Md@{V!6WVd=fdmCB>BS z^lG7(MDN&V;?J)f)eyu@8+1Gj%*};B%*^^8Pl_P43rME1e#Nk5iX*-bi_XX9-@??) zg6%mDLO;JrdssUIKj{}NndV!%tpI3G*k* z8dzFlqfKFs7ds)UlC>VCJju3x-1s(35R|i*i}=JA?h-@{%-#M9ON7{Zq_D%qrh`G< zI7Zb3xf|o)+V%F9LQup>Gnm*>7afjI$qx(R2u14{FLbyp4WmEERSxZ?trs?}K62#o z>!!~pm)XLzA{g_l4<-|n+z5FAYWeQYMe5uEHV_f9v3whvf3)O>p!20UxHJ(aoNNpR z=bUwITM%kyL1=(f;&^c#R$hoz~Z)f71AAqZzaCNT&?XF5`BpRTiq7JlBOlw$Lux%pAcM+{sLNFVQ%#@>#^s+Pk# z2UnPMaTFNg%M&BL$~KEU|G|LjYd!o!7RfChcz23GTM~}sAdAZeKyGTkE{C&q^s!@* zvG#efO@n1MliM!Yc&!Jy3yoa@^7-?}2Rj!GXxyFO#SsJ!Q?l!Mm`E}bYr=nzjyj;) z82Nl=pE?9VL^{E0^-IatRMegl?cZ^Kh)c^=9(6hOR`^P|%OeO;yd4TozFFX~WjU^z>-t^KfaB zTE3hG7U98FO@O^@B+>#bQd~2+gGBbaM96j)H>sYa#Z4_qp-OiB$0hObG=% z7_~pGl9P=sTyinClZEeBoZdG_206$XCjxq)2pFb%3eQah+SVfz*=Ad97fW{*>$Q(p zk<^dJI_XV#Bm(rN@%0S4N51RfB`OkEN>C{QJC^nN&o5(TWu@q7`RaN=txIY%VKw(G z6_;HGAdpiifB<)U(uIPmGl*JDa^2znc2}RsSl9rN0tSVG!r>5Xau^>xVXqu<$?JUUASh5-ob!q89ol)Id)C&_qz?pO855dym4}u&h2t>&MH!CNX z-ekY5%X{@bi24Xv?OIP-YB3ZF{FL2ME@v_hmLQp!exG7~6NxL#nP_+T_8!HA3D~K! z;!2tDJIRY03LR_KlX_ZPa_WdZ0lddGk?fh%w&8@OhD9c42z)rX1|b+ndBvI?dpy7m zBc^OH%p4oJKU(<7H^2B|>+izQfjXBWx^U-RtoNmI2Q&_C*m7~!Lf|-d)8Z)hkC)&u}PvY0)hzkhb$Fa$8HK?)dKmG#S60EE3QQXUP z^^a<;Hf0uF20M5bKtgeM*MvsZnBhc!$2aC{Dh1=%}z#5LGB1?6qpyIWIB zi%bF{2E}+v3T?BEovm#@;7^iT-b62-Xq?}@t14Uw9a7o@c>U@4L?bO zGZHx$#Qg4UIgGf8;59}ZMA4|f?o{PC*rzk2CHzEWuEeuL5t`_Jq=tGxL@n&?PlKjg zaKpMoN^ZSWO|)%Ap(kB!E3>|=sDt7@v{$HvCzPMDz;M&yGk`qoPM`WhNZ;q?7J*wr z0z9lo1Sy`M3xaB60Qr7(qLjY#u3b4kJ5^7=!>POj9yF2zcJPs5a=Hs~7IRvo8%$Y4 zJs%P$TnO&$UGL*wCDCaBsa*pAo$)65%xaiVeEo3&UNX3Gk%YbM%D^lPN(c2cX)6%H zAd(jQ`-jY4pCWT1g!RP>cVZjyZ~Fah&9}b0OUV5M-3unuup)N@zt06xNVp6P?b^=H z^1d1ACl^}xgRM_GZNUA;aOdgDrDTKEoQ}`L=q%eW)I=-`iaK|cYz_H=$_t(JLS9|I zvS)rj-{Wuw?9V7X#8Z@D+UXebQ{>M1%WhUT+e=SvdtE(sHh;2zmenn)PruIhTH>E6 zo}yufZ8PAcunQVXFZ)qQyZwlRr$e*}4&tPZ|9+aHzSoRN+<-kVj zFzR3t{1z2|qPF{svcI1?X$MmSPND1Ra4@~~z*WMHD9pVz%3MGnnPOB{zEI3x-WbRw zj0VBO)w=lGm!&ppaPxTCNX@)gAzx$tx2U+2gXv$C59L?snU5mUMsV<1^uy9pAr2(v2&=%G)CVpXoXmLLaHg7NuNC?)YW-|ff7wgHf!#;O~1h@M3LO# zgAsi<-5Bj>cMd&hxZcd}cKK?y+3&3zwAW!FyszR(;Yh{JKnE$=?sB|49<3piU%-xt z(Y^~p1m!X7y;IdiUMkDyHeKJv+Tr~o&dgFarRJ`r07dfgGw1nlN8IY=YScDyavZ<# zHLoH6u^&Zeu)%C%e=xpr&lX%VP^K2%jw5gD=1p!S-}(q|gg1XZYh(1{X`$uwe>f;N zLoNp;#|TlXcdU%Wd-fC5;#U>fSXVj6E3D?per^j>P;`7Og`(!!PFwv+9xL^XUDZfG zMb3;6ErXn5kwKH8u)E+AiP~8XW&jbWnFxGF2_<~;yfjCvklzk=2-IXu4Bg(Bfmd8zJ8rauh0%+y-_L-$ z&H#G=_~3)eO460Qy|3VvLkI}l-M2lKz{x8CIQk#dYhd(I(cUXTUCEkx82AS=G%eCM zLinzS3YO4}(E3K8eAI5WmfDYnC!hqZYZBAanBi+i{w~OA`QN>O0mfCl2?P;?Jrth8 z#I7wrUje0m4aVZ3y&{bZE62YXI6xd?5n+YlL5cf9QgU(_!3CjHq=Z5$PEdao8WQMv zI>Vs_8Ukm)kvGfBw`YDu8Eu5>k3!~TpkP@|q$)z+p4dN-e}wpg>hG@tEozp3Gp37<;o)mw-;CTS6b@nZix&CAE|F?z z_zr_97$~^zh^Y@)haVh|K_pG2U~unvDTM(%a+_vQtb zM_kJ*a}ZXLA{}Xf8d3-rcoGz~Y+SizA{#EW!i4{e8(5i)W81s87Hv3WaisGJ9TE}k zl3`z}=hz{AsE2+&5ds<@CJ*gyjH69M=)Q2Aq9ZD0ppp1Dy0b&X8meB_Q#5rQA6_qe_|@y4=KN9P$I=w?qbRf!~DQhZ9k#Q;2GtVeXjN zx)H|Ook=WFImanFDG&C!30}yt*u!x35qdt(dpM)^%1g?x7rWK^+uhz%JTg`AI=uzZ^)2b$OA_xgTdO+TFb<%J8E zo`T-qSk?S27fd&3=926b;s^zLK`W4h2NRt;hkWPgoS~B?{1!qu!Np2C(NODPR*{jT zBOyb)^u~GihZi#xyYJ5J&7h+wq?(dO09XMHvUe?eIrLw}PI|%jkBCY2?#z3p&ckg=>K#8mCM(;xEue zeY#rR-6^i(`;gtHf(ayg>xxz9w6iUja7~nde(ZPM;m4wjmmE~4;tspmC`xu$9oxt$ z(Gz|x?fJktpGpda$3?39R`6QkwLYSNFuj)o8E>*fuebtb2QWo0uxbyp*V*n_*!)?H5-4zZJ|~1PjA_ z8VaqJ*A%oFkh@hYg^3Ie@pQ{MRfEc5RQYHN)WNZ5Em)KPCLO6gcK~9DMkVYDT)}Fx z<`gp39tHLPEQ=6|_Y&Bb5hms82V-Fk#~H}M=qwV!#SBiR^{`iJ1Up4?F+KQXMGrsb z4$uDiqwLXsS#^olJb)g&2$VY6hL0&aV%z&FkF%`%)KXuTCMP-ZZMp1)bX~bvzm?5h z?wNli-QDjyFSmF6BXQb$IO%-y>5JdKscH%2$kf8(I)aHqg7e3;>({_&(`6qYY;uxu zql7BGwr*J^tx2o&{xm8=M_K*w&%F(96v_cMw%eomfi3H`(VMiK`S~W72y?n&&w*+eL6}%aNq^p%>@y3_w&rNXRr8= zB>T>x)4y|1{335$W~6}g@F?IkG9{j|Knq*|!}*ATT};UuF*t&>ALilLLIf7+0B(U) zol&PFw+8;9Vv1+>?9MySmVIz_eR38Vy0}};ft`!X$k|yE<>?r5@3i$!+xOvpjE~9~ znzSBOq`Cg5xJ7_gkrkh$f0XN<~39W$CvSkKZ*Xbyv4%6vy%|6%?W&~Pq z!Mp=6<;>h<2*^OFjLE#|z@3;_aWCV$-f7;vv;ZZKw`3rAZ<;);d5#`EN_HP9AHS_W zngRFf)j~%OMx$qeEPu^OniWFKRvh>`|??7BdV#cDi`O z_W?Dk(aEwM{MzhFs&QKqSFh1~@#s1;T`d*D2V_OfnQZcV1=25Qvzz2dKSFoebd}1v zi)R*}lx8mKJ!Ij_cJCM)CGkvpsP@!o;mxJTjv2*Gu38}|oRoZ#_VFG9@y|rS7|AA= z$?+qp5DGx(NW;;egwxFR(WmHRidlTuef^^JnMgD*V`L>Qp;%T;~f>eTsa%S`+^$NF`1 z@J(8t(Np!Yqd`)qy}wZ?JI)D_$aJ6cd@U^}=aJOnu#fiaLynwW^G+t4tvkMt7-iac z+|f5V#@u-8#x`_MNBZWtkc{~4KUDD39xE^bAcsk3!tBYl8xISEA3*yBwGKPV=CySs z-8j2qrrM#szUuaOpt8`yTXPCuzkZ!q2|^+tof8caNezrfFl?yJ-A0uiUXrDD6JR>! z0xPxNbmhH9)}K!fR+|BGls(^(l-y;RZE@yR8t^Lux1zZ$E?&qrffxO_rDgTHKg$a^ zDDQmdPF+uR^577pba2O9EO!Xacb`rO)F)f50ggD(zEJFaCj|3 zp*){IU^O_OIoA4H3i#jkCMY2bz6v(2HI!a?2544=C4-{jOh)V=v*CQ8y%25vkR+ZpCM~GmF za8^i61XUa9JdiGU|1blPvF19Ak9+a!S(Q3Gm0+gPk^Z3MHXQPaF(|3#h=QCuWq=J3^5h{8$w|J-k=YL6c2Dz?^!1 zhfC=NuJiAA>lf3c8y^dL$nBRse}1qmezyTC?WBMERsxMnltJ$iT?3i0z+&o$haLaR5_JOv;Fi0M{qtWPeJ_ zL+gss)`AtBBdh5b_IUFo}-Tv$>yb3fka|@AB4wv3GH}9 zU&tz?B_(w#SHma-IB3WDZ0yXTv*0mB=h%)gPY`lTV`JJm*>-e`GJ__C2)z?0lIUK* z?>l+vk{Dn?GF=Vgh_maRnno``e1nPirG4t5p21A=^75qILU)e|_dn@efr-d2vB|#j zpp3=LSXAtp>$fdcceoz?*GF>&-a-Ic%o+t7hz<4hNtkNK0?@t-k#} zwLTH`M|B0I+&oLeF4yJS_dxGq%%;0ii0a{t_=0vRE1DQ|{R2b1_4V~b>Z>b%@}e_K z%oIJF<+h$jpM;(Q5b;4>D((7%S5FFj`j2g<9ittrdIMaP;8qbVOU6KEvH5yYXx+R4 zwc6bH{=uzY8_%8O7d6}xR-4~6Bo0-TL(}_G@DzD%^r)#OTTOJY=uy{j$!pkco(p>T zUiK&>Vn7@0Nnz}m4Mhn=_7o5;BZ2qD9Ri?I4;&h3vg1NKB+#n;YfjEx$tNlPKQ>6y z2N&B_EAQtOHGD{s@e(QWMjo2)1HLtf_O>4S~jUX*ODbvvNt`5k|b0Xl7|+3$=+pOSaj)wswmBCyP_Ey-C@n0x}g5l(l} z?V_@>vge(Pfl}n$0{!Pc>(uT)hdBnStW}%^C+NwlCGr3F2eaKGzZ3%fe$n%lFkBRE zu3PBv91(f)z&`!n-P-nRLZ=mN)t6A(=PP5Q-v%50N-bny-`20ZOPc%!X?jV^R?_b8 zs|t}|J(=^UtcFe!{*wS!(U~lzyM{h%;l}-XqcTw3$o?slsK1Nm1Or9p0NWkT;H4af z<`H>m3HO42RoU?Hj4f%u;rKGp@m$_SxHvnZZbTcW$m-St9wls-e%zD<6k4%w3NcHc z&j99x%?nhQ4vw~7bxF4m`?D^lUO#n!Kl=6T#$8kUqNcu%RzJCMvXEKdnm+QA#82m& zw>xdV;PR!Nf-HrUazr&pOqyV`2iBjF>pbB45=`mc*K?Z%$TH0EiurbfGt*&5KH>fE z-Kp#N>J`gUcG%VW|D<(muRB3Vemow_Oj`S)RRZSv4iTU+mVX82>&8Yg3W?x*M&~Ss z!Vd_Z1cs5N5zax5-E#S#4-4hPIUir&*FXJ-ZvkMZ?N%*N>I19*h?WD^r?~d+Zm>s@ zK*+^7`~H~00N(GmLSAe^&%EwSJD-XvS&FHy5YaFX;@|X@v&Q4yNTPsj2c1?k}@27lHa-k|sJ_n(p$nKcy=eYHEIQWrj0)})ni|B0&~ z;1L5F?aYE`ry~%38~V9R8#&g^P4TME-Ty`XR64wlUG)H)7|}vdCsLtwnR`1wD?tnn zG38STEk#1aCdkU>r?L0Fz~pUYx}Jz7`N(U)^TkFu8>`XWE`7`wYD&FYCG*LE6KNwEDHdZCbz$u|ADJgZS ziN)iU%TzB8F|ddAvF{3YOl=wd{=L^vbnOR|&9c&BreRhD#NmOnk&M<{e-B$^3AhIC1 zT3Kwewo!M_9>+aZeYMO!Hy(1O3HeHX`52(^gYV`6|M0%r3rnHN`XDl zQ-5GF*EGwHiWNZbMe#*$mV97hPe?wuv%2o;dP?=h!Afj=;{;g_VGOX~=H?D1<6n@& z07DA{)JD8Ki0T7?Q$$=`+!>H$c)naxbk;aE7Md9#PTHqDLg(Fdi4g|LnZRWAxW0+F z$e_9>?I1D;6L%9~ITk!2l%fk!bhYM>?*-|5Vfr$&Mtl&@q<-T!%U@CrDAr(*X2@gATGK3ou@774)%bw21 z&w1hpuha%FUWloMVmKyX{YRkJjPyYih1$uyyxr&2;S8xFhdISS)m$;@t^3yQQw*u( zA>;AtwL+M9s%95%l<1+A;6lNhlOT;E>iZPdq4bBR2wGd>FlMjD&BS33U2xJ!{-Vgx z%*22x=+g`*xB3Zh6g#AENHiY^Q6{)lU>JkeWW=RQmpTDeA0o^OJePO+BW$>X#(V~s zGd32n3euO&E3LV8IPAq80c*POzGfYMd?}tU8DQoCAcu|9j=XZ{x6tx#JwOBz_dp@v zDka^&_ixx6eSG=OXJk#_J_lhsqpsI|6e+|N1mN$Q1$}LTIX>#5R*cu`xW=NxjxU5& z1=nlT1U#pozOF@yN=gv|4H}YTDKM1L=cAy6XpH8O8AHp|6=ec9gKni0p8*NLM5;tO z)TA32tBT!JNaa_)sE0aFYU5Zu7J(Csux`L|UkLoJTu- zD^H?JcP*^~mLF7mdr>ev$Q6hSSr zhf~$W`+9%$g90F^?TCD_?u);EBOILJQYt{u>w4i=xFq`#HF` z+K2n^{>sGu!VC0VWXYGb|5t>f^I}eH!tOT4_$7{Eud9>({;cCdAM590%L3|?b4wP_ z5AS`?!0h4uTiIEV@mxW{-zO=&>t-e3Y)l{nG#v<+fuQD?X@JOwyQipqJOnOPh_39& zj-ibt=C^peU<%#q90a&SrZtm69jFZ>;oA@Ivk2^R9@D(6w=*_CfwD zeX6D+q5bM^;wRx`^etz#%k+_j>yFCx@|rjYapcp2CXV}@1g9;K#0?t@%UETX}@-4J@um<1143cW+ zeP7kpvZ9hwk&6vg483$Rf=VQ;q~gUio@qd5M|V9swTMG_U~%&b5kw(@|5%x$%=r@M!c1<3aq^W z;LH%{A2Oz!M}O>z3xdvusB^Y&KO5+7@FfULCquy3gm;KRvL2$xKu9AzPEeYNm@PcK z)>SJ($>APq+)V9x+eQ(>IHbTK&*Yt{ao6-4EAPv~(*B|dLlGAfor}EVF1_wobCKKk zTifPq?MX5j;mEhQ>!@zF5BA1*CcHnx=cY(i`{5Us8&D~%VNo2|kvrf|gd%tilu#ST zqj}B2*fsF&Ur$-GpF=8y{IxcZF}Jc%i&>PCGS^*F-+P zf9Pe~ogXz9pz3nnyY5cqoYZmp$=cx0>2?w%QV^k8q$zRpr=Zsz3n>0xpE_ntx4ZxT zT!I{KL1NShTd~qU*O+MI!?=oEtgFWcc0Oh)3apm&;}pXg&hd1~j5qDOe^vO#*MR}c zfrvPCLMgOL33Lx28M*F%Jt4vTLJQ;K zR=lzS9RX(B6N5vd7l@>2)v^OBnnSq3P%PRcq~3_ZSD|DXb{Qu`jjrmT;^UhSTur>~ zt@T}|wyZpL{oP0V>)g_(L>bz;89a9Eh+J+(U(YqtmHgJ6N-G`DfXF8HRICL}R(jbo z{$0V(#5J6w?{*+|EJzBVN1Mi!AhnNL#LKB6UW|-E?i*K zS$i-?Ixorq&CGx!7MEQ4S?iZ{nNn$Qtyeqyg@1V5q&%v^9j0<@;fDSai;xRQQ!=HA z3_Xn-Cbu5=3=poh7VTX3=lj#uq&>x@{fDqZOf40gpFN|akjPoy^z`Yr`QAs5-j&3! z3bHp)w6o5+d{y5pVA!T}Z;l&> z=@#w1DPQPtqy^F|a14~ymkxaXTx~6n3WuAQmmak|8e4@G_@n_4eTYv2)NY&c+h0v5li(%>B%j zt-`A5ch-jezHq94_N#a|hj3FOsU9EeV0)0>H?(;`ixyd!{lScDm-O`Xel)?_+%IHg zD1Eu3hNl9ZTbyzeF5{}dkqF~`b5g2F<7JYJirs%#6xeq+hRC0DO@QGCejADk!dVH- z&p`D}UZdapEJbJA+mzVoo2m&Jqq-y1K7fTshReDLr|&rSD%>j# zpvhUg-47O02Izx>D5+p@&J!&*rUlY9sMU+8mJC_vom?;=fOKYwco+fPQxqdeBDx%p zfhPdXM8GJcPMPcnT=;Yp;PQjA+EnTX1r|XibMcsJ1GX||_K~=Oxz1e+mo=4lNnNt} zGbTQm%7UDF?U))Rbcxvle{UKyyHoE>CU;ZzQ?h(jBqW& z9%bx(s4pexe|E38M!1&c@S|fO>KOkQVkzE07b6$`b8@l)J+YhaZPI{PYQSS1eefe5 zoh=Hrw%MwEOfCRs}D+Ghjh@<=NiMph!><2yKF>B>w65U5N@D^nexi zYxR`U20@!9FePysnW#GprByBZP|9$n!vXt3L{S}wc3Hx1!M5W3!UnKhwag@eseRR? zyX10|R}M!6mH*&@{NTMLtQPsAF3qKP@8U&~p6@Dhsby@VJ92u$wS?8i`EgX5q74j; zH3NnTnwXIcO%Pe0`SjZd;OF?|C(M`cF-zE-$?PW@I^b)a0r@ zpsXrnni7~;6nJTvE)H{!zJ+qZye>ytR&;&z;{a=@i30z_s*)~Dm7QT50i&-DP!^hu z#A^|^b2Sv*s^_-E?k7JAm@vBRTB5*`yt3iiP#0x};s%5Vn0(-f5Eu6_NoVX}GO&VF z%t_)gczliH6rQia1)7ll(IAErB?c2H>o|dVfdpX$9vqP!cBnkb(UYB+I>g zodjEeXA3F@Dyz>Hae&OQQa5F?nYz|R2nlmp=2@uLhT_nTvzkIX(;R9?BhO$IN{(m@_u0%Qro*(cJ#V#mr@VlN5a z3bIvx@fVOKEZPpkfe)5T`K1S1f(pgi{Z;U))W^eq;7g3W$t2Pl$S$OT^?dnKFgJyu z72ySrtyPCi%IqmhQSy0NC)T`t|!T5;F;7vgDiS z$-r*V-!U!e+`T}(M-^h2oTXbpTkvEk%t;QF+BNI>zOm8M+iPe5_0_$J1r8!rg#wH6B=lG#5i{5J&sq=L+j4oVnm zLSo~Hlxgwe#TKIw8tJ>ZNZDMJlx|P)WMntH07hJK(BHjoc~X{57z3`zz!g#*hk8CT z(&bhQi^SE-jcy$&qsN=qv>00}qV~#NsQ|X9NT;N>l;i(lRz_AW5BI|V3j{0pOz0U- zPAvFGyAYlUf%4$r7tZqq#$H5Lb_xn@LPA198xFoATt+PA+=;9K%uRs%N(Sdpszu() zqF~Mho}iAQ$lIE(cE!z+cH&NeKg1!d_8%7+ML8JU*4@t^KVA&P6eMjGXAlGfqVV(n zzYj}!x?0mSy=B%CTp;-Jv<;?U-?Ols0395{u@&B4&}9HGTtqXV(hft$KK4DFSjYrM z{{1Me^?1v=+=YGWt5%tL-CzdMdBL~rzcy-8&PQD06`k`NT6W`VmX`L$(wxWW9<~qAE9Pte0i@B!jj~^4HiG%j$Hzwy z()OMa5M^OJfq@C>ClqC*3mu5h8r*~g8aZobeYLxURUcQ*wtY^S5C8O}2r+~lW?%v0 z#_hQS#5@DpcL~G`h+t&qpzv?sHxFVU)FIvrAPqO5sYGDy{06I!pJaUAa#DTR^YyqQ zKNd>ynoqecRLu*fVgEny&>Z2b8rod`#|}$+zVQ9Ki=il#b*F@akuU&geu3nTZXps# zN~l6?;?oZ#>msyO&}&Ssr6GwNS9V^kqq{%3vwVN%#|00jfRUPoz=ddi3;t3d$(4A# zkONl@^eNlQM-lMNBSf8HU`deLta8c2`BHe^qTiRHO{3HQk7?VNlk0B|{f&UpmjRd& zxz7M>g@SA&N>qS2Ir{WsGmt6?FKZ3$D3zMLKqm#lnu-!uMK=L#Kyx?2ps59B^0r{dW)S>qg$h=Rd3Zn>OZb&`r+DMyA;P;OH7;_@{Ve z4EvGwpuAwFV80Qw7Bcn|y~+~=ZK|o!C7{Yr!v)_v7rR3E-USy~)9FVo2r+SoG^Fh7 zhyh>N1@x&SkiDv4$PP(JAz(WKHM~CxPC}T%oe4!&GV%MmwBot7=C{8ZC4^8(leCKC zA5v|&p$rEJ7VeK6df1qRX?5h-u|e{K0RgGEH1UH4Slze6&sRU7hriL^vus%;iTGboaIN^yWw5JBGZlzFUTeeo|4z6MQIN>%k%yE?HkBsofd zzcZu|<$Z&M@0j)rwkFxcd~WNBAv`m+B%QjEApz&Flr_%|HW_gs)%te~ja zZfyc1#%=JDJSt*c-8v%l@9ppXtmV#aP-1v7JlZ((f3r-xQdYF%hq~Xn@2o3(HZ5q> zu^OWAkb-LaZ5j-LwgAshzqFI5GQc5^F8t`Ja=%~~&cDS);S1SJcoOETE3ya@(-}*{ zd!6MUSM}B`_)!i2#SEk28j0Jb!{SUzI9dxlWdRP*4GTD)Fu1Vw-d*mH*UpW!?VYKPw&tw#8ZgyK4l!^M0XoLkAD9CCqO>! zd;OY6Kp+Z=E?GIbUD!~hEJU(CZM2a`h-SG}eb=FVx<5N7tl0#wU=Li~AVfTIa7Ult z41N%@NMfe~?r1~49S;jy*!33H`z>*r#v=>t2PI83G ztmv!dnbd-^qWW!O18~3j`;MRw=T!q(xd;Z`A0pZFCWd<(%KfA9tRfTpC2F zBNG+@vK5rQ#(&0Kym--+nATZeQP{`^Li_1cxy(|>pK&1vJEeYUpkyUQ+^KlaLbHKN zGeb{`*bkQPdliOJ%Kgwy6F3%zGmJ1fMCW}F2XaAEOlY-G?hqN;Wn48wp*h#LQ0Gg8 zRH}ThKVa7HeGWq*=A}hF3noW+q4$iEvuZGX3s848>ON3lI7nB1i-;v3`dyJ2B8Ayp zPGO2M`RXg4JfZ%aG>e`Kl8G%Xy*l{Fo_Y_yqTE74EEon02~&Or+y~EyC}T%UuU^dsuVxju7%FWu z-<~~^d*Jpq|B$7AJ#G@a@=^M$1A^`1mYSbwOWBin!P%5>8ptGErIXRJR>UT*=qt~U#y$TYxD3u^ ztz$K7!Z1;Hy9(_^}TTDkRQfa$jP4i?PC%D{<8S;nCw+@*03-wHmv-UY20cJNVlUNs^k z1}e>%b-vrb;j|DYZP&wu%*N-KE;sSnhfWGq&Q9)Syh(LlYcRN$^8(cZyCd5N4{7qpyB@NF8`AUs5SQn z7W~uvV8th(cCkz~@a+vv98I%+W5LHW{*^0Ly)^L3I(R)s-ZeiUMk8HVNayInnWPJo ziO?>CeZ#+^D1T%Znitl*|BO{@3pYz>n4jlZBM~BIO}a?ht}1Et`k_Ve(26Amd!#&MTBF-rnTpN z1k*K}v?HR=m+Vn-BlwwgAu*&W)Gwfc|5fl*val;d7A>{iKJoF2a6v{0I$zwt%%P&9 z0v1T}J@%ZyYm&=^{C^+PYFqF2@$81e$b}Q-xgor{4Tn7jlHNFPP&}Z&Jb1DZS*@sc zuxNI6Ht~`8iR?~GQF}d$<%JOazL8;zrSG*TkK(r^9y{{&COl5@r?3f|841WzI9s)K z6Dc~vp3ztTci}=KXmAu&^q?JFOdm;{L3Q`|WdAkeLjs^|+)9Kma%SiACBW%@1pN-; z+J|xh20G}}ahKE}bc};-Ba*WvCvermx1i^)_{4RIft^Cp4V@kHzqg)f^#FyiGO-Aw z4;F>{zd+)tn<`A!Oun|VNUNUybD0WX?ZZaML;79_%(zSy9`{*rPaXJinKWf_8?D-v zm^EMta@EOe*RJuU$>lI(_jFf==&V%8zmqLleea)1npsfdtz$C0UB}rp1$$_1DrjDl zZuGs?7yquP8X9TPm6*WIkUlZZbDvSe_#%t5cE7Cl z_^|W_*TU{q^w>ht!_%=1Ida}CG!7a^#w0<+A_Euylhi=mirPay;3ze~FxZM-dL5Hw zu_B#*0-ayR&;2J|PW*p1wy{75k{LOm!6mLU*kn=^E}L`;Ek&153LxlP442(5R{1!! z%ZMaID{0re61E6V!85z>{#N&Wa~l7P5eft@%B_=#B$e}PIDh{DP7w>&{56SzHy6c1N zNGLccF*jRy>>vgTM6cBsjOk2-=D5YojEwL*Ev_X@;f7B!>DeKfI)WmAG_ka-w@jG9 zO7KEPc)yze(tpWOyU;m4f!E+v!S?{5tBntUNHQW34f~E^F95946MmSB5rVx8zJyyQMTl16M`@W=Qn z;sR6vZ4ZtfAhJxa`#2G~c%f+NqIG>Q00WJ{kjKc2=5yQjR3U2(oET!XVIA zcAjtpL17@)7UG0$7ylcNRo5O)3F(E3+gU1JcpQAU`7Y6TM6`2xbZwfY~ZlHbco1d z=4PjyCI*q%4ifwk(PtdH$LqQznNUFqO)#I?%s>x4e_?MRJi3BeuWXQCy@@kPq)Re* zz8Ge#Z}d@O=U!pDmawplL+fyiAvu6s;36_?6^)g6RL!X#OVBd~TcP&Nn}_YYmvLkR z@g_sG@U0l2CniJRU^zmtD0nBuAqU8Yzz+gjD~S|hw&&M+>zh;4L(EAv^@7Z9^W9!w zjEumb1t{ z&go^;JLa;>L*v5K%OVe)Vmt-jlv2x#{4IH6W2u)fU)F0*bBhd9TvHcKhX(<5%NaCT zc+0IO8y@9uHpXKJKi!G=B0?el2m`27KV&o}YKb`p&wNnrh1J>khcqWL2R2>9Uz(hL zD4ngv7K5@%Pr(UTq3LVF1(9BMRAGTq{`TtDqD)0+ztti63r@qZqh-lah>d)hzeNt# z0t$C;OD^8sl~D9db06j!*j$*B?~vjo{!v(Hep0ElF?T zhm~~h+>C9P5{o)%@*b%d(J?WPJg@fF&@Ao$?mPBf+5Q9U{#&dOpd|0n=<;y zhqa{j{4X37`gdQ|HzLvsittn0v+N!O$A;O=-Oct;NSo%a>d?Mh(nda(C2Ddzud-lG z`KZOee3h1~;@)~9WPVlvHk(WoAU(#FmFS|Q3f0}UOB&kFlP#NmZyCIULX`CHfvBT= zU@AIy`6{d6cA0_JhuH7IT!G4J?zwp<()SZo(MsjL)L~X5${D@pLd@{0NgK-ff?V5HWTmf3yf47)nZKjG~2+O?6wF zWh`&>Pq}p)czM2J5BM#k3r`yVFJ=ohDSiNQ9sp^3AL|6f!ljLoC5nqCtlZ6UWNG~1 z-bXKsvKL&f;Ea}i)3~l`;OHHmjHvsTGse#q_3RF*u%4oJ?+~uJku9pf!8ZHAnp3PT zNoO5SI|!)yf61Wt_b{-C4NZyT*>L27tNps2cY0E1^AAr4E`9hWB_~)j!E@ds zbZVX4nft4kmkOGkWBqf$=7jF>^|8(a@rg|gaq1P)-^ZHDscwpAgnM1BV!2b2Pu6yo zj<;#-)g0dGZ;{Q_==(ljpUK>2$-(h8UEP^@(slIjl2Wg&9QCU#^bA!gi7MLQ-o0OL zQF!mp+V(BmeRdY6%iaKN`Gcg}XoQ-8je5y~Y9KlGV#?p+k)Nz+&rbix&Tj1YYg%eI zIPPMwx#IrS zgK3FO$KW45p3TALdbIu7Z@#X!v|qVv_N~WIqwBpMJ#Mu{$sr>j-6PoJI}Ja7^gSWI zYGrGV|Fd+-w|HsZTGnNF+Udc4k#+nXTY-!aBWluCKL-L(45KWTpFUg_oKs zH(&n!X>@sFV)3tcWsj05 zHrt@)lah?8{Pj0WlnZZP={vscP4Ah|-XyLlfi1({QOkqA75==)>Cm)&{Q>>WHb?}L1H{s!ljD{EA_~&d~1t(+%)O?t>*oz4M zjUI*8x8r6iD%#l@S3XnIX4|cx7WLKcf>*}V!k2@)mT~i?By6>lOJ$&GE#K6aBc$3a z>YOnC`kFe=F2@tRUA5wqkK8}yWjeAR&izp`yyaCkd+c!@7nYNYH!IX-cHJ6l(-1f~ ze(cZ8^6SlU{ew!4uiZPo`YR>s>Ia|$dmJ1d!gnL%qTm=x#NUW~?0or#medHTUBim9 z;=fB->1q`;;xg%$2pq2EUod13c%FCcu5d!HTF1rv)xVMwrRDOtH;wG4=#*GdkDuSe z72QpH)|jBEKzo-`6f+dV%Se~Lv0K;mj^XYR>WAEX+T6ffnngyUv&F8h3wbkphd!=R{J9)f<;eNmxU1{c|6GcU zlb4t@PVa2w^wzxg=67~wno#*OUk0PEwrjU(seq>rTH@7kVSAZdt+xV#=ag44Q)N+mu{g!M({7 zmj2i3Iw-lRh8{WatI>D(%;oL(I~ra;u;OZKk& z&(^WC)uVgUn;v>*We4+Q@&KFu#xuh^`klQ`w_LrmtF4?WyZelT<7>SAf*o8HY3o*I zQNLs}$ZYJ@+>Q$F)lse%SE;VIpB@xleKFoU{!_8c`n}i8{fDgLLE?FtGS)B=b%7bB z?SmsKpoKFNS65TXe@IyJeg1P%2^F%1Y1h?~>3rk-9#xm~hkTy$mTbJ&CGzg27~=@_ zqs&HgmW`W*HmqG^SGDQjTv%IyTne5?AIOkxMsJf_h3XT_3L+h#zVUdE4B%SXg{n zR^Wu2qFOl9&Q)6a`GiJ}E9@IeppH=&gm&yy(b8Tv|JJ&S!MONVU_xwNlb9v5;c52DBPyP%< zz1?Dr;ky>*F?usCDBRYUG6FtTlHrg!5km> z#itZTjRMV(syupHgF3WSz`+7byY1bsL-p@1|W%%2iQf);_K|+NCjl=fWdnxvW!}j7*xUv}MX4T}Goj ztrv$Z$fGcTPP%?~L5Af&v#$S}O&L#8@bd4eyXQ1H%oM2ZdM;6u5L+Q0X!)p*7CGto@y>DW zZ{ZJmFkorGgsMvu2=!cc`n%k%uySLfKdjx$; zOu1TTNE204^O9`5nqq%*cng!>JGR@!rv4w}=}&Sk9l2S0CR-#t(sK#ba;MY&LXQ7c zv$ztHWlI11_m?ZYK=)q~RaU-a%NCb)KWn&Ij07q>wmDtCQkd+E{#?uoshlTc!OIB4 z)XLimxnj>$o3XJ?ke<`ngOmnpYFX2tvhnHFnp_m+f~%_>zwLGUq5>Z3f4#0u9=6=D z^lV;XL8C?dMJa2$?=+k-DAPIgSd03noV6)LqOE5uzw&9gwmbi=u`aJMG^D>WQPOzp zb>V8kd5sGv=FE5KaUZ<@E8-zz{d>S)UI&C44wWU{$>}j^uh+q4sp9)a6O>b;`GSi& zn3&L26JCAH#=0|6Bq4Wybrp>LwheW{zJunX1(2Z zb}U$#1;kRR=gc2W?JXy~wPyXM>)&UT3$Hk4(P5%~@Ex$8bj_oxUpl_tlpt4qbuD7c~pZE;%bk&9C^yQA*5p{Hi!wV`|Epa|8MMTPhffwiWR=pSiGva0IvooN61Y15IF(WVbxs?ooQQYR81nGah)-Pqe$ z5NWLJoY?&QfY9^@KjGW6{oIshakqpv2X7ARr97$OQ!D4mpJPEE%YVRI!b_k*gW&r*lsIIhPQs0Wz-{8sM9+$LN5K}!9xKghg z|2X~eH3!<}x;{)%Dlhs==eX_nKCDmF)!W#z+u3@VwgsII!_6T-;X@fu^Di&s`NB48 zlhbo@YNh3{<%i$a45;f*$H~9ou-qOe4G|RZZA>{7AhdAkky7N$$o_!apcFhpfN$$U zdjbg>)XIKvAV&}4@_9%vK7RO}e#@kQRY;se@QF+;x8%FmJAd?Nh~$-QPP|Yv_i7yd z*hBW(cAf({Cc>(UX8 zG|O2mVtMAXy5FuBuN`#*S0W*@9cOk7xH6fm3kMFOPy&8WgraDl8Sk(wvfLJ^hQpTo zw$bHr+Bxf@b^0;P^K5oYcRw=rAum#Q`}p|UUkMT~|48&CY|B)LecTlCM}ChKbHvae z?GxwcH=N!6cAiq`5MtYzQALrMI+L@QZ-vDCy<3WhjJG+Bj<3%9l+sa^If`cGfgMj6 zMR&g2;k!LI5l;Kb%Ql1W3(GMI3{ILew06d-8aMhlaOpoxIy%91d`!SQK4w)GL*fd( z1c_2(`q<;4qsuLav)(5-YYwNJIWh=2x#~ ztkM>Gq*R*B^6!~hPhD1l_}2gJ8#`po(Cr4g?XK@%!0n^+H79m=Gf1|z#*niHDHWWA zLkpJgeNwc-uIxv|@u|UQZIU`MS2dfR_gR(2XkS~`Q>7^Xa#$$f_=Zc`Z-+W@{&Pp!0(t$7=qII4KOCcWfe&mh+otxEdQJ+-gu z>dI>&EqWpIRWA$QI^5IFb2Pu6`F2g9#`2+&c_)Kx`y*Agr8)x^^Z5o;QRC)uU zI*gI+CMFt@I+={}xvX&FXLZjT)=KXje~qic*S1nZC7rW&E1a!dOVvJ^wDtI;ku)~Z z^+|u%`#qi4gbu}chBa0?HR9G!W*{_keIIGf&e;tQQp_-0O0sE90NDG)DSq_fZ%)G2 z2alT|o^brwJ}uF~xy?{LTvGAcrnMC?e@{jHJU_r7)Q8a5v9*$sPO$5lp*ACr<;f#e zyd6qJQpNS=hP6!7_p^qE)?dY)(5eR6@k_z^o*P$I`tI2E;jq!TSHxAteXWNCHg#*i zkYT@3wZ7VqvE8-zh^m;=s7dku-!n3ol=CVs&9Q>XJpMOVS-53a5caes-?fQMs~sVG zK3!*X3^_pA6BwbZutQ?FZROV*S2}6^NV`3zz8jk*zR)~kS1g|R!K`R$_UOMi$XIaE zMOiIj;|fo^gh<|XtKT*@Xw_((d$oAR@Z-?%_jmR`%VQeOBt2`rmy!^{-#&OxA}wKf z@0ybN{U}CEN{J*Sl}6jNkR(NmB9jVjRHV%$ zL$=f~#AI#NPo)%Xik2}I(>kP-+^>)3cR%-WJbyjM@!apuKw!oG)#+d&qxAq?WI4r-G~&Wp$8?B0Br!pqL1zegGoX zAbKV_>&TJEYkI)KvE!F7@5{FJ2mcg>%=;BTMtVS%ItXq78FT%3nC4BdM2bd%&B-W^ zIVB+lL=8q!ZINSSpxA>m$3Wg@k3;MIYQQZe=j zBCKKA0(W3m6M?qr>kiUu0qBO{R{&&=rS7-W-DZfK5zND_hC7-r38#*rUSk8$P(g<} zvE&w#@3hzJzyIeSgS^H8HARa|#s^?}(oRjDI<){mkPHX@F~FtMIhA)kqCNT(3>v19 zIN21s?}ht<)z^4~z49*uR&0@$D6g7fy=IjU)Yoq=1hUcf&%Qo{TjTnB@_5OoE3T(vR6vG|=P z^eH3+Jxp#HFjH1h7-Y|RY-O|$dk;lmtC^ZNOmZ{K(_8*$j6U;m`d-$)fJIHr|M_$h z?mbaIUB+Xh=~X&qk97+`p-nIE4gn&rA+H-OIV?xf{c}bI!to-Hfq9z7? z)0-ok=)O%KVR|m=%lsWQ{noKfAb8&VT>XFL2WwqokH{Ph`Np$w-oa2xQjTC{42a&Q zm)tR}eB|f0k!g$a4s(Kg;O)7lz008LG$h6*DYelfKg${Om3%{Q!WfXg3Nr zBPAt8+NFs9%j{jOUYNX9uQKUe&iWz;Nh84_6e>H9!J! zYx#~*n&_1W8#hh(VF#>K*RJUe%#LwiGlg_D?usA6PpoJ)h0*NdOEm^H zd4AoYwJHA0Qumb|V-BhK!(SVh;GlhcNYfg?=NU6D@t9Ck;50{rhO9jG=H0s%3dVeZ zmCnvfZ(49HhJcik<|T{p=1>%Pi>)@%ZKZ=zH6^hsbZK*w(rr zi`yq-3iAr|&f1Vi1(Y_~Y_reW@W<5f+;51~9X_TRU;c|eO%A2C!Gtwv;z9@7h)0P996ZPNi| zaC7AIM~)mRzEqY!7=HPi;=LWtZ(D5GEAj)V03o`u0L815HY-Xlm#$nvk)gYIL*R2` z>1^=NA9eWg`n(?08|MJ?c)t5<7Q*Es=_!dtOWh6r)FhVw4o16e{>!29=o>MCkk|k7 z2~%T0MInmj-8`~?Vq=NoETDu z2%FdO>|{}i7HEvAX*AWiNctcw5Zl*I6Zy)TKN~;4YJ3e9f8X7gj~<;Fv&=0aG_)NJ zj(FXnm`ovuVTObQCxxOQK|O54`~Ko2HT{5j%mqf{o{(2-pZxl3{}%b?CP5h3paroB zMMpK;hB@vf@$HFJf=Pm(A=X|HF(WeV^{gx{$HG+?7_wr9)_{oSqT1Jl0|NfDMIgmc z>h1R}UGZ13nGa?z0q|VrV;R6A86#3hlbkh2^|b3KhH=d3McN5yp@{6Fs`&=mLp}?E z@UJy|WiOP*V*5q}>7;O`pf<0?_w6~|Q7k2d zIvVLS5epE3PFL4iFF&)g6Z7WZ&05p%=k+g#kKbou?gTnXoN=)7JO@`_rL$&x*GG>ZZ^D0h!^amHmGAzT15{jWI*^8bSQy^u@v7)tbYk5Y z|GdBn^X`OW(sjR`df1f79cu=(yZj%_Pw0=AK0wMGI)P;3RmO+8YbZ7n&Ztdju%k zk9&T4);20LQ%kvyDtk`t!MA8zmdv&eX=9)rbjhXg^5yW2lZJ=(^zqt&9;!%wZB_5O ziKQ_uaJ;!sSa`S?oGAWC)#0y!v}0HCFAxec`w4fC(zLOfyZ1poQS!ko#AS>ow^3f> ze{oB`;;U&b&CECIj9C4b?Z7OwJ`Hwl4N`h=B2oTt0?4F2Q+5%)Rh2 z>L{w3_*Ec(EdCMA6%ZuA1J61f)wDAqnU{EP-VBT^V5P0_v3Q0lcv0facDbbI!Dbya z%xlbyjE!-$Ss`o|*+d+J7zKBj7C51%X}Tb=9Ehg|jX|Fk4!W>w5RC#rz1kdVitz$H zJaOBsrj;m`043bLcW(etNj)GhLeL_Lu6|X%6D@@u-A8oh4i5E>uiX{gLhrPP-X`WU zP1!K~YB4vmi8MW6+DC6(ssA77jYtLra8!QwgYkT4_%(P5K4si{<0G&QbbYK*;=qAO z++U-N9=(Rj=~7DCFnd-vS9k-x(V?r;ni4Eu)lY8|WY-0Jh;35oo!^$UMMGz4)vrO% z8ePk8H*kPRmf^IK)Fx~-5|e_cG?@DHvP9e9R%UQE!>^%3>Qjbr{x2QOk9oiS!*Wsk z8WNZ)caCkfsHNZkR#Q{sv;Bo#euhoEV&4P(=d0d2;rtcypW=Y`;I#K3?@8t{_%!+L ztYu9#S3Y5UksQKemi|YYOR*|l)JvBxT`&6+eYnD>1)iS6qb)4)D2lu{W8?NKPy7dI zUrPJSy1Qok2l}WM>qXa1tu?%jNKw1?7;>KRI-}WMd5AZMqDZCl zYOveYG6UEypr)KornKKZgwbN<-{MA+F@>$8NTQPLl%5!osndxCX%~5A{{S@}){H&s zT*a}kq5nRcDozJ31P$b(UKbjl?0qv;<)J`5fY>Sy$!8Wkd(%cQQJ0#>0#uiZLau5G z?kjG39?VDUNYhTrb-4Sa0i|n#3}Q9g5AbeR^E8ar!kJ$AO2D{8%Xy0zw^*`dNh}$~ zH~h%K6M`HpMAkr(7X_lvc=8jCQ@-HLU*DSTQ98o~E;h>?pSCdRUl+~5VY~7OGQrXuFTUwT$r2J{lw1t^19@*pLaz2c%#tWXXdX8$xvWKL>Dnw zMLPpp+!JvyVs&dE1#Ntej}hZlI*OTDSx-bXk&|9%ocw&4oZbG)!=Jq7<%4cyWiHpF zRG8t2bOlJ!XYQh%K0el7^g!a{Fcx?KW%CtF#y)72HnSa<^G&|07>6a;12XP$*jiO40dgMmZAC;!YDm7YC& z%0Y~r)6tB%wLdyJIyM0gq1}-d1wh`s*VRege$@sOcM&=fTJBD5lTAorVI1)WO+zzt zb-&vn9D6|9-?eL(tZ#l^*x!{eUTB0{O|Q0?Bc{tRmnvVCUQ4%%9XKp~G@gcB6i$PI z;o+Uorg>8|;#JAohKR7RdB_lgeGNq4R{|gX8V+s|k9K@hwF-GjacF`MqlN`Qlw*>3 z%gBDm$m>?w5^|GgWf>>m&R)v#F8+L|?^U^Ptb4VJ99-5V=cBln5ryU>V`JJNwj|&k zlzbrj-n(8s_{xncxXHJfq6_un`R?u-+(sO0H7IGF+&z%I0!}-YCyN(|&y&vuV?L{d zTQ#R9g_H0mcLBw6pVsuJM&aQUrL^M#xl%uF-m)b)A|gU*r)YNVYKWA&X#+_q)%7^n z+dK!T^zDfxc1fkF2y?0QfK+Mfix^&bgK9f+h1PA_?7`ZxjbfMJkcy^6q#qTAs@WW$ z=la3Bc6C;6khQv2sOX?-d<1 znQVNFB<)RIT^Fugr3ddzAAMrNgb9zIK8@ROq^jT+!Cbk-sh?Ia>cGk+fV_R#7+u1x z^oemd0RN_dV^?m(+kN}5!onMO=i;{gw!wG9hF*R8SVOk%N%f#=257tzeoBiFSJb9P ze)wZ=;qU4i8lCvg$UDe3q}MN5yx6E<88o{E;9!x_4ns!A)7yJ`6%C9Uoj7D1Y+axH z@kcKrHlUdfu_1ptS=-p0DD^xt)Z=G#xQxn<`<;Lty@+DQYW9>v)IHl#mX&lJIA7{U zWgs>qIUhgZ*Kz@Vr4mH=6cPlbTEC^~PD)Ttj&JghC;A#1TK--R)&4w=LYo2t!Yj7s zR@`J77QXhu{rmEhpbm$nqB!=v#?r>d@l-@;Xbvl6EJ;kUBr;cyPPNWYtLQ)jdYq%Gr6 zh&=P6e1Da~d1EW`T|T{Mfe14Qd-LMNvNYGd1SpozN=~n2)2mT;c)qOtN`9i1Gv<nv zuwMQ8O-(TF(R+0l!)m7&z8;SsK8&g>$_|>u zg({(#w7Z78wYZ@4!nh%>f+9I=to!`=M;#yJXljsg1t_6}+GoiplQ>~pE3-~}xh8*f zS&){RdZTOD_Mi=Pc2vy>K-C>DGsr*B^96|@N4`1V!Kw0~xtj8!tm5;rnx^ z`Sd;YD$=TiUJGgr`}K<;w^ZqY7Wx54=S7QRk?fglsl6{E!ZZ1_RKZ)3u6ojCv&z!TQ#Hqf=$v1gBFYy%caDtRQW zR7p8u>OzhVve2N(P_Hq4KP}$3TuhF6i5-h=#hEOvk%Th zw9H@piW5Ahl$Bv)LdH5PE*EP~b4~;A!XrLahDtt^GqK=^tYQ51}TTC{XJIJWQs zy-W>7H2WmQ4)~~+*tyJ@aqXc)ha7$B;nlpGmf#>I0vqPcg|-@;p!pmuDV|Rz3MZob zh}Gthz+S`apo^>uDb4#{IpnV$xO+W;Z@Y!-zs-jmpTT(Q5K8Y3KS2&JBkJ>?u{6Ltp2)tp&b=&gkCqToP8rpM=BdLMZm&si^~%c<-|oe~(Cmgm`GbYNFpS z#7IdF*}R*n`Q1vhGBaJAElRuAf391}`qLji0O#{7*z0*@(Y{maLv|-684Hz#_6vJj zg&S9w*Dc$TY1(2b3hZ)-#b?<*wxn?4GfrP+9K|gnfvq?z9UL8v)E;y0^llhzDMeF2 z*${^{%;prc)vHgP8F9R{QqbMNu^?MxhcV$(^({LDW}Qi}hUEzRrS zz1WVHgLXxq2aRZ?FE?=z=bs%{H>P20?%dj}`=xnv?d*gT zw;4MEq~z3~co}fq(LOxhYm=xVBP5*s_sm1Sw=U#7KB2R!_WZ4!6TfhR#81AT-+1B? z1iAIgXIIoFh5R`JbMSLTMJF762e;)4!{F(4;r6q5xZv7NRF8B6PS z9+6MhI2r)KFrAJk)|I=puKv<$&E+dsvQOSs z_a1fIySD#{N33h{jaEcq@OI)viX17PQvKM>VtY z$21&MSasdSTi(WID=m0=2%y#;%#eC;1TD>=_;NBIC7(gFca?cF!tKxHer#va`xE_I z&io(Za*rQZ$1<;^tnT-xKU46Wg@fw}J+fs`?()GE>8GaMO>)C-2Tu$|AweipE6~I$ zIjU-T&VZM9ZhPsd%qxzb&^9|HuqP~3v(j&k>p8;z%srJfTqnOF^40ovpuSWTRN;;m zpYlMjo{v{kn6cX?H48)HN#sQo0O2X+L*Qa7iF>|1c&0HR@v4XAsvUGZkFObf`yi9N zo_KDxuB~w2OK=GrBV^u#otpOkFqsUP}vyF);y7M1a0h8 z`$ig)GAoEZf!MwbTw*6j)V$MZ)hb_f`=1tdhY|H7A;&Zz5!hjOW5?t9q(M5*K2DG8 z8tPm3U=3sMQ#K9ca{WLS;a z|KyZB5!K&0Yaca_JYUSp7P;eO$=+2I(f47`7~fvLXpugx+GD*w3GX@X)yxi8J)ZM1 zbW^vL5An!;(?VPTAT+3dGYo=OSEpLNO@d7*3^N3qu-SeNlt(n#!}jf4@YkIbF@9H= zX^s^j9cgiYM5B^39+nJ|$8&G|dQS#m#m+`XMls+=Rn3&3kdP2agF#NooeAzAo{niu zC|Lx*u=e}DebLKq6`RW{*3{NEqDW!?EIhNRxvCju)zRN72qf!h6^G>#-sBx3P-tD2E_RwDK6z_x?x_cxmD~o{d z>YO1?$Itg3KD=zjdhM-6>qr@KuW6-Fd~-Jwzp0Q3zze@mZ!9RX?H=R3yhT(a@K!vJ zgPY+=zbOb|Q#eAD@7Se_+lxBtJ*wvKl!f;*J?lDkK#XsjG@RngMGlsgzV`2{Lj`pL zU9{(j5#8uHiP<)GV739i?#q_FrZg4LK7?K-*w}oH;=&9<>qGLm!fMUh2PqyjmL%j0 zCz{$`nk!CDLd+F_F3rau={>j5eUK@U}aKnPaPZ#1X%If?e##3nzz{+^G`85~YL5Ij$v zhfx3$2ZNvOgmDpJVAM-nDyz72ufQ4fC$*NZnJ6z-~5z$}))p*Z4!+ zi;LVHisb#+M&ku1Vi=zg0}u5{Ih3k+oo@zhLw9qnv)$%ZL#af-OG3 zjmUG+Xce^7nDEck#x}tr)34$5+hNUU>XvFqE3{a*9Knj$gkUv)yI1#aK3N1cO!0@k zZ6KNsI*Oe4HGu;A5Ho_LK2+-@*Ls*?euYn&wr*aJdS-6Wp<`+nZ`!u4ji3W*o+?WA zyL)cO+MsCgZ0b)THu6m(pn<&Ubwa$5nujjGpD%R~e?&WJKGnT6)Dq6cSdi4-4BfxK z6)i0>r6YIET=7U%1k^W8^76D6Nph)kIuOnoWnHAfPgm@6FN(HewWdA@zda~u$5yxh nvHo{cYyH2av;P0@lN(#c^!F)STRn5N!pm}kjroaj^EUkl{e$Ql literal 0 HcmV?d00001 diff --git a/mooncake-integration/ollama/docs/figures/stage2_path.dot b/mooncake-integration/ollama/docs/figures/stage2_path.dot new file mode 100644 index 00000000..e4886fda --- /dev/null +++ b/mooncake-integration/ollama/docs/figures/stage2_path.dot @@ -0,0 +1,28 @@ +// Stage-1 (file) vs Stage-2 (on-device) KV export paths. +// Render: dot -Tpng stage2_path.dot -o ../docs/figures/stage2_path.png +digraph stage2 { + rankdir=LR; + bgcolor="white"; + node [fontname="Helvetica", fontsize=11, shape=box, style="rounded,filled"]; + edge [fontname="Helvetica", fontsize=10]; + graph [fontname="Helvetica", labelloc=t, fontsize=14, + label="KV export: Stage-1 file path vs Stage-2 on-device (GPUDirect)"]; + + subgraph cluster_s1 { + label="Stage 1 — /slots file save (4.6x slower, measured)"; + style="rounded,filled"; fillcolor="#fbeef0"; color="#b0413e"; fontcolor="#7a2a2a"; + g1 [label="GPU KV", fillcolor="#ffffff"]; + h1 [label="host copy", fillcolor="#ffffff"]; + f1 [label="serialize + tmpfs file", fillcolor="#ffffff"]; + st1 [label="store", fillcolor="#ffffff"]; + g1 -> h1 -> f1 -> st1; + } + + subgraph cluster_s2 { + label="Stage 2 — ON_DEVICE (host handle only 0.1-0.2 MiB)"; + style="rounded,filled"; fillcolor="#eafaf0"; color="#1a7d32"; fontcolor="#14622a"; + g2 [label="GPU KV\n(stays on device)", fillcolor="#ffffff"]; + st2 [label="store", fillcolor="#ffffff"]; + g2 -> st2 [label="GPUDirect RDMA\n(zero host copy)", color="#1a7d32", penwidth=2, fontcolor="#14622a"]; + } +} diff --git a/mooncake-integration/ollama/docs/figures/stage2_path.png b/mooncake-integration/ollama/docs/figures/stage2_path.png new file mode 100644 index 0000000000000000000000000000000000000000..21e39a1d98727bb2e2672c227d372c02f8dd2e73 GIT binary patch literal 41481 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%0D%K*69k3Ucghq&1S%2 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 +(B) host get_data_ext : 587.8 ms (1.56 GB/s) GPU->host (one copy) +(C) ON_DEVICE export : (stays on device; hand the device buffer to + Mooncake TE registerLocalMemory for GPUDirect + RDMA -- zero host copy, the Stage-2 target) +import set_data_ext : 599.7 ms + +file-save overhead vs raw host copy : 4.26x slower +KV round-trip correctness (seq0==seq1 argmax): PASS -- 2.34.1 From ca8674f51b911adc5d1ce75b45886ef382984c66 Mon Sep 17 00:00:00 2001 From: zbtrs2 Date: Mon, 29 Jun 2026 22:29:34 +0800 Subject: [PATCH 9/9] test(store): normalize allocator test headings --- mooncake-store/tests/offset_allocator_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mooncake-store/tests/offset_allocator_test.cpp b/mooncake-store/tests/offset_allocator_test.cpp index d0e5519e..1cbbab52 100644 --- a/mooncake-store/tests/offset_allocator_test.cpp +++ b/mooncake-store/tests/offset_allocator_test.cpp @@ -865,7 +865,7 @@ TEST_F(OffsetAllocatorTest, RandomSmallAllocWithLargeAllocatorSize) { } } -// ========== EDGE CASE TESTS, Generated by AI ========== +// ========== EDGE CASE TESTS ========== // Test zero size allocation - should fail TEST_F(OffsetAllocatorTest, ZeroSizeAllocation) { @@ -1008,7 +1008,7 @@ TEST_F(OffsetAllocatorTest, PowerOfTwoAllocation) { } } -// ========== BIN SYSTEM TESTS, Generated by AI ========== +// ========== BIN SYSTEM TESTS ========== // Test bin size calculations and selection TEST_F(OffsetAllocatorTest, BinSizeCalculation) { -- 2.34.1