You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A from-scratch, iteration-level scheduler that serves many concurrent sessions through a single shared KV context, giving mesh vLLM/SGLang-class concurrency behavior (continuous batching, chunked-prefill interleave, KV-pressure preemption, zero-copy prefix reuse) without rewriting the KV cache and without porting external scheduler code.
This is the "step 3" workstream from the concurrency analysis (Buzz #general, 2026-08-23/24). It is a clean-sheet serving loop by explicit decision — the existing pieces were built for per-request-behind-a-lock serving and carry those assumptions.
Non-goals (hard boundaries)
Not a KV-cache rewrite. The paged substrate already exists upstream in our pinned llama.cpp (3af988fab) and Skippy already drives it. See "Substrate we build on" below. Re-deriving it is the trap Replace Skippy KV cache with a typed, block-paged cache system #1263 fell into.
Not a port of vLLM/SGLang. We reimplement the policy from their public design and cite it; the code is ours (avoids Apache-2.0 notice drag and their Python/CUDA-runtime assumptions).
Not the disk-KV tier. That is a separate workstream/PR.
Substrate we build on (verified from source at pin 3af988fab)
Unified KV / paging is already reachable: lane_count > 1 enables llama.cpp unified-KV — "every lane shares one n_ctx cell pool" (crates/skippy-runtime/src/config.rs:14-17), wired through to RawRuntimeConfig.lane_count (config.rs:129).
Zero-copy prefix sharing with refcount: cells carry a std::bitset of owning sequences (llama-kv-cells.h:30,6); same-stream seq_cp shares cells with no data copy; seq_rm frees a cell only when seq[i].none() (llama-kv-cells.h:247-266). Exception: ISWA/SWA/hybrid families do not yet share the sliding-window companion cache (if (other) return;, upstream TODO [TAG_KV_CACHE_SHARE_CELLS], llama-kv-cache.h:320-321).
Dependencies (must land under the scheduler; not part of this issue's code)
Remove the prefill serialization.restore_or_record_kv takes .lock() (token_generation.rs:377) and holds it across every .prefill() (:409,419,436,444); it self-reports llama_stage.runtime_lock_acquires=1 (:350). One request's prefill serialises all others.
Raise built-in parallelism.BUILTIN_PARALLEL = 1 (resolver/types.rs:13) and run with lane_count = target concurrency so unified KV is active.
A pristine scheduler over a locked single-lane runtime is still single-lane. These are prerequisites, tracked separately.
Architecture
New component in its own cratecrates/skippy-scheduler/ (decision 2026-08-24, James — deliberately not inside skippy-server, to keep the serving loop independently testable and free of server-crate assumptions), owning a single serving loop per stage runtime:
flowchart LR
R[Incoming requests] --> WQ[WaitingQueue]
WQ -->|admit while KV budget allows| RS[RunningSet]
RS -->|compose one ubatch per step| SB["StepBatch<br/>decode tokens + prefill chunk"]
SB --> DEC["runtime.decode<br/>one mixed ubatch"]
DEC -->|sampled tokens| POST{Per-sequence result}
POST -->|continue| RS
POST -->|EOS / limit| DONE["seq_rm<br/>frees cells; shared prefix survives via refcount"]
RS -->|KV pressure| PRE["Preempt newest /<br/>lowest priority"]
PRE -->|recompute on resume| WQ
Loading
Request: session id, token ids, exact-prefix identity (reuse Skippy's existing identity — do not reinvent), sampling params, assigned seq_id (lane).
WaitingQueue: admitted in order subject to KV budget.
RunningSet: sequences with live KV in the unified context.
StepBatch: the single ubatch built each iteration.
The per-step loop (core)
flowchart TD
S([Step start]) --> A["1. Admit: waiting to running while<br/>KV budget allows; assign seq_id"]
A --> C["2. Compose one ubatch: decode for running +<br/>bounded prefill chunk for admitted, under token budget"]
C --> E["3. Execute: single runtime.decode of the mixed ubatch"]
E --> P[4. Sample per sequence]
P --> D{EOS or limit?}
D -->|yes| RM["seq_rm - free cells"]
D -->|no| K{KV pressure?}
RM --> K
K -->|yes| PRE["5. Preempt newest / lowest priority<br/>recompute on resume"]
K -->|no| N([Next step])
PRE --> N
N --> S
Loading
Each iteration:
Admit: while KV budget (free cells / n_seq_max) allows, move requests waiting→running and assign a seq_id. Reuse the KV-token-budget accounting that already exists in frontend/admission.rs as reference logic.
Compose one ubatch: decode tokens for every running sequence + a bounded prefill chunk for newly admitted sequences, under a fixed per-step token budget (chunked prefill). Harvest logic from frontend/prefill.rs::PrefillChunkSchedule and frontend/decode_batcher.rs, rewritten for multi-request.
Execute: one runtime.decode(ubatch) for the whole step (llama.cpp decodes the mixed ubatch across sequences).
Post: sample per sequence; on EOS/limit, seq_rm the sequence (frees its cells; shared prefix cells survive via refcount).
Preempt under KV pressure: evict the newest/lowest-priority running sequence (recompute-on-resume first; swap-to-disk deferred to the disk tier).
Policies (each pluggable, simple default)
Admission: KV-budget gated, FCFS default.
Ordering/fairness: FCFS default; priority hook.
Prefix sharing: on admission, look up exact-prefix identity; if a live sequence holds the prefix, seq_cp to share cells zero-copy (dense families). SWA companion falls back to recompute until TAG_KV_CACHE_SHARE_CELLS.
Preemption: recompute-newest default.
Telemetry (day one)
Per-step: running count, waiting count, admitted, preempted, tokens (prefill vs decode), KV cells used/free, prefix-share hits/misses. Replace the single runtime_lock_acquires=1 marker with real concurrency metrics.
Test plan
Unit: admission under budget, batch composition (prefill/decode mix within token budget), preemption selection, prefix-share hit path.
Concurrency (the gate): N concurrent sessions; measure throughput/latency vs. the single-lane baseline; assert no request's prefill blocks others.
Correctness: per-family exact-output invariants unchanged — same identity, same bytes, no regression across supported families (this is the line we do not cross for a throughput win).
Loom/stress on the serving loop's admission/preemption transitions.
We carry correctness tests and pinned split topologies for each of the 5 KV-class representatives. Current state (verified in-tree):
crates/skippy-correctness/tests/parity_models.rs already declares all 5 picks — llama, gemma2, deepseek2, falcon_h1, qwen3next — all status: certified in docs/skippy/llama-parity-candidates.json. Each gets three tests: manifest_row_is_complete, activation_handoff_matches_full_model (splits the model into stages and asserts multi-stage handoff == full model), and cache_state_restore_matches_recompute (asserts cache restore == recompute — this already validates the recompute-preemption decision macos menu app #1 per family).
What the concurrency work must add on top:
Concurrent-parity per family. The parity harness above is single-sequence. The new scheduler needs the same exact-output parity to hold with N sessions in flight (mixed prefill/decode, seq_cp prefix-share). Add one concurrent-parity case per family = the correctness pre-gate.
Pinned splits per family. All 5 currently fall back to the harness default 1/3–2/3 layer boundary (splits/split_layer are empty in the manifest). Carry a pinned representative multi-stage split per family so the per-stage admission + min-across-stages ceiling path is exercised deterministically. qwen3next and falcon_h1 are recurrent: all — their splits must respect recurrent-state stage boundaries (KV is a recurrent state, not a plain cell pool), so pin those explicitly rather than trusting the default boundary.
Reuse the existing recompute-restore test as the per-family evidence for decision macos menu app #1 (recompute-newest preemption).
Cutover gate & benchmark methodology
The new engine replaces the old completely and only when it beats the old engine on concurrency and performance across every major KV family class, with zero correctness regression. Old engine = current single-lane locked path; new engine = this scheduler over kv_unified.
What to measure
Primary cutover metric: goodput @ SLO — requests/sec that meet a fixed latency target (TTFT + TPOT below thresholds). It's the honest single number because raw throughput can be inflated by tanking latency.
Supporting metrics:
Throughput — output tokens/sec and requests/sec at fixed concurrency N.
TTFT (time to first token) p50/p99 — prefill responsiveness under queueing.
TPOT / inter-token latency p50/p99 — decode smoothness under batching.
End-to-end request latency p50/p99.
Concurrency scaling curve — sweep N ∈ {1,2,4,8,16,32,64}; plot throughput and p99 latency vs N. Old engine flatlines early (single-lane); new engine should scale. The whole curve is the evidence, not one point.
KV-caching win (the "better performance" claim): prefix-cache hit rate and the TTFT/throughput it buys on shared-prefix workloads (zero-copy seq_cp), plus KV bytes per session (the paging/fragmentation win).
Workloads (benchmarks are meaningless without these)
Prefix shape: shared-prefix (system prompt / few-shot / multi-turn) vs. unique-prefix — isolates the prefix-cache win.
Arrival: open-loop Poisson at target QPS (industry standard for serving) and closed-loop fixed-N (clean scaling curve). Run both.
Family matrix — one representative per KV structural class
Not all 95 families — the KV structure is what varies for this work. All picks are already in the bench roster (evals/skippy-cache-production-bench.py) with GGUFs downloaded:
KV class
Model (roster id)
Why this rep
Dense full-attention (GQA)
Llama-3.2-1B (llama)
bread-and-butter; full zero-copy seq_cp sharing
MLA (latent KV)
DeepSeek-Coder-V2-Lite (deepseek2)
real MLA, small Q4; deepseek3 is layer-package-only/huge — stretch only
ISWA / sliding-window
Gemma-2-2B (gemma2)
canonical alternating SWA/full — hits the TAG_KV_CACHE_SHARE_CELLS sharing boundary
5-model matrix = the scientific minimum, one per KV structural class. GLM-4, OLMo, Gemma-3/4 are skins on one of these classes — add as confirmation runs, not gate rows.
MoE is orthogonal to KV structure — it changes FFN/expert compute, not KV cache layout — so it is not a separate benchmark axis. Coverage comes free via reps that happen to be MoE (DeepSeek, MiniMax); no dedicated MoE row.
Gate (all must hold, per family class)
Concurrency: new ≥ old on goodput/throughput at target N, and the new scaling curve dominates old everywhere — including no regression at N=1 (scheduler overhead negligible for single-user).
Performance / KV: shared-prefix TTFT/throughput improvement demonstrated; KV bytes/session no worse than old.
Correctness (pre-gate, non-negotiable): exact-output parity per family — same identity, same bytes. A faster wrong engine never cuts over; correctness is checked before speed.
Proposed default margins (yours to set): ≥2× throughput at N=32 with equal-or-better p99, and no N=1 regression. Cutover is per-family-class capable — a class that passes can cut over even if another is still being tuned, gated behind a flag.
Harness (decision: in-house is the gate)
Gate harness = extended skippy bench.evals/skippy-cache-production-bench.py today is single-request (--runtime-lane-count 1 --llama-parallel 1). We extend it with a concurrency sweep: fire N parallel OpenAI requests, record per-request timings, derive TTFT / TPOT / throughput / goodput, and sweep N ∈ {1,2,4,8,16,32,64}. It stays in-tree, family-aware, reproducible, no external dependency to vet, and it already owns the correctness + server-side KV-footprint side (via the skippy-cache-family-bench skill and Skippy's own telemetry — KV bytes/session, prefix-share hit/miss counters).
Optional external cross-check = eugr/llama-benchy. OpenAI-compatible, so it can drop a real vLLM / SGLang / llama-server reference number next to ours for context. Not the gate; not on the critical path. If used, smoke-test it against a Skippy endpoint first to confirm it emits per-request timings usable for p99/goodput.
Two methodology items we own regardless of tool: goodput @ SLO (derived from per-request latencies) and open-loop Poisson arrival (the concurrency sweep is closed-loop fixed-N, which covers the scaling curve; add an open-loop driver later only if arrival realism is needed).
Lane sizing & admission (decision 2026-08-24)
Lane count is derived from hardware, not configured — and it is not a static number. The coordinator planner already fits topologies resource-aware (skippy-coordinator/src/topology.rs): a nested search over context length (largest→smallest), node count (fewest→most), then lanes (most→fewest), keeping the subset whose VRAM fits weights + KV + 15% compute reserve. The winner's parallel_lanes → plan.slots → RuntimeConfig.lane_count (runtime/local.rs:834), and lane_count > 1 flips unified KV on.
Key physics: in unified KV the cells are one shared n_ctx allocation indexed by sequence id — the fit calc deliberately does not multiply KV by lanes (topology.rs:525-542; _parallel_lanes is unused, with a comment stating exactly this). So a lane costs almost no memory; it is permission to have another in-flight sequence over the same pool. The real limiter is cells per active session = n_ctx / concurrently-active sessions. Lanes and per-session context length trade against each other inside one fixed pool. Today's planner obscures this by hardcoding MAX_AUTO_PARALLEL_LANES = 4 and sizing context independently.
New design — two levels, not one number:
Derived ceiling (n_seq_max), set at load. Size the shared pool from VRAM (planner already computes this), then ceiling = pool_cells / floor_ctx_per_session. Because lanes are near-free, set it generously. Remove the hardcoded MAX_AUTO_PARALLEL_LANES = 4 cap; derive it.
Dynamic KV-gated admission at runtime. Admit a session only if the shared pool has cells for its actual need now. This is what delivers "good out of the box with as many lanes as possible": a high ceiling plus admission that packs the real pool with whatever mix of long/short prompts arrives — no static knob can get that right.
Multi-stage mesh: each stage has its own KV context sized by its node's VRAM, so the pipeline's effective ceiling is min across stages (a weaker node bounds concurrency). Per-stage scheduler (decision #4) fits cleanly: each stage admits against its own pool, and the min-stage becomes the natural backpressure point.
Open questions — resolved (2026-08-24, James)
Preemption → recompute for the first cut; swap arrives with the disk tier.
SWA/hybrid prefix sharing → carry a small patch, with an in-patch comment stating it tracks upstream TAG_KV_CACHE_SHARE_CELLS so the upstream dependency is explicit and easy to drop when it merges.
Per-step token budget → no static default. The resource is a shared pool, not partitioned; the step token budget is a global draw on it (default n_batch = 1024 in unified mode) and concurrency is admission against the same pool. Resolved by the lane sizing & admission design above — one pool, one budget, two views.
Scheduler → per-stage. Each stage runs its own loop over its own kv_unified context; a cross-stage coordinator is deferred until benchmarks show pipeline bubbles dominate.
Directive: no backwards compatibility (2026-08-24, James)
Cut the old code out; simplify aggressively; leave no back-compat shims. Concretely for this work: unified KV is always on (delete the lane_count == 1 single-lane branch and the dual default_n_batch_for_lane_count path, config.rs:206); lane_count stops being a user/config knob and becomes a derived ceiling; MAX_AUTO_PARALLEL_LANES = 4 is removed. Simpler and more capable.
Implementation tasks
Prerequisite: remove the prefill-serialization lock (token_generation.rs:377 held across .prefill()); raise BUILTIN_PARALLEL / run with lane_count > 1.
Gate harness: extend evals/skippy-cache-production-bench.py with a concurrency sweep (N parallel OpenAI requests, per-request timings → TTFT/TPOT/throughput/goodput, sweep N ∈ {1..64}), old-vs-new engine comparison. In-tree, family-aware. (approved 2026-08-24)
Baseline: run the concurrency sweep on the current engine across the 5-model matrix to capture the old-engine curve before any scheduler work.
Lane sizing: replace hardcoded MAX_AUTO_PARALLEL_LANES = 4 with a pool-derived ceiling (n_seq_max = pool_cells / floor_ctx_per_session); multi-stage ceiling = min across stages. (design 2026-08-24)
Simplification: delete the single-lane branch and dual default_n_batch_for_lane_count path (config.rs:206); unified KV always on; lane_count becomes a derived ceiling, not a config knob. (no-back-compat directive 2026-08-24)
Per-family concurrent-parity: add one N-session exact-output parity case per pick (llama, gemma2, deepseek2, falcon_h1, qwen3next) alongside the existing single-sequence parity tests. (2026-08-24)
Pinned splits: populate splits/split_layer for the 5 picks in docs/skippy/llama-parity-candidates.json (respect recurrent-state boundaries for qwen3next/falcon_h1) so multi-stage tests use deterministic boundaries, not the 1/3–2/3 default. (2026-08-24)
Prefix sharing: wire seq_cp zero-copy reuse on Skippy exact-prefix identity; recompute fallback for SWA companion pending TAG_KV_CACHE_SHARE_CELLS.
Cutover: per-family-class flag flip once the gate passes for that class.
Provenance
Design references: vLLM (continuous batching, PagedAttention, admission/preemption) and SGLang (RadixAttention prefix sharing, chunked prefill). Both Apache-2.0; we reimplement concepts, cite them, and copy no code.
Supersedes the "scheduler" portion of the reshaped #1263 discussion; the KV-substrate and disk-tier workstreams are tracked separately.
Status: design proposal · Date: 2026-08-24 · Owner: TBD
Goal
A from-scratch, iteration-level scheduler that serves many concurrent sessions through a single shared KV context, giving mesh vLLM/SGLang-class concurrency behavior (continuous batching, chunked-prefill interleave, KV-pressure preemption, zero-copy prefix reuse) without rewriting the KV cache and without porting external scheduler code.
This is the "step 3" workstream from the concurrency analysis (Buzz #general, 2026-08-23/24). It is a clean-sheet serving loop by explicit decision — the existing pieces were built for per-request-behind-a-lock serving and carry those assumptions.
Non-goals (hard boundaries)
3af988fab) and Skippy already drives it. See "Substrate we build on" below. Re-deriving it is the trap Replace Skippy KV cache with a typed, block-paged cache system #1263 fell into.Substrate we build on (verified from source at pin
3af988fab)lane_count > 1enables llama.cpp unified-KV — "every lane shares onen_ctxcell pool" (crates/skippy-runtime/src/config.rs:14-17), wired through toRawRuntimeConfig.lane_count(config.rs:129).find_slot(ubatch, cont=false),slot_info::is_contiguous()(llama-kv-cache.h:79,.cpp:842).get_k_idx/set_input_k_idxs(llama-kv-cache.h:209,248).std::bitsetof owning sequences (llama-kv-cells.h:30,6); same-streamseq_cpshares cells with no data copy;seq_rmfrees a cell only whenseq[i].none()(llama-kv-cells.h:247-266). Exception: ISWA/SWA/hybrid families do not yet share the sliding-window companion cache (if (other) return;, upstream TODO[TAG_KV_CACHE_SHARE_CELLS],llama-kv-cache.h:320-321).Dependencies (must land under the scheduler; not part of this issue's code)
restore_or_record_kvtakes.lock()(token_generation.rs:377) and holds it across every.prefill()(:409,419,436,444); it self-reportsllama_stage.runtime_lock_acquires=1(:350). One request's prefill serialises all others.BUILTIN_PARALLEL = 1(resolver/types.rs:13) and run withlane_count = target concurrencyso unified KV is active.A pristine scheduler over a locked single-lane runtime is still single-lane. These are prerequisites, tracked separately.
Architecture
New component in its own crate
crates/skippy-scheduler/(decision 2026-08-24, James — deliberately not insideskippy-server, to keep the serving loop independently testable and free of server-crate assumptions), owning a single serving loop per stage runtime:flowchart LR R[Incoming requests] --> WQ[WaitingQueue] WQ -->|admit while KV budget allows| RS[RunningSet] RS -->|compose one ubatch per step| SB["StepBatch<br/>decode tokens + prefill chunk"] SB --> DEC["runtime.decode<br/>one mixed ubatch"] DEC -->|sampled tokens| POST{Per-sequence result} POST -->|continue| RS POST -->|EOS / limit| DONE["seq_rm<br/>frees cells; shared prefix survives via refcount"] RS -->|KV pressure| PRE["Preempt newest /<br/>lowest priority"] PRE -->|recompute on resume| WQseq_id(lane).The per-step loop (core)
flowchart TD S([Step start]) --> A["1. Admit: waiting to running while<br/>KV budget allows; assign seq_id"] A --> C["2. Compose one ubatch: decode for running +<br/>bounded prefill chunk for admitted, under token budget"] C --> E["3. Execute: single runtime.decode of the mixed ubatch"] E --> P[4. Sample per sequence] P --> D{EOS or limit?} D -->|yes| RM["seq_rm - free cells"] D -->|no| K{KV pressure?} RM --> K K -->|yes| PRE["5. Preempt newest / lowest priority<br/>recompute on resume"] K -->|no| N([Next step]) PRE --> N N --> SEach iteration:
n_seq_max) allows, move requests waiting→running and assign aseq_id. Reuse the KV-token-budget accounting that already exists infrontend/admission.rsas reference logic.frontend/prefill.rs::PrefillChunkScheduleandfrontend/decode_batcher.rs, rewritten for multi-request.runtime.decode(ubatch)for the whole step (llama.cpp decodes the mixed ubatch across sequences).seq_rmthe sequence (frees its cells; shared prefix cells survive via refcount).Policies (each pluggable, simple default)
seq_cpto share cells zero-copy (dense families). SWA companion falls back to recompute untilTAG_KV_CACHE_SHARE_CELLS.Telemetry (day one)
Per-step: running count, waiting count, admitted, preempted, tokens (prefill vs decode), KV cells used/free, prefix-share hits/misses. Replace the single
runtime_lock_acquires=1marker with real concurrency metrics.Test plan
Per-family tests + splits (decision 2026-08-24, James)
We carry correctness tests and pinned split topologies for each of the 5 KV-class representatives. Current state (verified in-tree):
crates/skippy-correctness/tests/parity_models.rsalready declares all 5 picks —llama,gemma2,deepseek2,falcon_h1,qwen3next— allstatus: certifiedindocs/skippy/llama-parity-candidates.json. Each gets three tests:manifest_row_is_complete,activation_handoff_matches_full_model(splits the model into stages and asserts multi-stage handoff == full model), andcache_state_restore_matches_recompute(asserts cache restore == recompute — this already validates the recompute-preemption decision macos menu app #1 per family).What the concurrency work must add on top:
seq_cpprefix-share). Add one concurrent-parity case per family = the correctness pre-gate.splits/split_layerare empty in the manifest). Carry a pinned representative multi-stage split per family so the per-stage admission +min-across-stages ceiling path is exercised deterministically.qwen3nextandfalcon_h1arerecurrent: all— their splits must respect recurrent-state stage boundaries (KV is a recurrent state, not a plain cell pool), so pin those explicitly rather than trusting the default boundary.Cutover gate & benchmark methodology
The new engine replaces the old completely and only when it beats the old engine on concurrency and performance across every major KV family class, with zero correctness regression. Old engine = current single-lane locked path; new engine = this scheduler over
kv_unified.What to measure
Primary cutover metric: goodput @ SLO — requests/sec that meet a fixed latency target (TTFT + TPOT below thresholds). It's the honest single number because raw throughput can be inflated by tanking latency.
Supporting metrics:
seq_cp), plus KV bytes per session (the paging/fragmentation win).Workloads (benchmarks are meaningless without these)
Family matrix — one representative per KV structural class
Not all 95 families — the KV structure is what varies for this work. All picks are already in the bench roster (
evals/skippy-cache-production-bench.py) with GGUFs downloaded:llama)seq_cpsharingdeepseek2)deepseek3is layer-package-only/huge — stretch onlygemma2)TAG_KV_CACHE_SHARE_CELLSsharing boundaryfalcon_h1)llama-memory-recurrent+memory-hybridqwen3next) or MiniMax M2.7 (minimax_m27)5-model matrix = the scientific minimum, one per KV structural class. GLM-4, OLMo, Gemma-3/4 are skins on one of these classes — add as confirmation runs, not gate rows.
MoE is orthogonal to KV structure — it changes FFN/expert compute, not KV cache layout — so it is not a separate benchmark axis. Coverage comes free via reps that happen to be MoE (DeepSeek, MiniMax); no dedicated MoE row.
Gate (all must hold, per family class)
Proposed default margins (yours to set): ≥2× throughput at N=32 with equal-or-better p99, and no N=1 regression. Cutover is per-family-class capable — a class that passes can cut over even if another is still being tuned, gated behind a flag.
Harness (decision: in-house is the gate)
Gate harness = extended skippy bench.
evals/skippy-cache-production-bench.pytoday is single-request (--runtime-lane-count 1 --llama-parallel 1). We extend it with a concurrency sweep: fire N parallel OpenAI requests, record per-request timings, derive TTFT / TPOT / throughput / goodput, and sweep N ∈ {1,2,4,8,16,32,64}. It stays in-tree, family-aware, reproducible, no external dependency to vet, and it already owns the correctness + server-side KV-footprint side (via theskippy-cache-family-benchskill and Skippy's own telemetry — KV bytes/session, prefix-share hit/miss counters).Optional external cross-check =
eugr/llama-benchy. OpenAI-compatible, so it can drop a real vLLM / SGLang / llama-server reference number next to ours for context. Not the gate; not on the critical path. If used, smoke-test it against a Skippy endpoint first to confirm it emits per-request timings usable for p99/goodput.Two methodology items we own regardless of tool: goodput @ SLO (derived from per-request latencies) and open-loop Poisson arrival (the concurrency sweep is closed-loop fixed-N, which covers the scaling curve; add an open-loop driver later only if arrival realism is needed).
Lane sizing & admission (decision 2026-08-24)
Lane count is derived from hardware, not configured — and it is not a static number. The coordinator planner already fits topologies resource-aware (
skippy-coordinator/src/topology.rs): a nested search over context length (largest→smallest), node count (fewest→most), then lanes (most→fewest), keeping the subset whose VRAM fitsweights + KV + 15% compute reserve. The winner'sparallel_lanes→plan.slots→RuntimeConfig.lane_count(runtime/local.rs:834), andlane_count > 1flips unified KV on.Key physics: in unified KV the cells are one shared
n_ctxallocation indexed by sequence id — the fit calc deliberately does not multiply KV by lanes (topology.rs:525-542;_parallel_lanesis unused, with a comment stating exactly this). So a lane costs almost no memory; it is permission to have another in-flight sequence over the same pool. The real limiter is cells per active session =n_ctx/ concurrently-active sessions. Lanes and per-session context length trade against each other inside one fixed pool. Today's planner obscures this by hardcodingMAX_AUTO_PARALLEL_LANES = 4and sizing context independently.New design — two levels, not one number:
n_seq_max), set at load. Size the shared pool from VRAM (planner already computes this), thenceiling = pool_cells / floor_ctx_per_session. Because lanes are near-free, set it generously. Remove the hardcodedMAX_AUTO_PARALLEL_LANES = 4cap; derive it.Multi-stage mesh: each stage has its own KV context sized by its node's VRAM, so the pipeline's effective ceiling is
minacross stages (a weaker node bounds concurrency). Per-stage scheduler (decision #4) fits cleanly: each stage admits against its own pool, and the min-stage becomes the natural backpressure point.Open questions — resolved (2026-08-24, James)
TAG_KV_CACHE_SHARE_CELLSso the upstream dependency is explicit and easy to drop when it merges.n_batch= 1024 in unified mode) and concurrency is admission against the same pool. Resolved by the lane sizing & admission design above — one pool, one budget, two views.kv_unifiedcontext; a cross-stage coordinator is deferred until benchmarks show pipeline bubbles dominate.Directive: no backwards compatibility (2026-08-24, James)
Cut the old code out; simplify aggressively; leave no back-compat shims. Concretely for this work: unified KV is always on (delete the
lane_count == 1single-lane branch and the dualdefault_n_batch_for_lane_countpath,config.rs:206);lane_countstops being a user/config knob and becomes a derived ceiling;MAX_AUTO_PARALLEL_LANES = 4is removed. Simpler and more capable.Implementation tasks
token_generation.rs:377held across.prefill()); raiseBUILTIN_PARALLEL/ run withlane_count > 1.evals/skippy-cache-production-bench.pywith a concurrency sweep (N parallel OpenAI requests, per-request timings → TTFT/TPOT/throughput/goodput, sweep N ∈ {1..64}), old-vs-new engine comparison. In-tree, family-aware. (approved 2026-08-24)MAX_AUTO_PARALLEL_LANES = 4with a pool-derived ceiling (n_seq_max = pool_cells / floor_ctx_per_session); multi-stage ceiling =minacross stages. (design 2026-08-24)default_n_batch_for_lane_countpath (config.rs:206); unified KV always on;lane_countbecomes a derived ceiling, not a config knob. (no-back-compat directive 2026-08-24)crates/skippy-scheduler/(not insideskippy-server) — per-step loop (admit → compose ubatch → decode → sample → seq_rm/preempt), drivingkv_unified. (own-crate decision 2026-08-24)splits/split_layerfor the 5 picks indocs/skippy/llama-parity-candidates.json(respect recurrent-state boundaries forqwen3next/falcon_h1) so multi-stage tests use deterministic boundaries, not the 1/3–2/3 default. (2026-08-24)seq_cpzero-copy reuse on Skippy exact-prefix identity; recompute fallback for SWA companion pendingTAG_KV_CACHE_SHARE_CELLS.runtime_lock_acquires=1marker.Provenance
Design references: vLLM (continuous batching, PagedAttention, admission/preemption) and SGLang (RadixAttention prefix sharing, chunked prefill). Both Apache-2.0; we reimplement concepts, cite them, and copy no code.
Supersedes the "scheduler" portion of the reshaped #1263 discussion; the KV-substrate and disk-tier workstreams are tracked separately.