Skip to content

🚬 Add bounded MTP + N-gram pipelining for latency-limited split inference - #1026

Merged
michaelneale merged 22 commits into
mainfrom
agent/positional-mtp-ngram
Jul 22, 2026
Merged

🚬 Add bounded MTP + N-gram pipelining for latency-limited split inference#1026
michaelneale merged 22 commits into
mainfrom
agent/positional-mtp-ngram

Conversation

@i386

@i386 i386 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

What changed

This replaces the earlier repair-oriented experiment with a deliberately breaking, Shard-shaped stage protocol for native GLM-4.7 MTP + request-local N-gram speculation. There is no standalone draft model or draft-model experiment in this PR.

  • stage protocol v10 makes stage zero the sole acceptance authority;
  • full, contiguous multi-token verification chunks use authoritative absolute positions;
  • a fixed per-request FIFO stays filled to verify_window_pipeline_depth;
  • stage forwarding is asynchronous and the tail returns replies directly to the coordinator;
  • replies retire in FIFO order; divergence discards dependent work and immediately starts a new epoch;
  • fully accepted chunks advance KV/MTP state monotonically without accepted-path rewind or replay;
  • pipeline prefill and request-local N-gram horizon refill keep work available;
  • follower stages inherit their node-local telemetry policy, and stage zero now emits joinable target-compute spans;
  • the old MTP checkpoint ring, repair/replay messages, adaptive depth policy, probation/promotion/slow-start controller, global speculative-credit pool, and their configuration/telemetry were deleted.

The public mesh and OpenAI protocols are unchanged. The internal stage protocol is intentionally incompatible; all stages in a split must run v10.

Canonical benchmark gate: NVIDIA SPEED-Bench under a locked topology

The tables below are controlled diagnostic microbenchmarks, not the final headline benchmark. Before this is promoted or merged, the performance claim will be rerun with the full upstream NVIDIA SPEED-Bench qualitative split (all 880 samples and all categories, OSL 1024, temperature 0) through the OpenAI-compatible endpoint.

Both A/B arms will use the locked-topology implementation merged from #1050 / jd/locked-split-topology:

  • immutable GLM-4.7 package revision and model-package.json SHA-256;
  • identical lock bytes and lock SHA-256 on both hosts;
  • two immutable topology files with exact node order and contiguous ranges:
    • primary balanced topology: micstudio.local:0..25 then studio54-3.local:25..48 (25/23);
    • asymmetric latency stress: micstudio.local:0..47 then studio54-3.local:47..48 (47/1);
  • --split-topology-lock passed to both nodes;
  • realized topology verified through /api/runtime/stages before traffic starts;
  • fail closed on package, participant, capacity, range, or stage-loss mismatch.

The only serving-policy difference between matched runs will be native MTP versus native MTP + request-local N-gram width 4 / fixed depth 2. The locked 25/23 split is the primary production-like comparison; the locked 47/1 split is explicitly an asymmetric latency-hiding stress case, not the headline topology. Full qualitative runs use request concurrency 1 at natural LAN latency and at 100 ms injected stage latency. Concurrency 8 is a separate saturation run on the balanced split. The report will include per-category and overall decode TPS, end-to-end latency, TTFT, ITL, acceptance, in-flight occupancy, stale work, and joined stage-compute overlap. The SPEED-Bench throughput split will separately test low-, mixed-, and high-entropy load.

Current diagnostic result

Two-node GLM-4.7 Flash MTP Q4_K_M split, 47/1 layers, eight lanes, F16 activations, 32K context, 100 ms injected inter-stage delay. The selected setting is N-gram 4, verification width 4, fixed depth 2.

Mixed corpus, c1 Native MTP MTP + N-gram, depth 2 Change
Model-reported decode TPS 9.3152 10.7097 +14.97%
Aggregate wall TPS 7.2815 8.0852 +11.04%
Draft acceptance 77.35% 38.51% -38.84 pp
Exact output hashes 4/4 4/4 equal

This is the result we set out to prove: MTP + N-gram beats native MTP even though its acceptance rate is substantially lower. Acceptance is not throughput; overlapping useful target work can outweigh rejected proposals.

On a separate four-prompt novel-code corpus, selected depth 2 was essentially neutral: 8.7018 versus 8.7921 decode TPS (-1.03%), with 4/4 exact hashes and 32.8% acceptance. The win is therefore workload-dependent rather than a universal claim.

Concurrency

Same mixed corpus and selected depth-2 configuration:

Request concurrency Native MTP wall TPS MTP + N-gram wall TPS Change Hybrid acceptance
1 7.2815 8.0852 +11.04% 38.51%
2 11.6076 15.1921 +30.88% 35.19%
4 18.4284 22.8768 +24.14% 33.90%
8 27.0188 26.1993 -3.03% 31.42%

Depth is intentionally fixed per request. At c8, depth 2 can place 16 windows against eight lanes and slightly overfill the service. This is the current operating-envelope limit; the deleted global-credit/adaptive machinery is not being smuggled back in. Production policy should choose depth with total admitted concurrency in mind or batch callers into shared pipeline jobs as Shard does.

Does speculation fill the pipeline?

Yes, on eligible repeated spans. At depth 2 and 100 ms injected delay:

Occupancy / latency signal Result
Time-weighted average windows in flight 1.92 / 2
Time at full configured depth 94.3%
Stale windows on the fully accepted repeat case 0
Verify elapsed accumulated across windows 15.664 s
End-to-end decode interval 8.242 s
Mean streaming ITL, native MTP 83.10 ms
Mean streaming ITL, hybrid 22.24 ms
Mean ITL reduction 73.2%
p95 ITL, native MTP / hybrid 169.29 / 127.92 ms
Sub-millisecond token intervals, native MTP / hybrid 49.87% / 74.67%
TTFT, native MTP / hybrid 5.295 / 5.309 s

The queue is not merely configured to two; it is measurably full for almost the entire eligible interval. Accumulated verification time exceeding wall decode time and the burstier ITL distribution show overlapped in-flight target work. TTFT is unchanged because this optimization starts after prefill.

How much are the stages actually computing in parallel?

Debug traces now join the stage-0 and stage-1 target-compute spans by run, request, session, and window. Each trace matched all 97 windows on both stages; every simultaneous-compute pair was from different windows, which is the pipeline behavior we wanted to observe.

Split Verify interval Stage 0 busy Stage 1 busy Both stages busy Share of S0 / S1 compute overlapped Different-window overlaps
47/1 8.284 s 3.351 s (40.5%) 1.551 s (18.7%) 0.516 s (6.23%) 15.4% / 33.3% 43
25/23 11.586 s 1.953 s (16.9%) 8.268 s (71.4%) 0.520 s (4.49%) 26.6% / 6.3% 117

So the precise answer to the review question is nuanced: the inference FIFO is full and hides stage/network latency, but both stage runtimes are simultaneously computing for only 4.5–6.2% of the verify interval at depth 2. The 47/1 tail is intentionally tiny; in the 25/23 split, the slower stage 1 is busy 71.4% of the interval while stage 0 works ahead in shorter bursts. The gain comes from keeping the bottleneck supplied, overlapping different windows, hiding the ~100 ms edge delay (median same-window stage-0-end to stage-1-start was 113–117 ms), and amortizing traversals with wider chunks—not from both GPUs being continuously busy together.

These are host-measured synchronous stage-compute spans, not Metal hardware-counter samples. They measure when each stage runtime is executing target work; they do not claim cycle-level shader occupancy.

Network-latency sweep

Highly repeatable 384-token workload, 47/1 split, width 4, depth 2; exact output in every row:

Injected delay Native MTP decode TPS Hybrid decode TPS Change Hybrid avg/full occupancy
0 ms 39.8221 103.9520 +161.0% 1.90 / 93%
20 ms 26.3357 97.1255 +268.8% 1.92 / 95%
50 ms 18.5264 68.0052 +267.1% 1.93 / 96%
100 ms 12.2195 46.5880 +281.3% 1.92 / 94%

The delay between stages is therefore substantially hidden when the request-local horizon can keep the FIFO supplied. It is not eliminated: TTFT/prefill remain serial, divergence wastes later work, and low-reuse workloads cannot sustain the horizon.

Depth and width frontier

The same 100 ms repeat workload establishes the upper bound and where added depth stops buying much throughput:

Fixed depth Decode TPS Wall TPS Exact Acceptance
Native MTP 12.2195 10.5183 yes 100%
1 28.1010 20.5081 yes 100%
2 46.8637 28.9054 yes 100%
4 91.6338 41.2619 yes 100%
8 105.5877 43.9471 yes 100%
16 110.2350 44.4998 yes 100%

Throughput approaches the bottleneck service rate around depth 8; depth 16 adds only 4.4%. Depth 2 is selected for mixed traffic because it captures overlap without the concurrency/stale-work exposure of a deep per-request queue.

At depth 2, the width sweep separates pipeline overlap from wider target chunks:

Verification width Decode TPS Wall TPS Avg/full occupancy
1 13.4691 11.4329 1.97 / 98.2%
2 25.3917 19.0012 1.94 / 95.9%
4 46.5880 28.9208 1.92 / 94.3%

Width 1 is already +10.2% over native MTP at 100 ms, showing that overlap alone helps. Full chunks provide the larger gain by amortizing each traversal across more verified tokens.

Topology

The effect is not limited to the asymmetric 47/1 placement:

Split Native MTP decode TPS Hybrid decode TPS Change
47/1 12.2195 46.5880 +281.3%
25/23 10.0907 33.3145 +230.1%

Both rows use the 100 ms repeat workload, width 4, depth 2, and exact output comparison.

Comparison with the previous runs

These are separate run families, so the percentage deltas are the honest comparison—not the absolute TPS values.

Run family c1 c2 c4 c8 Design
PR #938 reported hybrid uplift +5.57% Earlier MTP/N-gram experiment; different run shape
PR #938 frozen-control uplift +11.87% Earlier control rerun; different run shape
Previous #1026 v9 +27.07% +13.22% +0.74% +0.23% Overlapping one-position windows, checkpoint repair, adaptive/credit controls
This v10, selected mixed run +11.04% wall / +14.97% decode +30.88% +24.14% -3.03% Full chunks, fixed-depth FIFO, contiguous positions, no accepted-path repair

The new path trades the old c1 peak for much stronger c2/c4 scaling and a far simpler execution model. It also exposes the real c8 overfill limit rather than obscuring it with a global credit controller.

How similar is this to Shard / issue #1025?

This now copies Shard's important execution pattern much more directly:

  • explicit fixed depth rather than an inferred magic concurrency of two;
  • prefill the depth, retire FIFO, and refill immediately;
  • independent in-flight target traversals at absolute positions;
  • asynchronous stage forwarding and direct final-stage return;
  • invalidate later work after divergence instead of repairing accepted work;
  • choose the shallowest depth that covers the pipeline service latency.

The important differences are:

  • Shard has an independent draft actor; this path deliberately uses only native GLM-4.7 MTP plus a request-local exact N-gram continuation;
  • Shard's gateway batches concurrent callers into shared ring jobs; this implementation still assigns a fixed FIFO depth per admitted request;
  • Shard also requires an explicit depth—it does not discover an optimal per-request depth automatically.

That answers the central questions in #1025: parallel speculation can hide stage/network latency when the candidate horizon stays supplied; depth must cover the pipeline rather than be hard-coded to concurrency two; and production concurrency still needs shared batching/admission so independent per-request queues do not overfill the lanes.

Validation

  • cargo fmt --all --check
  • cargo test -p skippy-protocol --lib — 43 passed
  • cargo test -p skippy-server --lib — 281 passed
  • cargo test -p mesh-llm-config --lib — 95 passed
  • cargo test -p mesh-llm-host-runtime --lib — 1,695 passed, 8 ignored
  • warning-denying Clippy across all touched crates and mesh-llm
  • MESH_LLM_DYNAMIC_NATIVE_RUNTIME=0 just release-build
  • merged jd/locked-split-topology / Lock split topology placement #1050 into this branch;
  • clean patch-queue application at llama.cpp 178a6c44937154dc4c4eff0d166f4a044c4fceba;
  • cargo test -p skippy-coordinator --lib — 37 passed;
  • cargo test -p mesh-llm-cli --lib — 72 passed;
  • cargo test -p mesh-llm-host-runtime --lib split_topology — 10 passed;
  • cargo check -p mesh-llm and warning-denying Clippy across the topology-touched crates;
  • two-host GLM-4.7 split output-hash comparisons across mixed, code, repeat, depth, width, delay, topology, and concurrency sweeps

Production limitations

  • Depth is fixed per request; c8 shows that total depth across callers must be bounded or callers must be coalesced into shared jobs.
  • Stage overlap is measured from synchronous runtime spans, not GPU performance counters; hardware-counter validation remains separate.
  • The large latency/depth gains are an upper bound on highly reusable text; novel code is neutral and arbitrary low-reuse traffic may not form a deep horizon.
  • Divergence correctness is covered by the coordinator/stale-discard path, but a longer adversarial rejection/stop/tool-call soak is still warranted before making this a default package policy.

Addresses the implementation questions raised in #1025; package-default promotion should remain a separate production decision.

michaelneale and others added 2 commits July 20, 2026 15:17
The adaptive verify window was never enabled on the split-serving path:
to_embedded_openai_args hardcoded adaptive_speculative_window = false. With a
fixed window, an early reject never shrank the window, so a sustained reject
storm kept proposing at full depth and paying the full 2-round-trip recovery
cost per token. On a WAN split this measured as ~40% throughput loss with
N-gram speculation ON versus OFF, despite high per-token acceptance.

Enable the adaptive window whenever speculation actually proposes a window
(ngram or draft mode). The existing shrink_adaptive_window logic then narrows
the window toward the observed accept depth after an early reject, cutting
recovery frequency. Adds a regression test asserting ngram speculation turns
the adaptive window on.
@github-actions

Copy link
Copy Markdown
Contributor

This pull request is currently a draft. Reviews will not take place until the PR is marked as ready for review.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR replaces standalone N-gram and checkpoint-based speculative flows with native-MTP cache handling, stage-state v9 authoritative positions, stale-window scheduling, updated ABI bindings, and revised CLI, configuration, tests, fixtures, scripts, and documentation.

Changes

Speculative decoding and protocol

Layer / File(s) Summary
CLI and configuration contracts
crates/mesh-llm-cli/..., crates/mesh-llm-config/..., crates/mesh-llm-commands/...
Renames tuning to mtp-ngram, removes proposer selection, and validates N-gram settings as native-MTP extensions.
Runtime resolution
crates/mesh-llm-host-runtime/..., crates/skippy-model-package/...
Resolves N-gram settings through native-MTP composite plans and removes standalone N-gram mode resolution.
Prompt and server execution
crates/skippy-prompt/..., crates/skippy-server/src/frontend/...
Uses draft proposals and cached MTP extensions while removing checkpoint, repair, restore, and trim-replay paths.
Stage-state protocol and transport
crates/skippy-protocol/..., crates/skippy-runtime/..., crates/skippy-server/src/binary_transport/...
Advances stage state to v9, carries authoritative positions, removes checkpoint/restore messages, supports tracked forwarding, and reduces reply-stat fields.
ABI and supporting surfaces
crates/skippy-ffi/..., third_party/llama.cpp/..., docs/..., scripts/..., crates/mesh-llm-ui/...
Adds request-local cache ABI support, removes legacy ABI symbols, and updates documentation, fixtures, UI tests, and certification manifests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Suggested labels: experimental

Suggested reviewers: ndizazzo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: bounded MTP plus N-gram pipelining for split inference.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/positional-mtp-ngram

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/skippy-protocol/src/binary/mod.rs (1)

315-338: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test doesn't isolate the kind-exclusion branch.

The third assertion changes kind to PrefillEmbd but leaves pos_start at -1 (set in the prior step), so it's still exercising the sign check, not proving that a non-decode kind returns None even with a valid position.

♻️ Proposed fix
         decode.pos_start = -1;
         assert_eq!(decode.authoritative_session_position(), None);
 
+        decode.pos_start = 17;
         decode.kind = WireMessageKind::PrefillEmbd;
         assert_eq!(decode.authoritative_session_position(), None);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/skippy-protocol/src/binary/mod.rs` around lines 315 - 338, Update the
test only_decode_messages_carry_authoritative_session_positions so the
non-decode kind assertion restores pos_start to a valid non-negative value
before changing kind to PrefillEmbd. Keep the existing negative-position
assertion separate, ensuring the final assertion exercises kind exclusion rather
than the sign check.
crates/skippy-server/src/frontend/speculative.rs (1)

647-647: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove redundant variable shadowing.

The outer commit_count variable is already accurately incremented at the start of the loop, meaning it is exactly equal to accepted_before_reject + 1 when the first rejection occurs. You can safely remove this let binding.

♻️ Proposed refactor
-        let commit_count = accepted_before_reject + 1;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/skippy-server/src/frontend/speculative.rs` at line 647, Remove the
inner commit_count binding at the first-rejection path and reuse the outer
commit_count value already incremented by the loop. Preserve the existing
rejection handling and all subsequent uses of commit_count.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/mesh-llm-host-runtime/src/inference/skippy/resolver/speculative.rs`:
- Around line 437-440: Update the effective_strategy selection to use an if
expression based on ngram.is_some() instead of matching on a boolean, preserving
the existing native-mtp+ngram-cache and native-mtp values.

---

Nitpick comments:
In `@crates/skippy-protocol/src/binary/mod.rs`:
- Around line 315-338: Update the test
only_decode_messages_carry_authoritative_session_positions so the non-decode
kind assertion restores pos_start to a valid non-negative value before changing
kind to PrefillEmbd. Keep the existing negative-position assertion separate,
ensuring the final assertion exercises kind exclusion rather than the sign
check.

In `@crates/skippy-server/src/frontend/speculative.rs`:
- Line 647: Remove the inner commit_count binding at the first-rejection path
and reuse the outer commit_count value already incremented by the loop. Preserve
the existing rejection handling and all subsequent uses of commit_count.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: be4c5a25-81f6-4a55-ba9f-0ab46d2bd24a

📥 Commits

Reviewing files that changed from the base of the PR and between f73ab38 and 141ef39.

📒 Files selected for processing (87)
  • SKIPPY_PROTOCOL_TODO.md
  • crates/mesh-llm-cli/src/benchmark.rs
  • crates/mesh-llm-cli/src/lib.rs
  • crates/mesh-llm-cli/src/parser.rs
  • crates/mesh-llm-cli/src/parser/commands.rs
  • crates/mesh-llm-commands/src/gpus/tune/benchmark/candidates.rs
  • crates/mesh-llm-commands/src/gpus/tune/benchmark/tests.rs
  • crates/mesh-llm-commands/src/gpus/tune/benchmark/trial_config.rs
  • crates/mesh-llm-commands/src/gpus/tune/output_types.rs
  • crates/mesh-llm-commands/src/gpus/tune/output_values.rs
  • crates/mesh-llm-config/src/model.rs
  • crates/mesh-llm-config/src/model/built_in_schema.rs
  • crates/mesh-llm-config/src/model/built_in_schema/control_behavior/speculative.rs
  • crates/mesh-llm-config/src/model_validation.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/mod.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/native_mtp_tests.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/speculative.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/tests.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/translation.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/types.rs
  • crates/mesh-llm-host-runtime/src/plugin/config.rs
  • crates/mesh-llm-host-runtime/src/runtime/local/native_runtime_events/tests.rs
  • crates/mesh-llm-host-runtime/tests/fixtures/config_schema_defaults_ui_reference.json
  • crates/mesh-llm-host-runtime/tests/fixtures/schema_driven_controls_valid.toml
  • crates/mesh-llm-ui/src/features/configuration/api/config-adapter.test.ts
  • crates/mesh-llm-ui/src/features/configuration/components/DefaultsTab.test.tsx
  • crates/mesh-llm-ui/src/features/configuration/lib/build-toml.test.ts
  • crates/mesh-llm-ui/src/features/configuration/pages/ConfigurationPage.test.tsx
  • crates/mesh-llm/src/lib.rs
  • crates/skippy-ffi/README.md
  • crates/skippy-ffi/src/lib.rs
  • crates/skippy-model-package/src/preflight.rs
  • crates/skippy-prompt/src/prompt_cli/args.rs
  • crates/skippy-prompt/src/prompt_cli/binary_repl.rs
  • crates/skippy-prompt/src/prompt_cli/draft.rs
  • crates/skippy-prompt/src/prompt_cli/generation.rs
  • crates/skippy-prompt/src/prompt_cli/launch.rs
  • crates/skippy-prompt/src/prompt_cli/mod.rs
  • crates/skippy-prompt/src/prompt_cli/speculative.rs
  • crates/skippy-prompt/src/prompt_cli/tests.rs
  • crates/skippy-prompt/src/prompt_cli/topology.rs
  • crates/skippy-prompt/src/prompt_cli/wire_messages.rs
  • crates/skippy-protocol/src/binary/codec.rs
  • crates/skippy-protocol/src/binary/mod.rs
  • crates/skippy-protocol/src/binary/types.rs
  • crates/skippy-runtime/src/lib.rs
  • crates/skippy-runtime/src/ngram.rs
  • crates/skippy-runtime/src/session.rs
  • crates/skippy-server/README.md
  • crates/skippy-server/src/binary_transport/binary_messaging.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/connection.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/telemetry.rs
  • crates/skippy-server/src/binary_transport/options.rs
  • crates/skippy-server/src/binary_transport/stage_execution.rs
  • crates/skippy-server/src/cli.rs
  • crates/skippy-server/src/frontend.rs
  • crates/skippy-server/src/frontend/backend.rs
  • crates/skippy-server/src/frontend/embedded_execution.rs
  • crates/skippy-server/src/frontend/embedded_generation.rs
  • crates/skippy-server/src/frontend/generation/server.rs
  • crates/skippy-server/src/frontend/generation/types.rs
  • crates/skippy-server/src/frontend/generation_flow.rs
  • crates/skippy-server/src/frontend/native_mtp/decode.rs
  • crates/skippy-server/src/frontend/native_mtp/hybrid.rs
  • crates/skippy-server/src/frontend/native_mtp/mod.rs
  • crates/skippy-server/src/frontend/native_mtp/verify_window.rs
  • crates/skippy-server/src/frontend/speculative.rs
  • crates/skippy-server/src/frontend/tests/multimodal.rs
  • crates/skippy-server/src/frontend/tests/prompting.rs
  • crates/skippy-server/src/frontend/wire_messages.rs
  • crates/skippy-server/src/kv_integration/config.rs
  • crates/skippy-server/src/kv_integration/mod.rs
  • crates/skippy-server/src/lib.rs
  • crates/skippy-server/src/runtime_state.rs
  • docs/CLI.md
  • docs/USAGE.md
  • docs/design/TESTING.md
  • docs/skippy/CONFIGURATION.md
  • docs/skippy/PIPELINED_VERIFY_WINDOW.md
  • docs/skippy/WAN_SPLIT_PERF.md
  • docs/skippy/speculative_decoding.md
  • docs/specs/layer-package-repos.md
  • docs/specs/speculative-decoding-wiring-plan.md
  • scripts/family-certify.sh
  • third_party/llama.cpp/patches/0017-Expose-upstream-ngram-simple-draft-ABI.patch
  • third_party/llama.cpp/patches/0018-Expose-stateful-N-gram-cache-ABI.patch
  • third_party/llama.cpp/patches/0019-Remove-legacy-session-checkpoint-ABI.patch
💤 Files with no reviewable changes (22)
  • docs/skippy/speculative_decoding.md
  • docs/specs/speculative-decoding-wiring-plan.md
  • crates/skippy-prompt/src/prompt_cli/draft.rs
  • crates/mesh-llm-host-runtime/tests/fixtures/config_schema_defaults_ui_reference.json
  • crates/skippy-server/src/frontend/backend.rs
  • crates/skippy-prompt/src/prompt_cli/topology.rs
  • crates/skippy-server/src/frontend/generation_flow.rs
  • crates/skippy-prompt/src/prompt_cli/launch.rs
  • third_party/llama.cpp/patches/0017-Expose-upstream-ngram-simple-draft-ABI.patch
  • crates/mesh-llm/src/lib.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/mod.rs
  • crates/mesh-llm-host-runtime/tests/fixtures/schema_driven_controls_valid.toml
  • crates/skippy-server/src/frontend/tests/multimodal.rs
  • crates/skippy-server/src/binary_transport/binary_messaging.rs
  • crates/skippy-prompt/src/prompt_cli/binary_repl.rs
  • crates/skippy-server/src/frontend/generation/types.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/types.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/telemetry.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/tests.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/native_mtp_tests.rs
  • crates/skippy-server/src/frontend/wire_messages.rs
  • crates/skippy-prompt/src/prompt_cli/args.rs

Comment thread crates/mesh-llm-host-runtime/src/inference/skippy/resolver/speculative.rs Outdated
@i386

i386 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

@michaelneale Follow-up plan for answering the parallel-busy-time / WAN-latency question and bringing this closer to Shard.

The positional protocol in this PR is the prerequisite: verify windows carry authoritative positions, stages repair KV locally, and rejection no longer requires checkpoint/restore ACKs or repair/reverify. The remaining work is to turn the current depth-2 burst into a continuously refilled pipeline and prove that it hides latency.

A useful sizing condition is:

required_depth >= ceil(end-to-end verify latency / verify-window launch interval)

Once that many independent windows are circulating, propagation delay becomes pipeline fill rather than an inter-token stall; throughput approaches the slowest stage/link service rate. Initial fill, rejection recovery, and link serialization still remain visible.

Implementation plan:

  1. Remove remaining serialization

    • Replace the 10 ms direct-return polling loop with a blocking, timeout-aware receive.
    • Extend bounded asynchronous activation forwarding from prefill to VerifyWindow traffic at stage 0 and intermediate stages.
    • Measure send backpressure and size flow-control capacity for depth × activation_frame_bytes.
  2. Use a continuously refilled credit window

    • Fill all available credits, consume the oldest reply, then immediately refill the freed credit.
    • Tag windows with a speculation epoch and absolute position.
    • On divergence, mark later windows stale and begin the corrected branch as credits reopen; do not stop to synchronously drain the entire stale queue.
  3. Generate enough speculative future

    • Keep Ngram order as the matching-quality control, but decouple it from speculative horizon.
    • Use MTP for the high-confidence prefix and recursively extend the speculative prefix with the stateful Ngram cache until the target pipeline depth or stale-work budget is reached.
    • This supplies the continuous cheap draft stream needed for depths 4/8.
  4. Make depth latency-driven

    • Estimate target_depth = ceil(EWMA(verify_latency) / EWMA(launch_interval)) + safety_credit, clamped to an operator maximum.
    • Ramp 1→2→4→8 while downstream wait remains exposed; shrink when stale compute exceeds saved wait.
    • Judge profitability in time: hidden_wait_ms > stale_compute_ms × margin, allowing lower acceptance when wall TPS and latency still improve.
  5. Measure real occupancy

    • Time at each in-flight depth and percentage of decode time at full depth.
    • Per-stage compute-busy, send-blocked, and input-idle time.
    • Exposed versus hidden downstream wait, stale compute/bytes, and inter-token p50/p95.
    • Rejection-recovery bubbles and pipeline warm-up separately from steady state.
  6. Controlled proof

    • GLM-4.7 split at injected 1/10/25/50/100 ms latency.
    • Depths 1/2/4/8, MTP versus MTP+Ngram, with long enough generations for steady state.
    • Compare against the existing PR baseline: MTP 42.85 wall TPS / 8.961 s mean latency versus MTP+Ngram 52.49 wall TPS / 7.316 s mean latency on LAN.

The success signal is not merely reaching depth 8. It is stage idle time collapsing as depth grows, exposed downstream wait approaching zero, TPS plateauing at slowest-stage throughput, and additional depth increasing stale work without improving throughput.

This is the same core loop as Shard's pipelined coordinator: direct return, multiple causally speculative chunks in flight, FIFO completion, and stale-result discard after divergence. The comparison we need to prove is whether our positional repair plus MTP+Ngram can achieve the same WAN latency hiding without reintroducing rollback/recovery serialization.

@michaelneale

michaelneale commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

One thing worth being explicit about for the WAN goal (#936 / #1025): this reaches depth 2, but not the deeper "keep the pipe full" regime shard uses, because the proposer is still coupled to the verify traversal. The next MTP draft comes from the target's own forward (NativeMtpDraftOrigin::VerifyNext) into a single next_draft slot (native_mtp/pipeline.rs), so proposal N+1 can't exist until reply N returns — a structural depth-2 cap, independent of permit_pipeline_width's profitability gate.

Shard's coordinate_pipe fills to depth because its predictor runs ahead, decoupled from verification (it optimistically appends its own proposals and drafts forward; a divergence just discards the stale in-flight chunks). #936's "Predictor-Agnostic Decode" section is the same idea: a PredictionProvider boundary where some source proposes a span the target verifies, and n-gram / prompt-lookup fits it precisely because it needs only the committed prefix, no target reply.

@i386

i386 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

@michaelneale The structural depth-two cap you identified is removed in c06bca6df.

The important clarification is that two is not configured or steady-state concurrency. It is now named and emitted as verify_window_bootstrap_probe_depth: depth one cannot tell us whether dependent work overlaps verification, so two is the smallest useful exploration point. After that, active depth is selected from ceil(mean verify elapsed / mean launch interval), bounded by the operator cap and recent continuation/stale-work profitability.

The proposer is no longer blocked on the single VerifyNext MTP slot. MTP remains the high-confidence anchor, but request-local stateful N-gram extends the optimistic branch and can seed work while an MTP reply or reject cooldown is outstanding. Credits are continuously refilled; replies commit FIFO; later epoch-tagged replies become stale after divergence without blocking the corrected branch on rollback or a synchronous drain. That is the predictor-ahead property needed by #1025 and Shard's coordinate_pipe.

On the two-node GLM-4.7 split with 100 ms injected one-way downstream delay, the four-prompt matched result was:

MTP depth 1 MTP + N-gram, adaptive cap 8
Decode TPS 11.53 29.30 (+154.19%)
Wall TPS 11.38 28.33 (+149.05%)
Mean latency 33.756 s 13.554 s (-59.85%)
Acceptance 91.23% 69.45%
Max selected / actual in-flight depth 1 7
Decode time with >=2 windows in flight 0% 34.66%
Time at the controller's current target 100% at serial target 24.12%

The exact final binary canary then reached selected and actual depth 8, spent 47.71% of decode time with multiple windows in flight, and produced 30.75 decode TPS / 13.076 s latency.

So the answer to “are both sides busy in parallel / is latency hidden?” is: materially, but not completely. At 100 ms the active depth grows to seven/eight and cuts request latency by ~60% in the matched run. Propagation overlaps while credits are full. Startup, proposal gaps, divergence recovery, bandwidth serialization, and the first corrected reply still leave bubbles; the 35-48% parallel-occupancy result tells us exactly how much room remains. On LAN the controller only reached actual depth two and spent 17.69% parallel, which is the expected behavior when deeper speculation does not repay itself.

This is now directly comparable to Shard's pipeline loop: bounded fill, predictor-ahead speculative chunks, FIFO commit, stale discard. Shard is given a depth bound; it does not infer the economically optimal depth from a serial trace. The extra piece here is the latency/stale-cost controller around that loop, plus positional repair instead of rollback.

The PR description now contains the full LAN, 100 ms, prior-baseline, and #938 comparison tables and the validation record.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/skippy-server/src/binary_transport/binary_messaging/async_forwarder.rs (1)

137-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Compute the write duration once and don't label failed writes as a successful write_ms.

elapsed_ms(job.enqueued_at) is computed twice (once for result, once for the telemetry attribute), and the "llama_stage.forward_write_ms" attribute is inserted unconditionally, even when write_stage_message_after_propagation fails. This mixes error cases into a duration metric meant to reflect a successful write, which will muddy exactly the latency/occupancy telemetry this PR's follow-up plan relies on.

♻️ Proposed fix
 fn forward_job(writer: &mut TcpStream, telemetry: &Telemetry, job: AsyncForwardJob) {
-    let result =
-        write_stage_message_after_propagation(writer, &job.message, job.wire_dtype, job.condition)
-            .context("async forward activation frame downstream")
-            .map(|()| elapsed_ms(job.enqueued_at))
-            .map_err(|error| format!("{error:#}"));
+    let write_result =
+        write_stage_message_after_propagation(writer, &job.message, job.wire_dtype, job.condition)
+            .context("async forward activation frame downstream");
+    let write_ms = elapsed_ms(job.enqueued_at);
     let write_end_unix_nanos = now_unix_nanos() as u64;
     let mut attrs = job.attrs;
-    attrs.insert(
-        "llama_stage.forward_write_ms".to_string(),
-        json!(elapsed_ms(job.enqueued_at)),
-    );
+    attrs.insert("llama_stage.forward_ok".to_string(), json!(write_result.is_ok()));
+    if write_result.is_ok() {
+        attrs.insert("llama_stage.forward_write_ms".to_string(), json!(write_ms));
+    }
     telemetry.emit_debug_span(
         "stage.binary_downstream_write",
         attrs,
         job.enqueued_unix_nanos,
         write_end_unix_nanos,
     );
+    let result = write_result
+        .map(|()| write_ms)
+        .map_err(|error| format!("{error:#}"));
     let _ = job.done.send(result);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@crates/skippy-server/src/binary_transport/binary_messaging/async_forwarder.rs`
around lines 137 - 156, Update forward_job to compute
elapsed_ms(job.enqueued_at) once after the write attempt, reuse that value for
the successful result and telemetry, and only insert
"llama_stage.forward_write_ms" into attrs when
write_stage_message_after_propagation succeeds; failed writes must retain their
error result without a successful-write duration attribute.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@crates/skippy-server/src/binary_transport/binary_messaging/async_forwarder.rs`:
- Around line 137-156: Update forward_job to compute elapsed_ms(job.enqueued_at)
once after the write attempt, reuse that value for the successful result and
telemetry, and only insert "llama_stage.forward_write_ms" into attrs when
write_stage_message_after_propagation succeeds; failed writes must retain their
error result without a successful-write duration attribute.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b56628db-7553-4c70-aaeb-c92499a09aa8

📥 Commits

Reviewing files that changed from the base of the PR and between 141ef39 and c06bca6.

📒 Files selected for processing (14)
  • crates/mesh-llm-host-runtime/src/inference/skippy/mod.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/stage/mod.rs
  • crates/skippy-server/src/binary_transport.rs
  • crates/skippy-server/src/binary_transport/binary_messaging.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/async_forwarder.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/connection.rs
  • crates/skippy-server/src/binary_transport/direct_return.rs
  • crates/skippy-server/src/binary_transport/wire.rs
  • crates/skippy-server/src/frontend/decode_scheduler.rs
  • crates/skippy-server/src/frontend/embedded_execution.rs
  • crates/skippy-server/src/frontend/embedded_generation.rs
  • crates/skippy-server/src/frontend/embedded_generation/lifecycle.rs
  • crates/skippy-server/src/frontend/generation/types.rs
  • crates/skippy-server/src/frontend/native_mtp/verify_window.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/skippy-server/src/frontend/generation/types.rs
  • crates/skippy-server/src/binary_transport/binary_messaging.rs
  • crates/skippy-server/src/frontend/native_mtp/verify_window.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/connection.rs
  • crates/skippy-server/src/frontend/embedded_generation.rs

@i386

i386 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

@michaelneale This diagnosis was exactly right for the implementation at the time of the comment: a target-produced VerifyNext MTP draft cannot run ahead of its own verify traversal, so MTP alone has a structural depth-two ceiling.

The follow-up at 55db3881d removes that ceiling from the hybrid path:

  • N-gram extends the optimistic prefix while older verifies are outstanding; it no longer waits for the next target MTP draft.
  • depth 2 is only the first identifiable overlap probe from depth 1, not configured concurrency.
  • the controller derives target depth from verify latency / launch interval, shares latency evidence across adaptive verify widths, and throttles when continuation-weighted stale cost is unprofitable.
  • FIFO commit and epoch-tagged stale discard match Shard's useful pipeline mechanism; positional trim replaces rollback.

I also found and fixed a correctness bug while tracing this: batched stage-0 serial correction could execute on the rejected suffix because alignment and decode were not atomic. All earlier c06bca6df performance figures are withdrawn. MTP and hybrid now produce identical per-prompt output hashes.

Corrected two-node GLM-4.7 split, controlled 100 ms one-way delay, 3 matched repetitions / 1,152 generated tokens per condition:

Condition Decode TPS Mean latency Acceptance Max selected / actual Time >=2 in flight
MTP depth 1 10.340 23.976 s 83.68% 1 / 1 0%
MTP + N-gram cap 8 13.715 19.388 s 52.80% 6 / 3 15.79%

That is +32.65% decode TPS and -19.14% latency despite 30.88 points lower acceptance. The hybrid proposed 1,284 tokens and accepted 678, versus MTP's 570 proposed / 477 accepted: lower acceptance ratio, but more useful speculative work completed.

The busy-time answer is workload-dependent and the new description separates it explicitly:

  • prompt copy: 11.485 -> 31.109 decode TPS, selected depth up to 6, actual max 3, 57.25% parallel time, 81/81 profitable continuations;
  • novel code: 9.402 -> 8.797 decode TPS, zero profitable continuations, 4.06% parallel time, controller falls back to depth 1.

So this now runs ahead independently in the Shard sense, but it does not claim continuous saturation: the bounded N-gram horizon plus fill/drain still leave bubbles. It fills enough to hide substantial latency when the text is draftable and refuses to manufacture stale traffic when it is not.

One nuance on “doesn't Shard figure depth out?”: current Shard has an optional acceptance-EMA throttle from 1 to a configured maximum. It starts optimistic at that maximum; it does not infer the maximum from measured traversal latency. This PR adds that latency-derived target on top of the refill/FIFO/stale-discard loop.

The PR description now contains the corrected results, #938 comparison, Shard source comparison, and exact validation details.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/skippy-server/src/frontend/generation_flow.rs`:
- Around line 859-867: Update the batch decode call to pass sampling only when
request.sampling.enabled is true, matching the conditional behavior used by
embedded_generation.rs. Also consume batch_outcome.session_alignment and emit
the same debug telemetry used for session auto-alignments in that path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 527b9311-7140-4628-aa90-b4215312e7ac

📥 Commits

Reviewing files that changed from the base of the PR and between c06bca6 and 55db388.

📒 Files selected for processing (5)
  • crates/skippy-server/src/binary_transport/binary_messaging/connection.rs
  • crates/skippy-server/src/binary_transport/decode_batcher.rs
  • crates/skippy-server/src/frontend/decode_scheduler.rs
  • crates/skippy-server/src/frontend/embedded_generation.rs
  • crates/skippy-server/src/frontend/generation_flow.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/skippy-server/src/binary_transport/binary_messaging/connection.rs
  • crates/skippy-server/src/frontend/embedded_generation.rs
  • crates/skippy-server/src/frontend/decode_scheduler.rs

Comment thread crates/skippy-server/src/frontend/generation_flow.rs
@i386

i386 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

@michaelneale Final follow-up to the structural coupling point in your comment: 0b3720c90 now removes the remaining proposal-boundary wait as well.

The hybrid no longer has to drain all active windows when one bounded proposal runs out. Before consuming the current horizon, it queries the committed-only N-gram index with the complete optimistic suffix (including dispatched-but-uncommitted tokens), appends any continuation, and immediately spends free verify credits. The suffix is never indexed until target commit. FIFO commit, epoch stale-discard, and positional repair are unchanged.

Fresh 3-process matched GLM-4.7 A/B at controlled 100 ms one-way delay (6 requests / 1,152 generated tokens per condition):

MTP depth 1 Refilled MTP + N-gram cap 8
Decode TPS 10.362 14.136 (+36.41%)
Wall TPS 8.020 10.113 (+26.10%)
Mean latency 23.941 s 18.985 s (-20.70%)
Acceptance 83.68% 45.10%
Max selected / actual 1 / 1 5 / 3
Time with >=2 windows active 0% 15.44%

Outputs matched MTP hashes in all repetitions. Relative to the corrected pre-refill hybrid baseline already on this PR, this is another +3.06% decode TPS / +2.12% wall TPS / -2.07% latency.

The busy-in-parallel answer is now measured rather than inferred:

  • draftable prompt-copy: actual depth 3, 60.64% parallel time, 35.177 vs 11.506 decode TPS, zero stale windows;
  • novel code: controller ends at depth 1, 4.07% parallel time, 8.845 vs 9.425 decode TPS, 21 stale windows.

At 0/50/100 ms delay, hybrid parallel time rises 46.05% -> 51.84% -> 58.35%, while its wall-TPS advantage over MTP rises +34.67% -> +61.73% -> +71.65%. That is the WAN behavior we wanted: propagation is increasingly hidden behind useful work, but fill/drain, finite predictor horizon, and serialized bandwidth remain visible.

Cap 2/4/8 on the 100 ms copy prompt reaches actual depth 2/3/3 and wall TPS 12.046/12.509/12.510. So cap 8 is not "concurrency eight" and forcing it would not help—the scheduler finds the plateau at actual depth 3. This now matches Shard's predictor-ahead fill/FIFO/stale-discard loop; the extra mechanism here is latency-derived depth plus positional repair rather than rollback.

The PR description has the full tables, #938 comparison, exact binary identity, and validation record.

@i386 i386 changed the title 🚬 Replace speculative rollback with positional MTP n-gram pipelining 🚬 Productionize bounded MTP + N-gram WAN pipelining Jul 20, 2026
@i386

i386 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

@michaelneale Final production update on the “how busy are the stages / is WAN latency hidden?” question.

First, one correction to my earlier explanation: authoritative position is enough to repair attention KV, but not the whole GLM-4.7 MTP sidecar. The sidecar also owns a recurrent hidden prefix and pending hidden vector. We now restore those from a bounded, process-local checkpoint on speculative trim. There is still no rollback/repair message or replay on the wire.

Final matched sustained copy result at 100 ms one-way delay:

Request concurrency Native MTP wall TPS Hybrid wall TPS Change Parallel occupancy Behavior
1 8.920 11.335 +27.07% 41.03% pipelined
2 17.222 19.499 +13.22% about 45.5% / request fairly shared
4 27.501 27.705 +0.74% 0% optional native fallback
8 29.395 29.463 +0.23% 0% optional native fallback

So yes: on draftable c1/c2 work, multiple dependent verify windows occupy the split concurrently and materially hide the 100 ms delay. At c4/c8 the independent requests already occupy the pipeline, so the global credit policy removes optional N-gram work instead of oversubscribing it.

The representative code/reasoning/copy/tool matrix is deliberately less exciting: LAN -0.96%, 20 ms +0.82%, 50 ms -0.69%, 100 ms +1.56%, and mixed WAN +0.61%. That meets the aggregate no-harm goal, but does not meet the requested +20% mixed-WAN bar. The >=20% proof is specific to sustained, highly draftable c1 text.

Relative to Shard, the refill/FIFO/stale-discard mechanism is now similar, but our scheduling adds MTP anchoring, latency/profitability depth selection, and globally fair request credits. Shard still has broader multi-stage and resumable failure machinery. Here, killing stage 1 during prefill now returns a bounded 502 in 29.06 seconds instead of hanging; the active request is not resumed.

The PR description now has the clean final tables, #938 and previous-run comparison, correctness repair, failure results, final binary identity, and production caveat. Commit: 20e457a0c.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/skippy-server/src/frontend/native_mtp/verify_window.rs (1)

26-456: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Function complexity: execute_native_mtp_verify_window remains very large and now handles more branching.

This function spans ~430 lines with 30+ parameters and now adds exact_positional_verify, decision_rejected_native_mtp_prefix, and dependent_target_rejected handling on top of the existing accept/reject/telemetry logic. As per coding guidelines, "Do not add Rust methods or functions exceeding the configured Clippy line-count or cognitive-complexity limits; split them into semantically named helpers." Consider extracting the proposal-buffer accept/reject accounting (lines 225-259) and the telemetry-attribute population (lines 319-450) into dedicated helpers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/skippy-server/src/frontend/native_mtp/verify_window.rs` around lines
26 - 456, Reduce the complexity of execute_native_mtp_verify_window by
extracting the proposal-buffer accept/reject accounting into a semantically
named helper and moving telemetry-attribute population into a dedicated helper.
Preserve the existing outputs, cooldown decisions, buffer exhaustion handling,
and emitted telemetry values while keeping execute_native_mtp_verify_window
focused on orchestration.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/skippy-server/src/frontend/speculative_credits.rs`:
- Around line 196-216: Update SpeculativeCreditRequest::drop to perform cleanup
only when this handle’s registered flag is true. Preserve the existing waiter
removal and request deactivation/removal behavior for successfully registered
requests, while ensuring duplicate handles returned by register() cannot modify
or remove the shared request entry.

In `@crates/skippy-server/src/frontend/speculative.rs`:
- Around line 284-291: Update the validation error returned by the cache N-gram
proposer to interpolate skippy_runtime::NGRAM_CACHE_MAX_NGRAM instead of
hardcoding 4, keeping the existing ngram_min/ngram_max validation conditions
unchanged.

---

Nitpick comments:
In `@crates/skippy-server/src/frontend/native_mtp/verify_window.rs`:
- Around line 26-456: Reduce the complexity of execute_native_mtp_verify_window
by extracting the proposal-buffer accept/reject accounting into a semantically
named helper and moving telemetry-attribute population into a dedicated helper.
Preserve the existing outputs, cooldown decisions, buffer exhaustion handling,
and emitted telemetry values while keeping execute_native_mtp_verify_window
focused on orchestration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c30e86ce-0b07-425f-80cf-3fcbd5dadddd

📥 Commits

Reviewing files that changed from the base of the PR and between 0b3720c and 20e457a.

📒 Files selected for processing (34)
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/speculative.rs
  • crates/mesh-llm-host-runtime/src/runtime/survey.rs
  • crates/skippy-metrics/src/lib.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/async_forwarder.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/connection.rs
  • crates/skippy-server/src/binary_transport/kv_eviction.rs
  • crates/skippy-server/src/binary_transport/restore_prefill_decode.rs
  • crates/skippy-server/src/frontend.rs
  • crates/skippy-server/src/frontend/decode_scheduler.rs
  • crates/skippy-server/src/frontend/embedded_execution.rs
  • crates/skippy-server/src/frontend/embedded_generation.rs
  • crates/skippy-server/src/frontend/embedded_generation/lifecycle.rs
  • crates/skippy-server/src/frontend/generation/persistent_lanes.rs
  • crates/skippy-server/src/frontend/generation/server.rs
  • crates/skippy-server/src/frontend/generation/types.rs
  • crates/skippy-server/src/frontend/generation_flow.rs
  • crates/skippy-server/src/frontend/native_mtp/decode.rs
  • crates/skippy-server/src/frontend/native_mtp/hybrid.rs
  • crates/skippy-server/src/frontend/native_mtp/mod.rs
  • crates/skippy-server/src/frontend/native_mtp/pipeline.rs
  • crates/skippy-server/src/frontend/native_mtp/verify_window.rs
  • crates/skippy-server/src/frontend/prefix_cache.rs
  • crates/skippy-server/src/frontend/speculative.rs
  • crates/skippy-server/src/frontend/speculative_credits.rs
  • crates/skippy-server/src/frontend/tests/mod.rs
  • crates/skippy-server/src/frontend/tests/multimodal.rs
  • crates/skippy-server/src/frontend/tests/prefill.rs
  • crates/skippy-server/src/frontend/tests/prompting.rs
  • crates/skippy-server/src/kv_integration/resident_prefix.rs
  • docs/plugins/telemetry.md
  • docs/skippy/PIPELINED_VERIFY_WINDOW.md
  • third_party/llama.cpp/patches/0020-Re-prime-native-MTP-after-state-restoration.patch
  • third_party/llama.cpp/patches/0021-Fix-N-gram-confidence-threshold-indexing.patch
  • third_party/llama.cpp/patches/0022-Restore-native-MTP-state-on-speculative-trim.patch
🚧 Files skipped from review as they are similar to previous changes (11)
  • crates/skippy-server/src/frontend.rs
  • crates/skippy-server/src/frontend/tests/prompting.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/connection.rs
  • crates/skippy-server/src/frontend/tests/multimodal.rs
  • crates/skippy-server/src/frontend/generation/server.rs
  • crates/skippy-server/src/frontend/native_mtp/mod.rs
  • crates/skippy-server/src/frontend/embedded_execution.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/speculative.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/async_forwarder.rs
  • crates/skippy-server/src/frontend/decode_scheduler.rs
  • crates/skippy-server/src/frontend/native_mtp/decode.rs

Comment thread crates/skippy-server/src/frontend/speculative_credits.rs Outdated
Comment thread crates/skippy-server/src/frontend/speculative.rs
@michaelneale

Copy link
Copy Markdown
Collaborator

Let's hold off on merging this one for now — it overlaps with an active research/WIP effort on the split-runtime decode path (both rewrite skippy-server/src/frontend.rs and binary_transport/direct_return.rs), and I want to reconcile the two designs before either lands rather than merge-then-untangle.

To be clear this isn't a quality concern — the benchmark writeup is excellent and the net simplification (deleting the checkpoint ring / adaptive-depth / credit-pool machinery) is genuinely appealing. I just want to sequence it deliberately against the in-flight work. Also worth flagging: this collides internally with #1034 (skippy-ffi ABI) and #1032 (preflight.rs), so merge order across your own stack matters here too.

Will follow up on sequencing. Leaving open, just please don't merge yet.

i386 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

@michaelneale — tagging you for review, particularly the new stage-overlap/latency evidence and the Shard comparison in the PR description.

@ndizazzo ndizazzo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lightweight review pass - found a couple things

description: 'Speculative runtime mode.',
inheritedLabel: 'Inherited by speculative defaults',
valueSchema: { kind: 'enum', values: ['draft', 'ngram'] },
valueSchema: { kind: 'enum', values: ['draft', 'disabled'] },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@i386 Is this the configuration surface you want for this work? This means that the user can't select Ngram MTP via any options config.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@i386 this comment seems to have been missed - @michaelneale opened #1056

Comment thread docs/plugins/telemetry.md
Comment on lines +151 to +152
| `llama_stage.verify_window.direct_return_upstream_opened` | Skippy decode summary | Boolean indicating that the preferred upstream-opened v10 prediction-return sink completed its handshake. |
| `llama_stage.verify_window.direct_return_reverse_fallback` | Skippy decode summary | Boolean indicating that the final stage used the bounded reverse-open v10 prediction-return fallback after the preferred sink was unavailable. |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat broken naming convention for these - not namespaces to mesh_llm.

michaelneale added a commit that referenced this pull request Jul 22, 2026
Capture the draft-model speculative-decode pipelining findings for branch
wip/wan-direct-prediction-return so the work can be picked up: what is proven
over WAN, the draft-vs-ngram acceptance-survival result, a Cohere/SWA trim
limitation, the 2-node bringup config trap, and pointers to the related
ngram-widening PRs (#1037, #1026, #875, #887).

Assisted-by: goose
i386 and others added 5 commits July 22, 2026 11:34
…treams

Ported from the WAN lab branch (wip/wan-direct-prediction-return, c340f74),
where it was validated live on a ~26ms WAN split. open_stage_transport_stream
re-applied the formation-time MAX_SPLIT_RTT_MS ceiling to every fresh
operational stream, so per-request direct-return sinks were rejected under
normal WAN RTT jitter while pooled forward lanes stayed healthy - surfacing as
ready-handshake timeouts and 502s on an already-admitted split. Split
admission still gates eligibility via gossiped, hysteresis-smoothed RTT plus
re-election; operational streams now warn and proceed.
…etup

Ported from the WAN lab branch (46108cf). Over a WAN mesh the return sink
connects to a local bridge alias, but the remote ready byte only arrives after
the bridge cold-establishes a fresh stage QUIC connection (~10s budget) and the
remote handler dials its local server. 5s timed out during that cold setup on
a healthy ~26ms split; forward lanes already use a 20s budget. Match it.
@michaelneale

Copy link
Copy Markdown
Collaborator

🤖 Pushed by micn's AI agent.

Pushed two small WAN-hardening commits onto this branch (87e691a4, 34bd3b89) — ports of the two transport fixes that were validated live on a real ~26ms WAN split (M5 Sydney ↔ AU RTX 4090) on the now-retired wip/wan-direct-prediction-return lab branch:

  1. 87e691a4 — stop re-applying the formation-time MAX_SPLIT_RTT_MS ceiling to fresh operational stage streams (warn-and-proceed). Without this, per-request direct-return sinks get rejected under normal WAN RTT jitter while pooled forward lanes stay healthy → ready-handshake timeouts → every request 502s on an already-admitted split. Split admission still gates on gossiped, hysteresis-smoothed RTT + re-election; this only relaxes the per-stream re-check.
  2. 34bd3b89 — return-sink ready-handshake timeout 5s→20s. Over WAN the ready byte only arrives after the bridge cold-establishes a fresh stage QUIC connection (~10s budget); 5s timed out during that cold setup on a healthy split. Matches the forward-lane budget.

Why: this PR's latency sweep (+161%→+281%) is injected-delay on LAN. These two fixes are what made a real internet split serve at all in the lab. Next step is the first real-WAN datapoint for v10: M5 (Sydney) ↔ cheap vast.ai AU 4090, GLM-4.7-Flash-MTP Q4_K_M 2-stage locked-topology split, native MTP vs MTP+ngram width 4 / depth 2. Numbers will be posted here.

Validation: cargo fmt --all --check clean; clippy -D warnings clean on mesh-llm-host-runtime, skippy-server, mesh-llm; cargo test -p mesh-llm-host-runtime --lib 1698 passed; cargo test -p skippy-server --lib 281 passed.

Not ported (deliberately): the lab branch's pre-warmed return-sink pool. v10 reworked direct_return (per-request sinks keyed by request/session), so it may not need it — will port only if the WAN rehearsal shows cold-open flakiness.

@michaelneale

Copy link
Copy Markdown
Collaborator

🤖 Posted by micn's AI agent (working on this branch with micn).

First real-WAN validation of v10 — results

Ran the first real-internet split test of this branch today: M5 (Sydney, Apple M5 Max, stage 0) ↔ vast.ai RTX 4090 (AU, stage 1, layers 27..48), 21–25ms real WAN RTT, GLM-4.7-Flash-MTP Q4_K_M, 2-stage split, greedy decode, /v1/completions, 384 tokens, 2 warm + 5 measured per arm. Both nodes at exactly 34bd3b89 (this branch tip, including the two WAN transport commits below).

Workload Arm A: native MTP (median dec tok/s) Arm B: MTP+ngram (min 2 / max 4 / depth 2) Change
repeat corpus 35.4 148.1 +318%
code re-emit w/ rename 35.6 69.8 +96%
  • Outputs byte-identical between arms (sha-256 over generated text: repeat 7ab048a22bff, rename 3d7b1f1f7c85 — same in both arms, stable across every rep).
  • Arm B repeat: occupancy 1.99 avg / 99% full-fraction, stale=0, ngram acceptance 384/384, direct_prediction_return=true, max_in_flight=2 on every run.
  • Arm B rename: occupancy ~1.22 / 26% full, 2 recovery epochs — partial fill on mixed-novelty text, matching this PR's own model of when the hybrid pays.
  • Cross-check against this PR's injected-delay sweep: it predicted ~+269% at 20ms injected; real 22ms WAN measured +318%. The injected-delay bench methodology holds up on the real internet.
  • Loopback ceiling (same binary, same flags, both stages on the M5): 206–237 dec tok/s on repeat → WAN at 148 leaves the residual gap as per-window traversal cost at depth 2, as expected.

Raw per-rep JSON (full timings blocks + generated text) captured for all four arm×workload runs; happy to attach if useful.

The two commits at the tip (87e691a4, 34bd3b89)

WAN transport hardening ported from the retired lab branch, where they were validated live on the same Sydney↔AU path:

  1. 87e691a4 — stop re-applying the formation-time MAX_SPLIT_RTT_MS ceiling to fresh operational stage streams (warn-and-proceed). Split admission still gates on gossiped, hysteresis-smoothed RTT + re-election. Without this, per-request direct-return sinks get rejected under normal WAN jitter → ready-handshake timeouts → 502s on an already-admitted split.
  2. 34bd3b89 — return-sink ready-handshake timeout 5s→20s to cover cold bridge QUIC setup over WAN; matches the forward-lane budget.

Validation: fmt clean, clippy -D warnings clean (mesh-llm-host-runtime, skippy-server, mesh-llm), cargo test -p mesh-llm-host-runtime --lib 1698 passed, -p skippy-server --lib 281 passed. Today's WAN run is the live proof: with these two commits, 15 consecutive requests over real WAN with zero 502s after formation.

Two real findings from the run (not yet fixed on this branch)

  1. Cold direct-return sink opens can still fail over WAN. First request of one bringup hit direct prediction return upstream-opened sink unavailable ... os error 35 + a lane-5 handshake failure → 502. The lab branch's prepared-return-sink pool (pre-warmed, age-rotated sinks; 2b76ba4e there) addresses exactly this; we deliberately hadn't ported it. Today says it's worth porting.
  2. Withdraw is terminal — a withdrawn split never re-forms. After that 502 the coordinator withdrew the topology (periodic_check, grace expired) and then sat in standby forever while the healthy worker sat "standing by for stage assignment" — dual restart with a fresh token was the only recovery. Root cause traced: SplitCoordinatorEvent::WithdrawStartupLoopControl::Breakstartup_local_model_loop tears down and returns; nothing re-enters the wait-for-participants → relaunch path. Proposed fix: on withdraw (when the model can't fit locally — the LocalFallback path already covers fits-locally), loop back to the participant-wait phase instead of ending the task. Working on this next; flagging here first in case there's an intended design for post-withdraw recovery we should follow instead.

…g the model task

Observed live on a real WAN split (Sydney M5 <-> AU 4090): one transient
direct-return 502 led periodic_check to mark the remote stage unavailable;
after the 75s grace the coordinator withdrew the topology. The Withdraw event
returned StartupLoopControl::Break, so startup_local_model_loop tore down and
the task ended permanently - while the remote worker sat healthy, logging
'standing by for stage assignment' forever. Only recovery was manually
restarting both nodes with a fresh token.

Make withdraw non-terminal: a new RelaunchSplit control/outcome runs the full
existing teardown, then loops back to the launch phase and re-enters
wait_for_split_participants, relaunching the split when an eligible peer
returns. The stop channel is checked before relaunch so explicit shutdown
still wins. LocalFallback (model fits locally) is unchanged.

The participant-wait loop's 30s cadence and stable-participant gating act as
the natural retry throttle; no extra backoff added.
@michaelneale

Copy link
Copy Markdown
Collaborator

🤖 Posted by micn's AI agent.

Follow-up to finding 2 in the WAN report above: pushed c05355adwithdrawn splits now relaunch when peers return instead of ending the model task.

Root cause: SplitCoordinatorEvent::WithdrawStartupLoopControl::Breakstartup_local_model_loop tore down and returned; nothing re-entered the participant-wait phase, so a withdrawn split needed a manual dual-restart even with a healthy worker standing by.

Fix shape (second-opinion reviewed): new RelaunchSplit outcome runs the full existing teardown, checks the stop channel (explicit shutdown still wins), then loops back to the launch phase and re-enters wait_for_split_participants. LocalFallback unchanged; the participant-wait loop's 30s cadence + stable-participant gating act as the retry throttle.

Validation: fmt clean, clippy -D warnings clean (mesh-llm-host-runtime, mesh-llm), cargo test -p mesh-llm-host-runtime --lib 1698 passed.

Still queued (not yet on branch): prepared-return-sink pool port for the cold-open 502 itself (finding 1).

…ngram

* origin/main:
  Add GLM DSA correctness trace reporting (#1033)
  Add GLM DSA package contract validation (#1032)
  Add direct quant layer package tooling (#1031)
  Lock split topology placement (#1050)
  fix nightly stability Qwen thinking
  ui(data-mode): migrate legacy harness storage to v2 (#1040)
  fix: Windows Vulkan runtime dependencies (#1046)
  test: validate MI300 runtime selection and placement (#1045)
  fix: support ROCm RDNA APU kernels safely (#1044)
  fix: ROCm GPU enumeration (#1039)
  Document public packaging installation channels
  Fix Windows autoupdate import (#1030)
  Update llama.cpp upstream pin
@michaelneale
michaelneale merged commit 3e87c38 into main Jul 22, 2026
29 checks passed
@michaelneale
michaelneale deleted the agent/positional-mtp-ngram branch July 22, 2026 08:32
@michaelneale
michaelneale restored the agent/positional-mtp-ngram branch July 22, 2026 08:45
danielwinterw added a commit to danielwinterw/mesh-llm that referenced this pull request Jul 22, 2026
…oser

Reconciles the standalone suffix N-gram proposer with Mesh-LLM#1026's positional-MTP
n-gram pipelining rework, which had diverged the config foundation.

Key decisions:
- Standalone N-gram stays allowed: validate() permits a request-local ngram
  proposer without native MTP, and the resolver produces a disabled native-MTP
  config plus "ngram" mode for standalone plans.
- Unify the decode loop on HistoryNgramProposer (cache + suffix superset) so the
  composite pipeline, verify-window path, and standalone path share one proposer
  type; drop the now-unused CachedNgramProposer::from_config.
- Adopt Mesh-LLM#1026's simplified NgramExtensionConfig ({max_tokens}) and the top-level
  arg cleanup (ngram bounds derive from speculative config).
- Drop the "simple" proposer kind: Mesh-LLM#1026 removed its skippy-ffi backing
  (skippy_ngram_simple_draft), leaving cache and suffix. Enum, validation,
  resolver, CLI, preflight, docs, and tests updated accordingly.

Gate the cache max-window (<=4) check on the cache kind in both the frontend
validate() and package preflight so suffix windows (<=64) are not rejected.

Build and lib tests pass across the affected crates.
danielwinterw added a commit to danielwinterw/mesh-llm that referenced this pull request Jul 22, 2026
Retargets the suffix N-gram proposer PR onto main, which now contains Mesh-LLM#1026's
positional-MTP n-gram rework (squash-merged). Builds on the earlier reconcile
of Mesh-LLM#1026; this merge folds in main's other changes.

- Keep the standalone-suffix reconciliation at every conflict (validate() allows
  a request-local ngram proposer without native MTP; resolver emits "ngram"
  mode; decode path unified on HistoryNgramProposer; simple proposer stays
  dropped since its skippy-ffi backing was removed upstream).
- Take main's non-suffix additions where they don't overlap: skippy-ffi
  dynamic_library module, the expanded preflight suite, and the rewritten
  layer-package-repos spec.
- Re-apply fixes the line-merge silently dropped where main touched the same
  regions: NgramProposerKind re-export (frontend.rs, lib.rs), the cache-only
  gating of the preflight ngram_max<=4 / history_scope checks, and the
  ngram_proposer path in the defaults UI schema fixture.
- Scrub stale ngram-simple references from the docs.

Workspace builds clean; lib tests green across skippy-server, mesh-llm-config,
mesh-llm-cli, mesh-llm-host-runtime, and skippy-model-package.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants