Skip to content

Pipeline MTP-anchored n-gram verify windows - #938

Merged
michaelneale merged 67 commits into
mainfrom
experiment/skippy-pipelined-decode
Jul 19, 2026
Merged

Pipeline MTP-anchored n-gram verify windows#938
michaelneale merged 67 commits into
mainfrom
experiment/skippy-pipelined-decode

Conversation

@i386

@i386 i386 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Skippy can now use native MTP, upstream llama.cpp N-gram proposal, or a composite of both while preserving target verification for every committed token. The old synchronous VerifySpan hot path is replaced by VerifyWindow, and deeper asynchronous verification is enabled only when request-local evidence predicts that overlap will repay stale speculative work.

This also makes speculative strategy a product configuration surface. Model packages can select native MTP, simple prompt lookup, or stateful cache N-gram behavior, while mesh-llm and skippy-server can apply deployment overrides.

Architecture

  • Replaces internal VerifySpan messages and hot-path machinery with versioned VerifyWindow request/reply messages. No internal compatibility lane remains.
  • Represents a proposal as zero-or-more native MTP prefix tokens followed by zero-or-more N-gram tail tokens.
  • Uses N-gram as a fallback proposal source when native MTP produces no token.
  • Verifies the combined linear stream in one target operation and commits the valid prefix when a later token rejects.
  • Uses llama.cpp's upstream simple prompt lookup and stateful N-gram cache ABIs instead of a separate in-tree N-gram implementation.
  • Treats verify_window_pipeline_depth as a maximum. A rolling exact-width profile estimates useful continuation overlap versus expected stale work before issuing a dependent future window.
  • Keeps synchronous batched VerifyWindow verification when the observed shape is not profitable.
  • Exposes composite, native-prefix, N-gram-tail, pure-fallback, adaptive-width, pipeline-policy, and stale-discard telemetry.
  • Charges native MTP tensors to the final logits-owning stage during direct GGUF split planning.

Product Configuration

  • Adds declarative speculative controls to model-package.json, built-in config schema, validation, and package preflight.
  • Supports simple, cache, and disabled N-gram proposal modes.
  • Carries package defaults through mesh split resolution into embedded Skippy stage options.
  • Adds matching mesh-llm and skippy-server configuration and CLI documentation.
  • Adds a model-agnostic direct N-gram strategy for models without native MTP.

Protocol

This intentionally breaks the internal Skippy stage decode protocol. Old and new staged-runtime binaries do not interoperate. Public mesh gossip, the OpenAI-compatible API, package selection, and mixed-version public mesh behavior are unchanged.

Lab Validation

Both hosts were rebuilt from scratch at 2678813b4 and reported 0.72.1+g2678813b4. The model was loaded as meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M with the normal --split product path. The planner selected micstudio 0..47 and studio54 47..48, four direct private-LAN lanes, F16 activation wire data, context 4096, temperature 0, seed 42, and concurrency 1.

Primary long-coding SPEED-Bench (qualitative, coding, OSL 1024, limit 4), three repetitions per condition:

Condition Completion tok/s Draft/Accepted Acceptance Improvement % over previous condition
No MTP 24.34 0 / 0 n/a n/a
Native MTP 32.27 7,665 / 5,247 68.45% +32.57%
MTP + simple N-gram 34.06 9,236 / 5,709 61.81% +5.57%

Frozen historical control (qualitative, coding and reasoning, OSL 512, limit 4), one repetition:

Condition Completion tok/s Draft/Accepted Acceptance Improvement % over previous condition
No MTP 26.21 0 / 0 n/a n/a
Native MTP 30.07 2,295 / 1,540 67.10% +14.73%
MTP + simple N-gram 33.64 2,540 / 1,541 60.67% +11.87%

Across the hybrid telemetry runs, N-gram proposed 2,624 tokens and contributed 1,121 accepted tail tokens. Pure N-gram fallback occurred 364 times. The adaptive policy recorded 875 window observations, 144 permit checks, and 97 permits. Metrics-server reports zero dropped events and zero export errors.

Verification

  • cargo test -p skippy-server --lib (273 passed)
  • cargo check -p skippy-server
  • cargo clippy -p skippy-server --all-targets -- -D warnings
  • cargo check -p mesh-llm
  • cargo clippy -p mesh-llm --all-targets -- -D warnings
  • cargo fmt --all -- --check
  • Clean host-native just release-build on both lab hosts
  • All GitHub PR checks green at the final commit

Remaining Performance Gate

The subsystem meets the functional gates: native MTP beats no MTP, hybrid N-gram contributes accepted tails, and hybrid beats native MTP. It does not meet the absolute 48.68 tok/s target derived from #875. The normal planner's 47/1 topology is not comparable with #875/#858's historical 22/26 topology, so a topology-matched run remains necessary before claiming that absolute promotion result.

Summary by CodeRabbit

  • New Features

    • Added configurable speculative decoding with native MTP and N-gram proposers, extensions, cooldown controls, and adaptive verification windows.
    • Added CLI and configuration-file overrides for speculative decoding settings.
    • Added pipelined verification and richer response timing, draft-token, and acceptance telemetry.
    • Added support for stateful N-gram caching and updated model-package proposer definitions.
    • Added restore-prefill decoding support and improved stage status handling during transient failures.
  • Improvements

    • Renamed verification terminology from “span” to “window” across serving and benchmarking tools.
    • Added benchmark artifacts for response timing data.
  • Documentation

    • Expanded configuration, CLI, package schema, and pipelined verification documentation.

@github-actions

github-actions Bot commented Jul 1, 2026

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 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

This PR adds typed speculative-decoding configuration and CLI overrides, N-gram simple/cache proposers, package proposer schemas, VerifyWindow protocol messages, pipelined native MTP execution, response telemetry, benchmark timing capture, and runtime resilience updates. It also renames local verification tooling from VerifySpan to VerifyWindow.

Speculative configuration and package resolution

Layer / File(s) Summary
Configuration, package schemas, and CLI overrides
crates/mesh-llm-cli/*, crates/mesh-llm-config/*, crates/skippy-model-package/*, crates/skippy-runtime/src/package.rs, crates/mesh-llm-host-runtime/src/inference/skippy/resolver/*
Adds speculative strategy, proposer, extension, native MTP, and VerifyWindow fields with precedence resolution, validation, package references, and CLI mapping.
N-gram ABI and runtime adapters
third_party/llama.cpp/patches/*, crates/skippy-ffi/*, crates/skippy-runtime/src/ngram.rs
Adds simple-draft and stateful cache ABI functions and Rust wrappers with bounds checking and lifecycle management.
VerifyWindow protocol and transport
crates/skippy-protocol/src/binary/*, crates/skippy-server/src/binary_transport/*, crates/skippy-prompt/src/prompt_cli/*
Replaces VerifySpan messages and statistics with VerifyWindow equivalents and serializes typed reply windows and native MTP drafts.
Pipelined native MTP frontend
crates/skippy-server/src/frontend/*
Adds adaptive VerifyWindow scheduling, composite native-MTP/N-gram proposals, pipelined verification, stale-window cleanup, and telemetry propagation.
Runtime and benchmark support
crates/mesh-llm-host-runtime/src/runtime/*, crates/skippy-bench/*, crates/skippy-correctness/*, crates/openai-frontend/*
Adds response timing aggregation, typed draft reporting, non-blocking startup work, transient stage-refresh handling, and benchmark timing artifacts.
Documentation and build updates
docs/*, website/src/docs/pages/CLI.md, scripts/*
Documents speculative configuration and VerifyWindow behavior and updates static build-directory resolution and release-build tests.

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

Possibly related issues

Possibly related PRs

Suggested reviewers: ndizazzo, michaelneale

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant RuntimeResolver
  participant StageOpenAiBackend
  participant VerifyWindowScheduler
  participant BinaryTransport
  participant NativeMtpVerifier

  CLI->>RuntimeResolver: provide speculative overrides
  RuntimeResolver->>StageOpenAiBackend: resolve typed speculative plan
  StageOpenAiBackend->>VerifyWindowScheduler: open pipeline window
  StageOpenAiBackend->>BinaryTransport: send VerifyWindow
  BinaryTransport->>NativeMtpVerifier: verify proposal
  NativeMtpVerifier-->>BinaryTransport: typed draft and reply window
  BinaryTransport-->>StageOpenAiBackend: return verification reply
  StageOpenAiBackend->>VerifyWindowScheduler: complete window in FIFO order
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.89% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: native MTP-anchored n-gram proposals verified through pipelined VerifyWindow flow.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch experiment/skippy-pipelined-decode

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.

@michaelneale

Copy link
Copy Markdown
Collaborator

Very nice

i386 added 2 commits July 11, 2026 08:52
…elined-decode

# Conflicts:
#	crates/skippy-server/src/frontend/embedded_generation.rs
@i386 i386 changed the title Replace Skippy VerifySpan with verify windows Pipeline direct-return n-gram verify windows Jul 10, 2026
@i386 i386 changed the title Pipeline direct-return n-gram verify windows Pipeline MTP-anchored n-gram verify windows Jul 14, 2026
@i386

i386 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Added the two requested standalone proposer rows. These are three-repetition SPEED-Bench long-coding results on the same 47/1, four-lane, F16 two-host topology.

Condition Completion tok/s Draft/Accepted Acceptance Improvement % over previous condition
No speculation 24.34 0 / 0 n/a n/a
Simple N-gram 25.51 5,013 / 2,009 40.08% +4.79%
Cached N-gram 24.76 4,712 / 1,766 37.48% -2.91%
Native MTP 32.27 7,665 / 5,247 68.45% +30.30%
MTP + simple N-gram 34.06 9,236 / 5,709 61.81% +5.57%
MTP + cached N-gram 33.49 7,589 / 5,123 67.51% -1.67%

Standalone simple N-gram beats no speculation by 4.79%. Cached N-gram is functioning, but is only 1.74% over no speculation and trails simple N-gram on this workload. Both hybrid variants beat native MTP against their direct parent; MTP + simple remains the winner.

For the two standalone rows, Draft/Accepted comes from correlated stage.openai_decode_verify_window telemetry. The pure N-gram path currently does not copy these totals into the llama-compatible response draft_n fields, which is now documented as a telemetry gap.

Artifacts and complete reproduction identity: https://github.com/Mesh-LLM/lab-experiments/blob/6ee91ad/skippy-pipelined-decode/phase-6/20260717T043625Z/RESULTS.md

@i386

i386 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Single-node SPEED-Bench parity is complete for GLM 4.7 Flash Q4_K_M on studio54 (Metal, ctx 4096, one slot, three repetitions per row).

Condition Direct llama tok/s Mesh tok/s Mesh vs llama
No speculation 48.07 46.63 -3.00%
Simple N-gram 44.92 47.05 +4.73%
Cached N-gram 33.28 42.42 +27.45%
Native MTP 55.66 28.22 -49.29%
MTP + simple N-gram 43.87 27.85 -36.53%
MTP + cached N-gram 44.86 32.14 -28.36%

This confirms a small base serving-path deficit and a severe Mesh native-MTP hot-path regression. Direct llama MTP improves 15.78% over its baseline; Mesh MTP regresses 39.48% from its baseline. Mesh simple N-gram does beat direct llama simple N-gram by 4.73%.

Telemetry also has two remaining diagnostic gaps: pure N-gram/extender proposal and acceptance counts are unavailable, and the Mesh response-level MTP draft denominator reports 29.07% while native verification telemetry reports 87.29%.

Actually unpatched llama.cpp at the exact base pin cannot load this GGUF (expected 868 tensors, got 862). The direct control is canonical llama-server from the same patched llama checkout, which holds model support and inference core constant while removing the Mesh/Skippy serving path.

Full configs, repetitions, compact telemetry, and interpretation: https://github.com/Mesh-LLM/lab-experiments/tree/2208edf/skippy-pipelined-decode/phase-6/20260717T143804Z

Bring in #1011 (resilient to memory pressure & network jitter) which adds
bounded timeouts on the prediction-return and lane-open paths. Resolved
conflicts in binary_transport/direct_return.rs and frontend.rs by taking
main's bounded single-attempt connection logic while keeping the branch's
speculative decode code. Removed now-unused retry helpers/constants.
@micspiral

Copy link
Copy Markdown

WAN split validation: Sydney ↔ Melbourne (2-node), real GPU

Ran this branch as a genuine 2-node split over the public internet: M5 Max (Sydney, Metal, stage-0)RTX A5000 (Melbourne vast.ai, CUDA, stage-1), ~20 ms RTT, direct iroh hole-punch. Both nodes built from the same commit. Model meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M, forced split via --max-vram 13 per node so neither can hold the whole model.

Headline: it works well over a real WAN link 🎉

Once main is merged in (see note below), the split is stable and correct across the internet, and speculative decoding does exactly what it should — amortizes per-token round-trips.

Latency contribution (the interesting bit)

tok/s ms/token
Solo (full model, 1 GPU, no network) 77.3 12.9
2-node split (RTT ~20 ms) ~17 57.8
Split overhead +44.9

Split overhead 44.9 ms/token ≈ 2×RTT — latency (the per-token round-trips) is essentially the entire cost of splitting here, not compute. That's precisely why prediction matters on WAN.

Speculative A/B (RTT ~20 ms)

Condition short (OSL 48) coding (OSL 256)
MTP off ~16.9
native MTP 17.6
MTP + N-gram 19.1 20.1 (max 21.3)

+13–19% over MTP-off, and the gain grows on longer/coding output — speculation commits multiple tokens per round-trip, hiding the 44.9 ms latency tax.

Stability / recovery (killed the remote stage mid-generation)

  • In-flight request failed fast (502 in 1.79 s) ✅ — the bounded-timeout fix from Make split serving resilient to memory pressure and network jitter #1011 works, no more infinite hang.
  • A new request after peer death still routed to the dead stage and hung ~30 s ❌ — stale peer state; slow dead-peer detection for new requests. Fix candidate identified separately.

Note on main

The GPU split was broken on this branch alone (KV-evict / execution-lane timeout race — only visible on the fast CUDA backend; the slow CPU path masked it). Merging main (#1011, "resilient to memory pressure and network jitter") fixed it — its bounded timeouts on the prediction-return and lane-open paths are required for GPU split viability. main is now merged into this branch.

Numbers are functional WAN validation on a single cheap A5000 (not a topology-matched perf run); throughput is latency-bound at ~20 ms RTT.

…ition

Adds docs/skippy/WAN_SPLIT_PERF.md: the single-stream per-token cost model
(TPOT ~= C_total + (S-1)*2*RTT + (S-1)*P), compute-bound vs latency-bound
criteria, when adding a stage helps (memory, concurrency/pipeline overlap,
dense compute-bound models), and speculation as the WAN amortization lever.

Backed by 2026-07-18 Sydney<->Melbourne 2-node measurements: solo 12.9 ms/tok
compute, split 57.8 ms/tok, decomposing to 12.9 compute + 40 (2xRTT) + 4.9
protocol. Workload was latency-bound (~78% network).
Documents the measured ~30s hang when a new request routes to a killed
split stage, the confirmed root cause (60s heartbeat / lenient failure
threshold + slow lane-open timeouts), and a two-layer fix (short
steady-state lane-open deadline; feed lane failures into target_health
cooldown) plus an explicit validation gate. Mesh-timing changes are out
of scope pending live multi-node validation.
@michaelneale

Copy link
Copy Markdown
Collaborator

This is starting to work well

…stage

When a downstream split stage dies, a new request would open a fresh lane
and wait the full ~20s warmup ready-deadline before erroring (observed as a
~30s hang in the Sydney<->Melbourne kill test). The 20s deadline is only
needed during pool warmup, when the downstream may still be loading its
model.

Split the deadline: pool warmup keeps LANE_READY_READ_TIMEOUT (20s); mid-life
reconnects from checkout()/replace_lane() on an already-serving mesh use a
new LANE_STEADY_CONNECT_TIMEOUT (3s). A healthy peer answers in milliseconds,
so a dead stage now fails in ~3s instead of ~30s.

Restores receive_persistent_lane_ready as the shared bounded-handshake helper
(dropped during the main merge) and removes a now-obsolete retry test that
covered pre-#1011 retry behavior. Adds tests asserting the steady-state
deadline stays well under the warmup deadline and that the handshake read
fails fast on a silent downstream.
Records verified planner behaviour (skippy-coordinator/topology.rs,
skippy-topology, host-runtime call site):
- latency is a placement cost (rtt_ms penalty), not just relay-only exclusion
- planner selects a node subset; does not have to use every eligible node
- stage count is gated on a decode-TPOT target (shallower-that-meets beats
  deeper-that-does-not)

And the gaps that matter at many-node scale:
- no peer-to-peer RTT matrix in production (edge_signals never wired; only
  coordinator-RTT is used) -> co-located nodes cannot be exploited
- network estimate is max(coordinator RTT) x node_count, a worst-case proxy
- no first-class prefer-fewer/never-place-above-Y policy beyond the TPOT gate
A pooled downstream lane whose stage died while checked in was a dead TCP
stream; reusing it blocked the next generation read forever (handshake
read-timeout is cleared for pooled lanes so long generations don't truncate).
checkout() now probes lane liveness with a nonblocking peek and discards a
dead lane so it reconnects with the short steady-state deadline instead of
hanging.

Validated on a loopback 2-node split with a mid-flight worker kill: new
request now fails faster than main (60s vs main's 90s baseline). Does not
fully solve the recovered-local routing path, tracked as follow-up.
…elined-decode

# Conflicts:
#	crates/mesh-llm-host-runtime/src/mesh/mod.rs
#	crates/mesh-llm-host-runtime/src/mesh/tests.rs
@michaelneale
michaelneale marked this pull request as ready for review July 19, 2026 09:51
@github-actions
github-actions Bot requested a review from ndizazzo July 19, 2026 09:51

@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: 5

🧹 Nitpick comments (2)
crates/skippy-server/src/lib.rs (1)

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

Consider importing these from skippy_server::frontend instead of expanding crate-root re-exports.

frontend is a public module, so consumers like resolver/speculative.rs can use skippy_server::frontend::{SpeculativeDecodeConfig, ...} directly rather than growing the root re-export surface.

As per coding guidelines: "Minimize crate-root re-exports. Root re-exports are acceptable as temporary compatibility shims during refactors, but new code should prefer importing from the owning module directly."

🤖 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/lib.rs` around lines 30 - 33, Update the imports in
the affected consumer to use the public skippy_server::frontend module for
SpeculativeDecodeConfig and the other frontend-owned symbols, instead of adding
or relying on crate-root re-exports. Keep only symbols that genuinely belong at
the crate root and preserve existing behavior.

Source: Coding guidelines

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

320-465: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Split execute_native_mtp_verify_window into helpers
execute_native_mtp_verify_window is 444 lines. Extracting only the telemetry block still leaves it well above the 200-line Clippy limit, so this needs a broader split—at least the debug-telemetry section plus another helper for the main proposal/verify state updates.

🤖 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
320 - 465, Split execute_native_mtp_verify_window into multiple focused helpers
to bring the function below Clippy’s 200-line limit. Extract the shown telemetry
construction and emission into a dedicated helper, and move the main
proposal/verification state-update logic into another helper while preserving
existing behavior and data flow.

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-bench/src/evals/adapters/speed_bench.rs`:
- Around line 35-60: Update the JSONL write in capture_response_timings so the
generated Python script writes an actual newline after each serialized timing
record, rather than the literal backslash-n text. Preserve the existing JSON
serialization and locking behavior.

In `@crates/skippy-server/src/frontend/native_mtp/decode.rs`:
- Around line 225-241: Update observe_adaptive_verify_window so
adaptive_verify_window_width_min uses the already-incremented
adaptive_verify_window_count to detect the first observation, rather than
checking whether the stored minimum is zero. Preserve zero as a legitimate
minimum and apply min(existing, width) for every subsequent observation.

In `@crates/skippy-server/src/frontend/native_mtp/pipeline.rs`:
- Around line 58-80: Update next_window so dispatched_native_mtp_token_count
also advances for the expected_free_target removed by candidates.pop_front()
when that token belongs to the native proposal prefix. Preserve the existing
drained proposal counting and ensure the next window’s native_mtp_token_count
reflects both removed portions.

In `@docs/skippy/PIPELINED_VERIFY_WINDOW.md`:
- Around line 287-306: Update the three command examples under “No MTP
Baseline,” “Native MTP Only,” and “MTP With Cache-backed N-gram Extension” so
each uses the matching --speculative-strategy value: disabled, mtp, and
mtp-cache respectively, while preserving the existing command options.

In `@scripts/build-release.sh`:
- Around line 141-145: Separate the LLAMA_STAGE_BUILD_DIR declaration from its
command-substitution assignment in the release build flow. Keep the existing
build-llama.sh invocation and environment variables unchanged, then export
LLAMA_STAGE_BUILD_DIR afterward so its exit status propagates and set -e can
detect failures.

---

Nitpick comments:
In `@crates/skippy-server/src/frontend/native_mtp/verify_window.rs`:
- Around line 320-465: Split execute_native_mtp_verify_window into multiple
focused helpers to bring the function below Clippy’s 200-line limit. Extract the
shown telemetry construction and emission into a dedicated helper, and move the
main proposal/verification state-update logic into another helper while
preserving existing behavior and data flow.

In `@crates/skippy-server/src/lib.rs`:
- Around line 30-33: Update the imports in the affected consumer to use the
public skippy_server::frontend module for SpeculativeDecodeConfig and the other
frontend-owned symbols, instead of adding or relying on crate-root re-exports.
Keep only symbols that genuinely belong at the crate root and preserve existing
behavior.
🪄 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: 1f5beaea-dc81-448e-99fc-19b1af451b08

📥 Commits

Reviewing files that changed from the base of the PR and between 6e0ee2e and 0972bc7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (92)
  • crates/mesh-llm-cli/src/lib.rs
  • crates/mesh-llm-cli/src/parser.rs
  • crates/mesh-llm-config/src/lib.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/validate.rs
  • crates/mesh-llm-host-runtime/Cargo.toml
  • crates/mesh-llm-host-runtime/src/inference/skippy/mod.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/package.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/translation.rs
  • crates/mesh-llm-host-runtime/src/inference/skippy/resolver/types.rs
  • crates/mesh-llm-host-runtime/src/mesh/stage_transport.rs
  • crates/mesh-llm-host-runtime/src/mesh/tests/requirements.rs
  • crates/mesh-llm-host-runtime/src/plugin/mod.rs
  • crates/mesh-llm-host-runtime/src/runtime/mod.rs
  • crates/mesh-llm-host-runtime/src/runtime/options.rs
  • crates/mesh-llm-host-runtime/tests/fixtures/config_schema_defaults_ui_reference.json
  • crates/mesh-llm/src/lib.rs
  • crates/openai-frontend/src/completions.rs
  • crates/skippy-bench/src/chat_corpus.rs
  • crates/skippy-bench/src/cli.rs
  • crates/skippy-bench/src/evals.rs
  • crates/skippy-bench/src/evals/adapters/speed_bench.rs
  • crates/skippy-bench/src/evals/run.rs
  • crates/skippy-bench/src/main.rs
  • crates/skippy-bench/src/verify_window_local.rs
  • crates/skippy-correctness/src/runner/native_mtp.rs
  • crates/skippy-ffi/README.md
  • crates/skippy-ffi/build.rs
  • crates/skippy-ffi/src/lib.rs
  • crates/skippy-model-package/src/main.rs
  • crates/skippy-model-package/src/preflight.rs
  • crates/skippy-prompt/src/prompt_cli/generation.rs
  • crates/skippy-prompt/src/prompt_cli/speculative.rs
  • crates/skippy-prompt/src/prompt_cli/tests.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/package.rs
  • crates/skippy-server/README.md
  • crates/skippy-server/src/binary_transport.rs
  • crates/skippy-server/src/binary_transport/direct_return.rs
  • crates/skippy-server/src/binary_transport/kv_eviction.rs
  • crates/skippy-server/src/binary_transport/options.rs
  • crates/skippy-server/src/binary_transport/restore_prefill_decode.rs
  • crates/skippy-server/src/binary_transport/tests.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/decode_scheduler.rs
  • crates/skippy-server/src/frontend/embedded_execution.rs
  • crates/skippy-server/src/frontend/embedded_generation.rs
  • crates/skippy-server/src/frontend/generation_flow.rs
  • crates/skippy-server/src/frontend/local_generation.rs
  • crates/skippy-server/src/frontend/native_mtp/batched_verify.rs
  • crates/skippy-server/src/frontend/native_mtp/decode.rs
  • crates/skippy-server/src/frontend/native_mtp/draft.rs
  • crates/skippy-server/src/frontend/native_mtp/env.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/trim.rs
  • crates/skippy-server/src/frontend/native_mtp/verifier.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/tests.rs
  • crates/skippy-server/src/frontend/wire_messages.rs
  • crates/skippy-server/src/lib.rs
  • docs/README.md
  • docs/USAGE.md
  • docs/skippy/CONFIGURATION.md
  • docs/skippy/DEAD_PEER_FAST_FAIL_PLAN.md
  • docs/skippy/PIPELINED_VERIFY_WINDOW.md
  • docs/skippy/TOPOLOGY_PLANNER.md
  • docs/skippy/WAN_SPLIT_PERF.md
  • docs/skippy/family/qwen-results.md
  • docs/skippy/speculative_decoding.md
  • docs/specs/layer-package-repos.md
  • evals/skippy-coding-agent-loop.jsonl
  • scripts/build-mac.sh
  • scripts/build-release.sh
  • scripts/tests/test_build_release.py
  • 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
  • website/src/docs/pages/CLI.md
💤 Files with no reviewable changes (3)
  • crates/skippy-server/src/frontend/native_mtp/trim.rs
  • crates/skippy-server/src/frontend/native_mtp/env.rs
  • crates/skippy-server/src/frontend/native_mtp/batched_verify.rs

Comment on lines +35 to +60

timings_path = os.environ.get("SKIPPY_BENCH_RESPONSE_TIMINGS_PATH")
timings_lock = threading.Lock()
original_json = requests.models.Response.json

def capture_response_timings(self, *args, **kwargs):
response = original_json(self, *args, **kwargs)
timings = response.get("timings") if isinstance(response, dict) else None
if (
timings_path
and isinstance(timings, dict)
and not getattr(self, "_skippy_timings_captured", False)
):
# Preserve only scalar timing counters; never copy request or response content.
safe_timings = {
key: value
for key, value in timings.items()
if isinstance(key, str) and isinstance(value, (bool, int, float))
}
with timings_lock:
with open(timings_path, "a", encoding="utf-8") as output:
output.write(json.dumps({"timings": safe_timings}, sort_keys=True) + "\\n")
self._skippy_timings_captured = True
return response

requests.models.Response.json = capture_response_timings

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

python -c 'import json; print(json.dumps({"test": 1}) + "\\n" + json.dumps({"test": 2}))'

Repository: Mesh-LLM/mesh-llm

Length of output: 180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="crates/skippy-bench/src/evals/adapters/speed_bench.rs"

echo "== outline =="
ast-grep outline "$file" --view expanded || true

echo
echo "== relevant lines =="
sed -n '1,120p' "$file" | cat -n

Repository: Mesh-LLM/mesh-llm

Length of output: 5588


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "SKIPPY_BENCH_RESPONSE_TIMINGS_PATH|response timings|timings" crates/skippy-bench -S

Repository: Mesh-LLM/mesh-llm

Length of output: 4721


Write an actual newline in the JSONL output
crates/skippy-bench/src/evals/adapters/speed_bench.rs:35-60
\\n is emitted literally into the Python script here, so the timings file gets \n text instead of a line break and stops being valid JSONL. Replace it with \n.

🤖 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-bench/src/evals/adapters/speed_bench.rs` around lines 35 - 60,
Update the JSONL write in capture_response_timings so the generated Python
script writes an actual newline after each serialized timing record, rather than
the literal backslash-n text. Preserve the existing JSON serialization and
locking behavior.

Comment on lines +225 to 241
pub(in crate::frontend) fn observe_adaptive_verify_window(
&mut self,
width: usize,
previous_width: usize,
next_width: usize,
) {
self.adaptive_verify_window_count += 1;
self.adaptive_verify_window_width_sum += width;
self.adaptive_verify_window_width_min = if self.adaptive_verify_window_width_min == 0 {
width
} else {
self.adaptive_verify_window_width_min.min(width)
};
self.adaptive_verify_window_width_max = self.adaptive_verify_window_width_max.max(width);
self.adaptive_verify_window_grow_count += usize::from(next_width > previous_width);
self.adaptive_verify_window_shrink_count += usize::from(next_width < previous_width);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Sentinel-zero conflates "unset" with a legitimate zero width.

adaptive_verify_window_width_min uses 0 both as the "not yet observed" sentinel and as a real width value. If a genuine width == 0 observation occurs first (e.g. AdaptiveVerifyWindow::width returns 0 when available_tokens == 0 near the generation limit), any subsequent non-zero width will incorrectly overwrite the true minimum instead of taking min(existing, new), since the check self.adaptive_verify_window_width_min == 0 can't distinguish "never set" from "legitimately zero."

Use the already-incremented adaptive_verify_window_count to detect the first observation instead of relying on the value itself.

🐛 Proposed fix
         self.adaptive_verify_window_count += 1;
         self.adaptive_verify_window_width_sum += width;
-        self.adaptive_verify_window_width_min = if self.adaptive_verify_window_width_min == 0 {
+        self.adaptive_verify_window_width_min = if self.adaptive_verify_window_count == 1 {
             width
         } else {
             self.adaptive_verify_window_width_min.min(width)
         };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub(in crate::frontend) fn observe_adaptive_verify_window(
&mut self,
width: usize,
previous_width: usize,
next_width: usize,
) {
self.adaptive_verify_window_count += 1;
self.adaptive_verify_window_width_sum += width;
self.adaptive_verify_window_width_min = if self.adaptive_verify_window_width_min == 0 {
width
} else {
self.adaptive_verify_window_width_min.min(width)
};
self.adaptive_verify_window_width_max = self.adaptive_verify_window_width_max.max(width);
self.adaptive_verify_window_grow_count += usize::from(next_width > previous_width);
self.adaptive_verify_window_shrink_count += usize::from(next_width < previous_width);
}
pub(in crate::frontend) fn observe_adaptive_verify_window(
&mut self,
width: usize,
previous_width: usize,
next_width: usize,
) {
self.adaptive_verify_window_count += 1;
self.adaptive_verify_window_width_sum += width;
self.adaptive_verify_window_width_min = if self.adaptive_verify_window_count == 1 {
width
} else {
self.adaptive_verify_window_width_min.min(width)
};
self.adaptive_verify_window_width_max = self.adaptive_verify_window_width_max.max(width);
self.adaptive_verify_window_grow_count += usize::from(next_width > previous_width);
self.adaptive_verify_window_shrink_count += usize::from(next_width < previous_width);
}
🤖 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/decode.rs` around lines 225 -
241, Update observe_adaptive_verify_window so adaptive_verify_window_width_min
uses the already-incremented adaptive_verify_window_count to detect the first
observation, rather than checking whether the stored minimum is zero. Preserve
zero as a legitimate minimum and apply min(existing, width) for every subsequent
observation.

Comment on lines +58 to +80
pub(in crate::frontend) fn next_window(
&mut self,
verify_width: usize,
) -> Option<PipelinedCandidateWindow> {
let verify_width = verify_width
.min(self.parallel_verify_width)
.min(self.candidates.len());
if verify_width == 0 {
return None;
}
let native_mtp_token_count = self
.proposal
.native_mtp_token_count()
.saturating_sub(self.dispatched_native_mtp_token_count)
.min(verify_width);
let proposal_tokens = self.candidates.drain(..verify_width).collect();
self.dispatched_native_mtp_token_count += native_mtp_token_count;
Some(PipelinedCandidateWindow {
proposal_tokens,
expected_free_target: self.candidates.pop_front(),
native_mtp_token_count,
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files crates/skippy-server/src/frontend/native_mtp/pipeline.rs
ast-grep outline crates/skippy-server/src/frontend/native_mtp/pipeline.rs --view expanded
wc -l crates/skippy-server/src/frontend/native_mtp/pipeline.rs

Repository: Mesh-LLM/mesh-llm

Length of output: 2355


🏁 Script executed:

sed -n '1,260p' crates/skippy-server/src/frontend/native_mtp/pipeline.rs

Repository: Mesh-LLM/mesh-llm

Length of output: 7711


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
p = Path('crates/skippy-server/src/frontend/native_mtp/pipeline.rs')
print(p.exists(), p.stat().st_size if p.exists() else None)
print(p.read_text().splitlines()[0:220])
PY

Repository: Mesh-LLM/mesh-llm

Length of output: 8043


🏁 Script executed:

rg -n "struct NativeMtpHybridProposal|fn native_mtp_token_count|ngram_tail_rejected|native_mtp_prefix_rejected|dispatched_native_mtp_token_count|expected_free_target" crates/skippy-server/src/frontend/native_mtp crates/skippy-server/src/frontend -g '*.rs'

Repository: Mesh-LLM/mesh-llm

Length of output: 10890


🏁 Script executed:

sed -n '1,240p' crates/skippy-server/src/frontend/native_mtp/mod.rs
printf '\n---\n'
sed -n '1,260p' crates/skippy-server/src/frontend/native_mtp/proposal.rs
printf '\n---\n'
rg -n "native_mtp_token_count\(|ngram_tail_rejected\(|native_mtp_prefix_rejected\(" crates/skippy-server/src/frontend/native_mtp -g '*.rs'

Repository: Mesh-LLM/mesh-llm

Length of output: 5094


🏁 Script executed:

sed -n '1,260p' crates/skippy-server/src/frontend/native_mtp/proposal.rs
printf '\n---\n'
rg -n "native_mtp_token_count\(|ngram_tail_rejected\(|native_mtp_prefix_rejected\(" crates/skippy-server/src/frontend/native_mtp -g '*.rs'

Repository: Mesh-LLM/mesh-llm

Length of output: 4339


🏁 Script executed:

rg -n "native_mtp_prefix_rejected|ngram_tail_rejected|native_mtp_token_count" crates/skippy-server/src -g '*.rs'

Repository: Mesh-LLM/mesh-llm

Length of output: 9182


🏁 Script executed:

sed -n '129,205p' crates/skippy-server/src/frontend/native_mtp/hybrid.rs
printf '\n---\n'
sed -n '490,520p' crates/skippy-server/src/frontend/native_mtp/verify_window.rs
printf '\n---\n'
sed -n '1030,1160p' crates/skippy-server/src/frontend/embedded_generation.rs

Repository: Mesh-LLM/mesh-llm

Length of output: 12033


Count the reserved free target in the native-prefix offset dispatched_native_mtp_token_count only advances by the drained proposal slice, but expected_free_target also removes one candidate. If that token is still native, the next window will compute the wrong native_mtp_token_count and misclassify the tail as native.

🤖 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/pipeline.rs` around lines 58 -
80, Update next_window so dispatched_native_mtp_token_count also advances for
the expected_free_target removed by candidates.pop_front() when that token
belongs to the native proposal prefix. Preserve the existing drained proposal
counting and ensure the next window’s native_mtp_token_count reflects both
removed portions.

Comment on lines +287 to +306
```bash
mesh-llm serve meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M --split --no-draft
```

Use `[models.speculative] strategy = "disabled"` to make this an explicit
baseline instead of relying on environment variables.

### Native MTP Only

```bash
mesh-llm serve meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M --split --no-draft
```

Use `[models.speculative] strategy = "mtp"` to force this control.

### MTP With Cache-backed N-gram Extension

```bash
mesh-llm serve meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M --split --no-draft
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the duplicated bash commands to reflect the respective modes.

The bash code blocks under "No MTP Baseline", "Native MTP Only", and "MTP With Cache-backed N-gram Extension" are identical. Consider adding the appropriate CLI --speculative-strategy flags to each example so they match their headings and serve as better copy-paste snippets.

📝 Proposed fix to differentiate the examples
 ### No MTP Baseline
 
 ```bash
-mesh-llm serve meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M --split --no-draft
+mesh-llm serve meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M --split --no-draft \
+  --speculative-strategy disabled

Use [models.speculative] strategy = "disabled" to make this an explicit
baseline instead of relying on environment variables.

Native MTP Only

-mesh-llm serve meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M --split --no-draft
+mesh-llm serve meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M --split --no-draft \
+  --speculative-strategy mtp

Use [models.speculative] strategy = "mtp" to force this control.

MTP With Cache-backed N-gram Extension

-mesh-llm serve meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M --split --no-draft
+mesh-llm serve meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M --split --no-draft \
+  --speculative-strategy mtp-cache
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🤖 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 `@docs/skippy/PIPELINED_VERIFY_WINDOW.md` around lines 287 - 306, Update the
three command examples under “No MTP Baseline,” “Native MTP Only,” and “MTP With
Cache-backed N-gram Extension” so each uses the matching --speculative-strategy
value: disabled, mtp, and mtp-cache respectively, while preserving the existing
command options.

Comment thread scripts/build-release.sh
Comment on lines +141 to +145
export LLAMA_STAGE_BUILD_DIR="$(
LLAMA_STAGE_BACKEND="$BACKEND" \
LLAMA_STAGE_LINK_MODE=static \
"$SCRIPT_DIR/build-llama.sh" --print-build-dir
)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Separate declaration and assignment to avoid masking the return value.

When export and command substitution are combined on the same line, the exit code of export (which is typically 0) will mask any failure from the build-llama.sh script. This prevents set -e from aborting the build if the script fails.

🛠️ Proposed fix to separate assignment
-    export LLAMA_STAGE_BUILD_DIR="$(
+    LLAMA_STAGE_BUILD_DIR="$(
         LLAMA_STAGE_BACKEND="$BACKEND" \
             LLAMA_STAGE_LINK_MODE=static \
             "$SCRIPT_DIR/build-llama.sh" --print-build-dir
     )"
+    export LLAMA_STAGE_BUILD_DIR
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export LLAMA_STAGE_BUILD_DIR="$(
LLAMA_STAGE_BACKEND="$BACKEND" \
LLAMA_STAGE_LINK_MODE=static \
"$SCRIPT_DIR/build-llama.sh" --print-build-dir
)"
LLAMA_STAGE_BUILD_DIR="$(
LLAMA_STAGE_BACKEND="$BACKEND" \
LLAMA_STAGE_LINK_MODE=static \
"$SCRIPT_DIR/build-llama.sh" --print-build-dir
)"
export LLAMA_STAGE_BUILD_DIR
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 141-141: Declare and assign separately to avoid masking return values.

(SC2155)

🤖 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 `@scripts/build-release.sh` around lines 141 - 145, Separate the
LLAMA_STAGE_BUILD_DIR declaration from its command-substitution assignment in
the release build flow. Keep the existing build-llama.sh invocation and
environment variables unchanged, then export LLAMA_STAGE_BUILD_DIR afterward so
its exit status propagates and set -e can detect failures.

Source: Linters/SAST tools

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