Skip to content

Add Ngram Suffix Proposer - #1037

Merged
i386 merged 34 commits into
Mesh-LLM:mainfrom
danielwinterw:feat/suffix-ngram-proposer
Jul 22, 2026
Merged

Add Ngram Suffix Proposer#1037
i386 merged 34 commits into
Mesh-LLM:mainfrom
danielwinterw:feat/suffix-ngram-proposer

Conversation

@danielwinterw

@danielwinterw danielwinterw commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

suffix N-gram draft proposer (prompt-lookup decoding)

Summary

Adds a third N-gram draft proposer, suffix: a pure-Rust longest-suffix matcher (prompt-lookup decoding). simple and cache are bound by llama.cpp's 4-token match window. suffix is not, so it matches verbatim spans up to 64 tokens and copies them as the speculative draft. Additive and default-off.

Motivation

The existing simple/cache proposers wrap llama.cpp's N-gram proposer, capped at a 4-token match window (NGRAM_CACHE_MAX_NGRAM). In long agent transcripts a 4-token match is ambiguous: it occurs in many places with different continuations, so drafts stay short and get rejected.

Agent-coding workloads are dominated by re-emission: read a file, write it back with a small change; echo tool output; repeat identifiers. The output is largely a copy of text already in context. A 16 to 32 token suffix match is usually unique, so it can copy whole spans the model would otherwise decode one token at a time.

What's in the PR

  • SuffixNgramProposer (speculative/suffix.rs): indexes committed history by an exact seed key (no hash collisions), finds the longest verbatim earlier occurrence of the query suffix (up to SUFFIX_NGRAM_MAX_WINDOW = 64), and copies what followed. Draft length scales with match length. Stays silent below ngram_min.
  • Standalone support (speculative/standalone.rs): Cache and Suffix now work as standalone (non-MTP) proposers, not just Simple. A plain ngram_proposer = "suffix" config activates it. No MTP model required.
  • Telemetry: per-request proposer stats (attempts, hits, max/sum match length, proposed/accepted tokens, sync/lookup time) in response timings. Standalone timings report generic proposed/accepted/rejected/acceptance totals so arms are comparable.
  • Two observability fixes made while benchmarking: prefix_cache.enabled = false now survives model-family defaults, and standalone timing fields are populated. Neither changes the lookup algorithm.
  • Config: ngram_proposer = "suffix" (mesh config and CLI). Reuses NgramProposalConfig (min_ngram/max_ngram/max_proposal_tokens).
  • Docs: docs/skippy/SUFFIX_NGRAM_PROPOSER.md, USAGE.md, CONFIGURATION.md.

Benchmarks

Standalone N-gram matrix on GLM-4.7-Flash-MTP-GGUF:Q4_K_M, two-stage split, deterministic re-emit-a-file prompt (rename one function, generate 384 tokens). Medians of 5 measured requests after 2 warmups. Prefix caching disabled so a cross-request restore cannot skew the comparison.

Arm Server decode tok/s End-to-end tok/s Acceptance Proposed / accepted
Target only 22.6 19.9 n/a 0 / 0
Cache 78.0 52.9 67.1% 523 / 351
Simple 107.2 64.4 53.2% 694 / 369
Suffix 119.2 68.5 84.9% 425 / 361

Suffix was the fastest arm: +427% over target-only, +11% over Simple. It had the highest acceptance while proposing the fewest tokens. Lookup cost was about 12.6 microseconds per request.

Simple beat Cache on throughput despite lower acceptance, because it proposed more tokens in absolute terms. Acceptance percentage alone does not rank a proposer.

Reproduced on a local 8B two-stage split (base M1 Pro): about 3.6x over baseline on a realistic file re-emit, neutral on freeform chat.

Not in scope / follow-ups

  • Quality equivalence is not proven. This is a mechanism microbenchmark, not a quality result. Outputs matched on the copied body but diverged at the one edited line before reconverging. Exact greedy-trajectory equivalence is unresolved, so these numbers are not proof of identical output.
  • Standalone only. Not the MTP plus N-gram composite path.
  • Prefix-cache interaction: with caching enabled, a second standalone Simple request returned 502 after a chain_restore_hit. Disabling the cache made sequential requests reliable. Speculative checkpoint state appears to interact with cross-request KV restore. This should be fixed before a cache-enabled rerun.

Testing

  • Unit tests for the proposer (longest-match-wins, long-run edit workload, silence below min, provisional-prefix read-only, incremental sync vs rebuild) and standalone dispatch (simple/cache/suffix), plus config round-trip and validation.
  • Full workspace build and clippy clean.

Config example

toml [models.speculative] strategy = "auto" ngram_proposer = "suffix" ngram_min = 5 ngram_max = 32 ngram_max_proposal_tokens = 48 ​

Summary by CodeRabbit

  • New Features

    • Added ngram-suffix speculative decoding for standalone and native-MTP flows (as ngram-suffix / native-mtp+ngram-suffix).
    • Added a split-deploy advanced --split-topology-lock option.
    • Updated standalone proposal handling to use a single proposal-limit value (instead of min/max) for embedded serving.
  • Bug Fixes

    • Tightened validation for suffix and restricted strategy/proposer combinations.
    • Improved speculative decode/verify safety and KV-cache disable behavior.
  • Documentation

    • Added and expanded suffix proposer docs and configuration guidance.
    • Updated CLI help and config schema references.
  • Telemetry

    • Added suffix proposer and history ngram proposer performance metrics.

michaelneale and others added 18 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.
Adds a third speculative n-gram proposer kind, `suffix`: a pure-Rust
longest-suffix matcher that is not bound by llama.cpp's 4-token match
window (NGRAM_CACHE_MAX_NGRAM). It indexes committed history by a hashed
seed n-gram, finds the longest verbatim earlier occurrence of the query
suffix (up to SUFFIX_NGRAM_MAX_WINDOW = 64), and copies the tokens that
followed it, scaling draft length with match length.

This targets input-grounded, repetitive workloads — re-emitting a file
with a small edit, echoed tool output, repeated identifiers — where a long
match is unambiguous and justifies a long, high-confidence draft. It stays
silent below `ngram_min`, so it is roughly neutral on freeform prose.

Implementation is additive and default-off:
- SuffixNgramProposer + a HistoryNgramProposer enum dispatching Cache/Suffix
  behind the existing propose(committed, prefix, max) contract; call sites
  change mechanically from CachedNgramProposer to the enum.
- No FFI/llama.cpp changes; no changes to transport, verify, or recovery
  paths. Reuses NgramProposalConfig fields (min_ngram/max_ngram/
  max_proposal_tokens).
- Config selectable via `ngram_proposer = "suffix"` (mesh config + CLI).
Documents the suffix proposer in USAGE.md and CONFIGURATION.md with a config
example. Adds evals/skippy-suffix-proposer-bench.py: attaches to running
endpoints, compares off/simple/cache/suffix arms across edit, tool-loop and
chat workloads, reading decode tok/s and acceptance from the server timings.
Requires a >=2-stage split.
@coderabbitai

coderabbitai Bot commented Jul 21, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 098cc1c0-b8ef-42f7-84ad-ee91b2e23f94

📥 Commits

Reviewing files that changed from the base of the PR and between 165db93 and e461bb1.

📒 Files selected for processing (7)
  • 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/skippy-server/src/frontend/speculative.rs
  • crates/skippy-server/src/frontend/speculative/suffix.rs
  • docs/USAGE.md
  • docs/skippy/SUFFIX_NGRAM_PROPOSER.md
💤 Files with no reviewable changes (2)
  • docs/USAGE.md
  • docs/skippy/SUFFIX_NGRAM_PROPOSER.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/skippy-server/src/frontend/speculative/suffix.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

📝 Walkthrough

Walkthrough

Changes

Suffix N-gram speculation is added across configuration, package validation, CLI parsing, standalone serving, native-MTP verification, telemetry, benchmarks, and documentation. Legacy proposer and extension controls are removed, while split-topology lock parsing and explicit disabled KV-cache materialization are added.

Suffix speculation

Layer / File(s) Summary
Configuration and strategy resolution
Cargo.toml, crates/mesh-llm-cli/..., crates/mesh-llm-config/..., crates/mesh-llm-host-runtime/..., crates/skippy-model-package/...
Adds ngram-suffix, validates suffix bounds and request history scope, removes legacy fields, and resolves standalone and native-MTP strategies.
History and suffix proposer implementation
crates/skippy-server/src/frontend/speculative/*, evals/skippy-suffix-proposer-bench.py
Adds indexed suffix matching, incremental synchronization, standalone proposal dispatch, proposer statistics, tests, and benchmark reporting.
Native-MTP integration
crates/skippy-server/src/frontend/native_mtp/*, crates/mesh-llm-host-runtime/tests/*
Reworks composite proposals, verify-window handling, sidecar controls, suffix extensions, and telemetry counters around HistoryNgramProposer.
Embedded pipeline and lifecycle
crates/skippy-server/src/frontend/embedded_generation*, crates/skippy-server/src/frontend/generation/*
Updates pipelined verification, direct prediction paths, cache restoration, decode-position checks, timing totals, and session teardown.
Documentation and fixtures
docs/*, crates/mesh-llm-host-runtime/tests/fixtures/*
Documents suffix configuration, package requirements, lookup behavior, telemetry, benchmarks, and updated schema defaults.

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

Possibly related issues

Possibly related PRs

Suggested labels: experimental

Suggested reviewers: michaelneale

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.45% 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 clearly captures the main addition of a new suffix N-gram proposer and is concise.
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 unit tests (beta)
  • Create PR with unit tests

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/skippy-server/src/frontend/embedded_generation.rs (1)

57-2000: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

generate_embedded_stage_zero_tokens / this file substantially exceed the repo's Rust file-size guideline.

This PR adds new logic (lines 842, 853-855, 1364-1371, 1399, 1419-1428, 1964-1966) inside an already ~2000-line file and a single function spanning the bulk of it. As per coding guidelines, "When modifying a Rust file already over 1,000 lines, extract any separable responsibility into a named module, keep the new file under 1,000 lines, and move or add its tests with the extracted behavior," and "Do not add Rust source files over 2,000 lines; split approaching oversized files by responsibility." Extracting the decode-loop's native-MTP/verify-window/speculative-proposal orchestration (much of which is already split into native_mtp/* submodules) further out of this file/function would help bring it into compliance.

🤖 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/embedded_generation.rs` around lines 57 -
2000, Extract the separable native-MTP, verify-window, and speculative-proposal
orchestration from generate_embedded_stage_zero_tokens into a named module or
helper responsible for decode-loop coordination. Move the associated
implementation and tests with that behavior, keep the extracted Rust file under
1,000 lines, and leave generate_embedded_stage_zero_tokens focused on
request/session and stage orchestration while preserving existing behavior.

Source: Coding guidelines

🧹 Nitpick comments (1)
crates/skippy-server/src/frontend/speculative/suffix.rs (1)

176-198: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unbounded candidate scan in lookup() could degrade on repetitive workloads.

bucket.iter().rev() examines every prior occurrence of the seed with no cap, and each candidate runs an O(max_window) backward comparison. For highly repetitive content (e.g., many prior occurrences of a common 3–8 token seed, plausible in code completion), the per-decode-step proposal cost can grow with total occurrence count for the life of a long request. The existing test retains_useful_matches_beyond_eight_seed_occurrences intentionally exercises this unbounded path, so this is likely a known tradeoff, but a bound on the number of most-recent candidates examined (or a time budget) would cap worst-case latency without materially hurting match quality.

♻️ Sketch: cap candidates examined
-        for &end in bucket.iter().rev() {
+        const MAX_CANDIDATES: usize = 32;
+        for &end in bucket.iter().rev().take(MAX_CANDIDATES) {
             let end = end as usize;
             if end + 1 >= committed_len {
                 continue;
             }
🤖 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/suffix.rs` around lines 176 -
198, Bound the candidate scan in lookup by examining only a fixed number of the
most-recent entries from bucket.iter().rev(), while preserving the existing
filtering and match selection behavior. Define or reuse an appropriate
candidate-limit constant near the lookup logic, increment candidates_examined
only for candidates actually evaluated, and ensure the useful-match behavior
covered by retains_useful_matches_beyond_eight_seed_occurrences remains intact.
🤖 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 207-208: Remove the #[allow(clippy::too_many_lines)] attribute and
split resolve_decode_config into semantically named helpers for its distinct
responsibilities, including ngram-kind resolution, extension-controls
application, and verify-window resolution. Keep resolve_decode_config focused on
orchestration while preserving the existing configuration behavior.

In `@crates/skippy-server/src/frontend/embedded_generation.rs`:
- Around line 1419-1428: Update the speculative request construction in
crates/skippy-server/src/binary_transport/binary_messaging.rs and
crates/skippy-server/src/frontend/generation/server.rs:379-380 so ngram_max
carries speculative.ngram.max_proposal_tokens for every proposer kind, including
Cache and Suffix, or remove the redundant field. In
crates/skippy-server/src/frontend/embedded_generation.rs#L1419-L1428, update the
propose_configured_ngram_tokens call to pass proposal_limit directly without
min(request.ngram_max), preserving the proposer’s internal max_proposal_tokens
enforcement.

---

Outside diff comments:
In `@crates/skippy-server/src/frontend/embedded_generation.rs`:
- Around line 57-2000: Extract the separable native-MTP, verify-window, and
speculative-proposal orchestration from generate_embedded_stage_zero_tokens into
a named module or helper responsible for decode-loop coordination. Move the
associated implementation and tests with that behavior, keep the extracted Rust
file under 1,000 lines, and leave generate_embedded_stage_zero_tokens focused on
request/session and stage orchestration while preserving existing behavior.

---

Nitpick comments:
In `@crates/skippy-server/src/frontend/speculative/suffix.rs`:
- Around line 176-198: Bound the candidate scan in lookup by examining only a
fixed number of the most-recent entries from bucket.iter().rev(), while
preserving the existing filtering and match selection behavior. Define or reuse
an appropriate candidate-limit constant near the lookup logic, increment
candidates_examined only for candidates actually evaluated, and ensure the
useful-match behavior covered by
retains_useful_matches_beyond_eight_seed_occurrences remains intact.
🪄 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: 4852109b-584a-4961-afd6-8f326208730c

📥 Commits

Reviewing files that changed from the base of the PR and between e997c41 and 7afcf95.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • Cargo.toml
  • crates/mesh-llm-cli/src/parser/commands.rs
  • crates/mesh-llm-config/src/model/built_in_schema.rs
  • crates/mesh-llm-config/src/model_validation.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/skippy-model-package/src/preflight.rs
  • crates/skippy-server/Cargo.toml
  • 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/verify_window.rs
  • crates/skippy-server/src/frontend/speculative.rs
  • crates/skippy-server/src/frontend/speculative/standalone.rs
  • crates/skippy-server/src/frontend/speculative/suffix.rs
  • crates/skippy-server/src/frontend/tests/multimodal.rs
  • crates/skippy-server/src/frontend/tests/prompting.rs
  • docs/README.md
  • docs/USAGE.md
  • docs/skippy/CONFIGURATION.md
  • docs/skippy/PIPELINED_VERIFY_WINDOW.md
  • docs/skippy/SUFFIX_NGRAM_PROPOSER.md
  • docs/specs/layer-package-repos.md
  • evals/skippy-suffix-proposer-bench.py
💤 Files with no reviewable changes (2)
  • crates/skippy-server/src/frontend/generation_flow.rs
  • crates/skippy-server/src/frontend/tests/multimodal.rs

Comment thread crates/skippy-server/src/frontend/embedded_generation.rs

i386 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Integrated PR #1026 (68f45027b) with the suffix proposer work and reran the
full eight-arm matrix on the lab. Integration/test head: b83c4ee0e (runtime
binary was 39bd799db; the final commit is docs-only).

Scenario

  • meshllm/GLM-4.7-Flash-MTP-GGUF:Q4_K_M
  • Two-stage 47/1 layer split: M3 Ultra stage 0, M1 Ultra stage 1
  • 8 lanes, ctx 32,768, prefix cache disabled
  • Same deterministic repeated-code edit prompt, temperature 0, seed 42,
    384 generated tokens
  • Sequential release-mode arms, 2 warmups + 5 measured requests each
  • Direct lab LAN, no injected inter-stage delay
  • Every telemetry run contained 7 requests; collector logged no export errors

Results versus the previous runs

Values are median server decode TPS / end-to-end wall TPS.

Arm Previous With #1026 Change
Target only 22.93 / 19.68 22.96 / 20.20 +0.1% / +2.6%
Standalone Simple 107.22 / 64.38 27.45 / 23.54 -74.4% / -63.4%
Standalone Cache 77.96 / 52.94 27.17 / 23.32 -65.1% / -56.0%
Standalone Suffix 119.24 / 68.49 27.33 / 23.49 -77.1% / -65.7%
MTP only 30.53 / 25.84 30.37 / 25.67 -0.5% / -0.7%
MTP + Simple 56.56 / 42.16 30.42 / 25.71 -46.2% / -39.0%
MTP + Cache 64.62 / 46.40 118.71 / 68.92 +83.7% / +48.6%
MTP + Suffix 78.05 / 53.24 138.94 / 75.98 +78.0% / +42.7%

Against the current MTP-only row, MTP+Suffix is 4.57x server TPS and
2.96x wall TPS. MTP+Cache is 3.91x and 2.68x respectively.

@michaelneale: are the stages actually busy in parallel?

For Cache and Suffix, yes; for MTP-only and Simple, no.

Arm N-gram acceptance Max in flight Avg in flight Time at depth 2 Refill successes / attempts Stale windows
MTP only 1 0.49 0.0% 0 / 0 0
MTP + Simple no N-gram tokens 1 0.50 0.0% 0 / 0 0
MTP + Cache 59.3% 2 1.12 20.5% 25 / 180 15
MTP + Suffix 72.2% 2 1.29 51.2% 50 / 55 10

Suffix contributes 2,135 refill tokens across the five measured requests,
versus Cache's 355. That longer horizon is what keeps the positional pipeline
occupied. Suffix lookup cost is only 34 microseconds total across those five
requests, so hashing/index lookup is not the bottleneck.

The standalone regression is also revealing: Cache and Suffix accept 96.4%
and 98.9% of standalone proposals, yet both remain around 27 server TPS. High
acceptance without overlapping stage work is not useful. The standalone path
still needs to be moved onto the positional pipeline. MTP+Simple is configured
as hybrid but records zero refill attempts/tokens, so it is not an active
extension under #1026 and should not be presented as one.

This rerun proves the pipeline mechanism, but not the controlled-WAN claim:
injected delay was 0 ms. The next experiment must repeat the same matrix at
20/100 ms and report occupancy, downstream wait, and stale work. The prior run
showed Suffix beating Cache despite slightly lower tail acceptance; this rerun
does not reproduce that lower-acceptance ordering—Suffix now has both higher
acceptance and higher occupancy. What it does show clearly is that occupancy,
not acceptance alone, predicts the large TPS gain.

Correctness caveat: every arm was deterministic within-arm. Target-only, all
standalone arms, MTP-only, MTP+Simple, and MTP+Cache share one output hash;
MTP+Suffix differs at the requested edit. Both 384-token generated fragments
are malformed, so these are throughput/scheduling results, not a quality or
exact-greedy-equivalence result.

@ndizazzo

Copy link
Copy Markdown
Collaborator

We might want to consider targeting this PR to #1026 for the time being since these were integrated... Once #1026 is merged GitHub will retarget main as the base.

Suffix lookup now early-exits once a full-window match is found, bounding
scan cost on repetitive content. Keeps the full scan otherwise so older,
longer matches are still found (retains_useful_matches_beyond_eight_seed_occurrences).

Adds docstrings across the N-gram proposer surface (suffix, standalone, and
the speculative config types) for the docstring-coverage gate.
@danielwinterw
danielwinterw changed the base branch from main to agent/positional-mtp-ngram July 21, 2026 22:40
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
michaelneale added a commit that referenced this pull request Jul 22, 2026
Transport-core status/handoff for PR #1028: what is proven over WAN, the
landed keep-set, why fixed pipeline depth is diagnostic-only, the go-forward
suffix-ngram + adaptive-depth direction (#1037), and the 2-node bringup config
trap. Draft-model work is deferred on wip/wan-draft-ahead.

Assisted-by: goose
i386 and others added 5 commits July 22, 2026 12:29
The binary transport only filled ngram_min/ngram_max when the proposer
kind was Simple, so embedded stage-0 requests with cache or suffix
configs arrived with ngram_max=0 and the standalone fallback proposed
nothing. Mirror the host runtime translation and pass the configured
limits through for every proposer kind.
…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.
…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
michaelneale deleted the branch Mesh-LLM:main July 22, 2026 08:32
@michaelneale michaelneale reopened this Jul 22, 2026
@danielwinterw
danielwinterw changed the base branch from agent/positional-mtp-ngram to main July 22, 2026 08:53
…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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/skippy-server/src/kv_integration/config.rs (1)

79-92: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Fail closed when recurrent-state detection is inconclusive.

model_requires_recurrent_state returns false when model inspection cannot open the model or enumerate tensors. In crates/skippy-server/src/binary_transport/binary_messaging/connection.rs, Line 106 negates this result and therefore treats an unknown model as safe for VerifyWindow. Use a fallible or tri-state helper for this protocol gate and reject when inspection is inconclusive; retain the permissive fallback only where it cannot enable positional speculation.

🤖 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/kv_integration/config.rs` around lines 79 - 92,
Update model_requires_recurrent_state and its VerifyWindow caller in the binary
messaging connection flow to distinguish confirmed non-recurrent models from
inconclusive inspection. Propagate inspection failures as an unknown/error
state, and make the protocol gate reject unknown models instead of negating the
result to allow them; preserve permissive behavior only for callers that cannot
enable positional speculation.
crates/mesh-llm-config/src/model_validation.rs (1)

419-427: 🎯 Functional Correctness | 🟠 Major

Removing "ngram" from mode's accepted values — same compatibility concern as built_in_schema.rs.

See the companion comment on crates/mesh-llm-config/src/model/built_in_schema.rs (lines 604-610) — this is the enforcement half of the same schema tightening and carries the same backward-compatibility risk for existing speculative.mode = "ngram" configs.

🤖 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/mesh-llm-config/src/model_validation.rs` around lines 419 - 427,
Update validate_speculative to preserve “ngram” as an accepted value for
config.mode, matching the compatibility behavior required by the companion
built-in schema definition. Keep the existing validation for “auto”, “disabled”,
and “draft” unchanged.
♻️ Duplicate comments (1)
crates/mesh-llm-host-runtime/src/inference/skippy/resolver/speculative.rs (1)

196-207: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

#[allow(clippy::too_many_lines)] on resolve_decode_config — previously flagged, still unresolved.

resolve_decode_config grew further with the suffix-proposer handling added in this PR; the #[allow(...)] still bypasses the length/complexity guardrail instead of extracting the already-distinct sub-blocks (ngram-kind resolution, extension-controls application, verify-window resolution) into named helpers.

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" and "do not use #[allow(...)] to silence them without a clear reason and developer approval."

🤖 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/mesh-llm-host-runtime/src/inference/skippy/resolver/speculative.rs`
around lines 196 - 207, Remove the #[allow(clippy::too_many_lines)] attribute
from resolve_decode_config and split its distinct logic into semantically named
helpers. Extract ngram-kind resolution, extension-controls application, and
verify-window resolution while preserving the existing configuration precedence
and behavior.

Source: Coding guidelines

🧹 Nitpick comments (6)
crates/mesh-llm-host-runtime/src/runtime/startup_handles.rs (1)

1242-1412: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider extracting the relaunch loop/outcome handling into a named submodule. This file is already well over 1,000 lines and this change adds the launch loop plus startup_resolve_loop_outcome/StartupLoopOutcome relaunch logic to it. As per coding guidelines, "When modifying a Rust file already over 1,000 lines, extract any separable responsibility into a named module, keep the new file under 1,000 lines, and move or add its tests with the extracted behavior." The split-relaunch lifecycle is a natural separable responsibility.

🤖 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/mesh-llm-host-runtime/src/runtime/startup_handles.rs` around lines
1242 - 1412, Extract the split-relaunch lifecycle from the large startup_handles
module into a named Rust submodule, including the launch loop,
startup_resolve_loop_outcome, StartupLoopOutcome, and their related
state/helpers. Update call sites and visibility/imports so behavior remains
unchanged, and move or add tests for the extracted relaunch behavior in the new
module, keeping it under 1,000 lines.

Source: Coding guidelines

crates/mesh-llm-host-runtime/src/runtime/split_planning.rs (1)

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

Consider deduplicating the shared post-planning tail. plan_locked_runtime_slice_topology_with_resources and plan_runtime_slice_topology_with_resources differ only in the planning call (plan_locked_topology vs plan_runtime_slice_topology_result); the participant_by_id/map_runtime_slice_stages/sort_by_key/validate_split_capacity/tracing tail is identical. Extracting a helper that takes the resolved TopologyPlan (or stages) would keep the two paths from drifting.

🤖 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/mesh-llm-host-runtime/src/runtime/split_planning.rs` around lines 208
- 256, The post-planning logic in
plan_locked_runtime_slice_topology_with_resources and
plan_runtime_slice_topology_with_resources is duplicated and should be
centralized. Extract a helper that accepts the resolved TopologyPlan (or mapped
stages plus required metadata) and performs participant indexing, stage mapping
and sorting, validate_split_capacity, validation tracing, and
PlannedRuntimeSliceTopology construction; have both functions invoke it after
their distinct planning calls while preserving existing inputs and behavior.
crates/skippy-bench/src/telemetry_report.rs (1)

143-166: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider an explicit timeout on the finalize call.

finalize_only builds a bare reqwest::blocking::Client::new() with no explicit timeout configured. Depending on the exact reqwest 0.12 default for blocking::Client, this may already carry an implicit ~30s timeout, but that's not obvious from the call site and isn't tuned for this endpoint (which may need longer if the metrics server is still flushing spans under load). An explicit .timeout(...) on the client/request would make the behavior self-documenting and tunable independent of the library default.

🤖 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/telemetry_report.rs` around lines 143 - 166, Update
finalize_only’s reqwest blocking client configuration to apply an explicit,
self-documenting timeout suitable for metrics-server finalization, using the
project’s existing timeout constant or configuration if available. Preserve the
current finalize request flow and error handling while ensuring the client
construction propagates any configuration error correctly.
third_party/llama.cpp/patches/0018-Expose-stateful-N-gram-cache-ABI.patch (1)

31-38: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

cache->dynamic / cache->static_cache are allocated but never populated.

skippy_ngram_cache_update (called from both reset and append) only feeds cache->context. cache->dynamic and cache->static_cache stay empty for the lifetime of the cache, yet skippy_ngram_cache_draft still passes them into common_ngram_cache_draft, which (per upstream lookup-decoding design) treats nc_dynamic as a second, lower-confidence fallback tier. With dynamic always empty, that fallback tier can never contribute a draft, and the strict/lax threshold split reinforced by patch 0021 has no effect for this path — the cache degrades to context-only lookup.

If this single-tier behavior is intentional for the stateful per-session cache, consider documenting it explicitly (and dropping the unused fields/params to avoid confusion); if not, dynamic should also be updated on append.

Also applies to: 44-84, 118-163

🤖 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 `@third_party/llama.cpp/patches/0018-Expose-stateful-N-gram-cache-ABI.patch`
around lines 31 - 38, Update skippy_ngram_cache_update, including its reset and
append paths, so cache->dynamic is populated when tokens are appended and
available to skippy_ngram_cache_draft as the lower-confidence fallback tier.
Keep cache->context behavior unchanged, and either populate cache->static_cache
according to its intended tier semantics or remove/document it if single-tier
behavior is deliberate.
crates/mesh-llm-host-runtime/src/inference/skippy/resolver/native_mtp_tests.rs (1)

398-406: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Restore the removed proposer-kind assertion.

The assertion that the resolved cache proposer's ngram.kind equals Cache was dropped from package_composite_strategy_resolves_native_mtp_with_cache_extension. This test is specifically about composite (mtp+cache) resolution, where verifying the correct proposer kind was actually selected is a meaningful regression guard distinct from just checking min_ngram/max_ngram/max_proposal_tokens.

✅ Suggested restoration
     let ngram = resolved
         .speculative
         .decode
         .ngram
         .as_ref()
         .expect("cache proposer should resolve");
+    assert_eq!(ngram.kind, skippy_server::NgramProposerKind::Cache);
     assert_eq!(ngram.min_ngram, 2);
     assert_eq!(ngram.max_ngram, 4);
     assert_eq!(ngram.max_proposal_tokens, 9);
🤖 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/mesh-llm-host-runtime/src/inference/skippy/resolver/native_mtp_tests.rs`
around lines 398 - 406, Restore the proposer-kind assertion in
package_composite_strategy_resolves_native_mtp_with_cache_extension by verifying
the resolved ngram.kind is Cache alongside the existing ngram configuration
assertions. Keep the current min_ngram, max_ngram, and max_proposal_tokens
checks unchanged.
crates/mesh-llm-config/src/model_validation.rs (1)

541-575: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Suffix window limits (min >= 3, max <= 64) are hardcoded here and duplicated in skippy-model-package/preflight.rs.

Both validate_speculative_proposer_controls here and validate_ngram_proposer in crates/skippy-model-package/src/preflight.rs (lines 837-867, per the emitted unsupported_ngram_suffix_window check) independently hardcode the same 3/64 bounds. Extracting a shared constant (e.g., re-exported from the crate that owns the actual suffix-proposer runtime limit) would prevent the two validators from silently drifting apart if the limit changes.

🤖 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/mesh-llm-config/src/model_validation.rs` around lines 541 - 575,
Extract the suffix proposer window bounds used by
validate_speculative_proposer_controls into shared constants owned by the crate
defining the runtime limit, then reuse those constants in both this validation
and validate_ngram_proposer in preflight.rs. Remove the duplicated literal 3/64
bounds while preserving the existing min/max validation behavior and
diagnostics.
🤖 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-cli/src/lib.rs`:
- Around line 15-16: Restore the crate-root re-export for
SpeculativeNgramProposerCli in the public exports of lib.rs so downstream
mesh_llm_cli::SpeculativeNgramProposerCli imports continue to compile;
alternatively, bump the crate’s major version if the removal is intentional.

In `@crates/mesh-llm-host-runtime/src/inference/skippy/resolver/speculative.rs`:
- Around line 143-145: Update the n-gram mode override in the speculative
resolver and the n-gram-building gate in resolve_decode_config to honor
requested_strategy. When strategy is explicitly "disabled", do not build n-gram
configuration, derive n-gram effective strategies, or force mode to "ngram";
preserve the existing behavior for other strategies.

In `@crates/mesh-llm-host-runtime/src/runtime/model_lifecycle.rs`:
- Line 407: Update the runtime-loaded model lifecycle path where
split_topology_lock is set to None so it forwards
ctx.options.split_topology_lock to the planner. Preserve fail-closed behavior
when a topology lock is configured, ensuring local_split.rs selects the locked
planner; alternatively, explicitly reject runtime loads under a configured lock.

In `@crates/mesh-llm-host-runtime/src/runtime/run_auto.rs`:
- Around line 601-604: Extract the node/plugin startup orchestration around
run_auto in crates/mesh-llm-host-runtime/src/runtime/run_auto.rs (anchor lines
601-604) into a semantic run_auto submodule, keeping the new module under 1,000
lines and preserving behavior. Also extract the additional-model startup
orchestration around crates/mesh-llm-host-runtime/src/runtime/serving_surface.rs
line 1128 into a semantic module, likewise keeping it under 1,000 lines; both
sites require refactoring.

In `@docs/SKIPPY_SPLITS.md`:
- Around line 94-102: Replace the private .local hostnames in the topology-lock
example at docs/SKIPPY_SPLITS.md lines 94-102 with neutral placeholders such as
<node-a> and <node-b>. Make the same replacement in
website/src/docs/pages/CLI.md lines 225-234 and explain there that users must
provide their own node selectors.

In `@SKIPPY_PROTOCOL_TODO.md`:
- Around line 158-165: Update the completed “Make speculative positions
authoritative in stage-state” checklist entry in SKIPPY_PROTOCOL_TODO.md from
stage-state v9 to v10, preserving the existing authoritative-position semantics
and compatibility guidance.

---

Outside diff comments:
In `@crates/mesh-llm-config/src/model_validation.rs`:
- Around line 419-427: Update validate_speculative to preserve “ngram” as an
accepted value for config.mode, matching the compatibility behavior required by
the companion built-in schema definition. Keep the existing validation for
“auto”, “disabled”, and “draft” unchanged.

In `@crates/skippy-server/src/kv_integration/config.rs`:
- Around line 79-92: Update model_requires_recurrent_state and its VerifyWindow
caller in the binary messaging connection flow to distinguish confirmed
non-recurrent models from inconclusive inspection. Propagate inspection failures
as an unknown/error state, and make the protocol gate reject unknown models
instead of negating the result to allow them; preserve permissive behavior only
for callers that cannot enable positional speculation.

---

Duplicate comments:
In `@crates/mesh-llm-host-runtime/src/inference/skippy/resolver/speculative.rs`:
- Around line 196-207: Remove the #[allow(clippy::too_many_lines)] attribute
from resolve_decode_config and split its distinct logic into semantically named
helpers. Extract ngram-kind resolution, extension-controls application, and
verify-window resolution while preserving the existing configuration precedence
and behavior.

---

Nitpick comments:
In `@crates/mesh-llm-config/src/model_validation.rs`:
- Around line 541-575: Extract the suffix proposer window bounds used by
validate_speculative_proposer_controls into shared constants owned by the crate
defining the runtime limit, then reuse those constants in both this validation
and validate_ngram_proposer in preflight.rs. Remove the duplicated literal 3/64
bounds while preserving the existing min/max validation behavior and
diagnostics.

In
`@crates/mesh-llm-host-runtime/src/inference/skippy/resolver/native_mtp_tests.rs`:
- Around line 398-406: Restore the proposer-kind assertion in
package_composite_strategy_resolves_native_mtp_with_cache_extension by verifying
the resolved ngram.kind is Cache alongside the existing ngram configuration
assertions. Keep the current min_ngram, max_ngram, and max_proposal_tokens
checks unchanged.

In `@crates/mesh-llm-host-runtime/src/runtime/split_planning.rs`:
- Around line 208-256: The post-planning logic in
plan_locked_runtime_slice_topology_with_resources and
plan_runtime_slice_topology_with_resources is duplicated and should be
centralized. Extract a helper that accepts the resolved TopologyPlan (or mapped
stages plus required metadata) and performs participant indexing, stage mapping
and sorting, validate_split_capacity, validation tracing, and
PlannedRuntimeSliceTopology construction; have both functions invoke it after
their distinct planning calls while preserving existing inputs and behavior.

In `@crates/mesh-llm-host-runtime/src/runtime/startup_handles.rs`:
- Around line 1242-1412: Extract the split-relaunch lifecycle from the large
startup_handles module into a named Rust submodule, including the launch loop,
startup_resolve_loop_outcome, StartupLoopOutcome, and their related
state/helpers. Update call sites and visibility/imports so behavior remains
unchanged, and move or add tests for the extracted relaunch behavior in the new
module, keeping it under 1,000 lines.

In `@crates/skippy-bench/src/telemetry_report.rs`:
- Around line 143-166: Update finalize_only’s reqwest blocking client
configuration to apply an explicit, self-documenting timeout suitable for
metrics-server finalization, using the project’s existing timeout constant or
configuration if available. Preserve the current finalize request flow and error
handling while ensuring the client construction propagates any configuration
error correctly.

In `@third_party/llama.cpp/patches/0018-Expose-stateful-N-gram-cache-ABI.patch`:
- Around line 31-38: Update skippy_ngram_cache_update, including its reset and
append paths, so cache->dynamic is populated when tokens are appended and
available to skippy_ngram_cache_draft as the lower-confidence fallback tier.
Keep cache->context behavior unchanged, and either populate cache->static_cache
according to its intended tier semantics or remove/document it if single-tier
behavior is deliberate.
🪄 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: 6d692003-5687-455b-a283-f174574273db

📥 Commits

Reviewing files that changed from the base of the PR and between bf13960 and 05d0e09.

📒 Files selected for processing (137)
  • SKIPPY_PROTOCOL_TODO.md
  • crates/llama-spec-bench/README.md
  • crates/llama-spec-bench/src/main.rs
  • 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/inference/skippy/stage/mod.rs
  • crates/mesh-llm-host-runtime/src/mesh/stage_transport.rs
  • crates/mesh-llm-host-runtime/src/plugin/config.rs
  • crates/mesh-llm-host-runtime/src/runtime/local.rs
  • crates/mesh-llm-host-runtime/src/runtime/local/native_runtime_events/tests.rs
  • crates/mesh-llm-host-runtime/src/runtime/local_split.rs
  • crates/mesh-llm-host-runtime/src/runtime/local_split/coordinator.rs
  • crates/mesh-llm-host-runtime/src/runtime/local_split/recovery.rs
  • crates/mesh-llm-host-runtime/src/runtime/local_split/test_support.rs
  • crates/mesh-llm-host-runtime/src/runtime/local_split/tests.rs
  • crates/mesh-llm-host-runtime/src/runtime/mod.rs
  • crates/mesh-llm-host-runtime/src/runtime/model_lifecycle.rs
  • crates/mesh-llm-host-runtime/src/runtime/options.rs
  • crates/mesh-llm-host-runtime/src/runtime/run_auto.rs
  • crates/mesh-llm-host-runtime/src/runtime/serving_surface.rs
  • crates/mesh-llm-host-runtime/src/runtime/split_planning.rs
  • crates/mesh-llm-host-runtime/src/runtime/split_topology_lock.rs
  • crates/mesh-llm-host-runtime/src/runtime/startup_handles.rs
  • crates/mesh-llm-host-runtime/src/runtime/startup_models.rs
  • crates/mesh-llm-host-runtime/src/runtime/survey.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-system/src/autoupdate.rs
  • 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-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/telemetry_report.rs
  • crates/skippy-coordinator/src/topology.rs
  • crates/skippy-coordinator/src/topology/locked.rs
  • crates/skippy-ffi/README.md
  • crates/skippy-ffi/src/lib.rs
  • crates/skippy-metrics/src/lib.rs
  • crates/skippy-model-package/src/package.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/package.rs
  • crates/skippy-runtime/src/session.rs
  • crates/skippy-server/README.md
  • 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/binary_messaging/reply.rs
  • crates/skippy-server/src/binary_transport/binary_messaging/telemetry.rs
  • crates/skippy-server/src/binary_transport/decode_batcher.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/stage_execution.rs
  • crates/skippy-server/src/binary_transport/wire.rs
  • crates/skippy-server/src/cli.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/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/standalone.rs
  • crates/skippy-server/src/frontend/tests/prefill.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/kv_integration/resident_prefix.rs
  • crates/skippy-server/src/runtime_state.rs
  • docs/CLI.md
  • docs/SKIPPY_SPLITS.md
  • docs/USAGE.md
  • docs/design/TESTING.md
  • docs/plugins/telemetry.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
  • 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
  • website/src/docs/pages/CLI.md
  • website/src/docs/pages/config-defaults.md
💤 Files with no reviewable changes (17)
  • docs/specs/speculative-decoding-wiring-plan.md
  • crates/skippy-model-package/src/package.rs
  • crates/skippy-prompt/src/prompt_cli/topology.rs
  • crates/mesh-llm-host-runtime/tests/fixtures/schema_driven_controls_valid.toml
  • docs/skippy/speculative_decoding.md
  • crates/mesh-llm-host-runtime/tests/fixtures/config_schema_defaults_ui_reference.json
  • crates/skippy-prompt/src/prompt_cli/draft.rs
  • crates/skippy-server/src/frontend/backend.rs
  • third_party/llama.cpp/patches/0017-Expose-upstream-ngram-simple-draft-ABI.patch
  • crates/skippy-runtime/src/package.rs
  • crates/skippy-prompt/src/prompt_cli/launch.rs
  • crates/skippy-prompt/src/prompt_cli/binary_repl.rs
  • crates/skippy-server/src/frontend/wire_messages.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/skippy-prompt/src/prompt_cli/args.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/specs/layer-package-repos.md
  • docs/USAGE.md

Comment on lines +15 to +16
RuntimeSurface, SkillAgentArg, SkillCommand, TrustCommand, TrustPolicy,
legacy_runtime_surface_warning, normalize_runtime_surface_args, validate_discovery_mode_args,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== crates/mesh-llm-cli/src/lib.rs ==\n'
sed -n '1,120p' crates/mesh-llm-cli/src/lib.rs

printf '\n== Search for SpeculativeNgramProposerCli ==\n'
rg -n "SpeculativeNgramProposerCli" crates/mesh-llm-cli -S

printf '\n== Search for crate-root imports of mesh_llm_cli::SpeculativeNgramProposerCli ==\n'
rg -n "mesh_llm_cli::SpeculativeNgramProposerCli|use\s+mesh_llm_cli::\{[^}]*SpeculativeNgramProposerCli|use\s+mesh_llm_cli::SpeculativeNgramProposerCli" -S .

Repository: Mesh-LLM/mesh-llm

Length of output: 1253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' crates/mesh-llm-cli/src/lib.rs
printf '\n---\n'
rg -n "SpeculativeNgramProposerCli" crates/mesh-llm-cli -S
printf '\n---\n'
rg -n "mesh_llm_cli::SpeculativeNgramProposerCli|use\s+mesh_llm_cli::\{[^}]*SpeculativeNgramProposerCli|use\s+mesh_llm_cli::SpeculativeNgramProposerCli" -S .

Repository: Mesh-LLM/mesh-llm

Length of output: 1097


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('crates/mesh-llm-cli/src/lib.rs')
print(p.read_text().splitlines()[:40])
PY

Repository: Mesh-LLM/mesh-llm

Length of output: 749


Keep the crate-root re-export or bump the major version for this API break. Removing mesh_llm_cli::SpeculativeNgramProposerCli breaks downstream imports from the crate root.

🤖 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/mesh-llm-cli/src/lib.rs` around lines 15 - 16, Restore the crate-root
re-export for SpeculativeNgramProposerCli in the public exports of lib.rs so
downstream mesh_llm_cli::SpeculativeNgramProposerCli imports continue to
compile; alternatively, bump the crate’s major version if the removal is
intentional.

.and_then(|m| m.flash_attention)
.unwrap_or(FlashAttentionType::Auto),
parallel_override,
split_topology_lock: None,

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 | 🏗️ Heavy lift

Do not bypass the configured topology lock for runtime-loaded models.

Line 407 forces unlocked planning even when ctx.options.split_topology_lock is set; local_split.rs:307-333 consequently takes the non-locked planner. Forward the configured lock (or explicitly reject runtime loads under a lock) so a lock remains fail-closed.

Proposed fix
-            split_topology_lock: None,
+            split_topology_lock: ctx.options.split_topology_lock.as_deref(),
📝 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
split_topology_lock: None,
split_topology_lock: ctx.options.split_topology_lock.as_deref(),
🤖 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/mesh-llm-host-runtime/src/runtime/model_lifecycle.rs` at line 407,
Update the runtime-loaded model lifecycle path where split_topology_lock is set
to None so it forwards ctx.options.split_topology_lock to the planner. Preserve
fail-closed behavior when a topology lock is configured, ensuring local_split.rs
selects the locked planner; alternatively, explicitly reject runtime loads under
a configured lock.

Comment on lines +601 to +604
node.set_stage_control_sender(skippy::spawn_stage_control_loop(
Some(Arc::new(node.clone())),
skippy_telemetry_options(options),
))

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 | 🟠 Major | 🏗️ Heavy lift

Extract responsibilities from the modified oversized Rust modules. Both files exceed 1,000 lines after modification and contain separable orchestration responsibilities.

  • crates/mesh-llm-host-runtime/src/runtime/run_auto.rs#L601-L604: move node/plugin startup orchestration into a semantic run_auto submodule.
  • crates/mesh-llm-host-runtime/src/runtime/serving_surface.rs#L1128-L1128: move additional-model startup orchestration into a semantic module.

As per coding guidelines, when modifying a Rust file already over 1,000 lines, extract any separable responsibility into a named module and keep the new file under 1,000 lines.

📍 Affects 2 files
  • crates/mesh-llm-host-runtime/src/runtime/run_auto.rs#L601-L604 (this comment)
  • crates/mesh-llm-host-runtime/src/runtime/serving_surface.rs#L1128-L1128
🤖 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/mesh-llm-host-runtime/src/runtime/run_auto.rs` around lines 601 - 604,
Extract the node/plugin startup orchestration around run_auto in
crates/mesh-llm-host-runtime/src/runtime/run_auto.rs (anchor lines 601-604) into
a semantic run_auto submodule, keeping the new module under 1,000 lines and
preserving behavior. Also extract the additional-model startup orchestration
around crates/mesh-llm-host-runtime/src/runtime/serving_surface.rs line 1128
into a semantic module, likewise keeping it under 1,000 lines; both sites
require refactoring.

Source: Coding guidelines

Comment thread docs/SKIPPY_SPLITS.md
Comment on lines +94 to +102
{
"node": "micstudio.local",
"layer_start": 0,
"layer_end": 31
},
{
"node": "studio54-3.local",
"layer_start": 31,
"layer_end": 47

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove private hostnames from both tracked examples.

The topology-lock examples use machine-specific .local hostnames instead of neutral placeholders, disclosing private connection details in repository documentation.

  • docs/SKIPPY_SPLITS.md#L94-L102: replace micstudio.local and studio54-3.local with placeholders such as <node-a> and <node-b>.
  • website/src/docs/pages/CLI.md#L225-L234: make the same replacement and explain that users must provide their own selectors.

As per coding guidelines, **/*: Never commit credentials or private machine connection details to tracked files; keep them outside the repository.

📍 Affects 2 files
  • docs/SKIPPY_SPLITS.md#L94-L102 (this comment)
  • website/src/docs/pages/CLI.md#L225-L234
🤖 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_SPLITS.md` around lines 94 - 102, Replace the private .local
hostnames in the topology-lock example at docs/SKIPPY_SPLITS.md lines 94-102
with neutral placeholders such as <node-a> and <node-b>. Make the same
replacement in website/src/docs/pages/CLI.md lines 225-234 and explain there
that users must provide their own node selectors.

Source: Coding guidelines

Comment thread SKIPPY_PROTOCOL_TODO.md
Comment on lines +158 to +165
- [x] Make speculative positions authoritative in stage-state v9.
- Decode and `VerifyWindow` messages carry the absolute position each stage
must have before execution.
- Stages ahead of that position rewind attention KV locally.
- Speculation never checkpoints, restores, trims by control message, or
replays a rejected prefix.
- Recurrent-state stages reject positional speculation instead of falling
back to the removed checkpoint protocol.

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

Correct the stage-state generation.

This section says v9, but the PR’s serving-pipeline contract is stage-state v10. Labeling the authoritative-position semantics as v9 makes the protocol rollout and compatibility guidance ambiguous.

🤖 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 `@SKIPPY_PROTOCOL_TODO.md` around lines 158 - 165, Update the completed “Make
speculative positions authoritative in stage-state” checklist entry in
SKIPPY_PROTOCOL_TODO.md from stage-state v9 to v10, preserving the existing
authoritative-position semantics and compatibility guidance.

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.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/USAGE.md (1)

754-763: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove unsupported confidence and scaling claims.

The proposer contract supports exact matching with an independent max_proposal_tokens cap; it does not establish “high-confidence” drafts or that draft length scales with match length. This also conflicts with the preceding statement that match length and continuation length are separate controls. Reword this to describe potential long drafts without implying guaranteed quality or sizing behavior.

🤖 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/USAGE.md` around lines 754 - 763, Revise the `suffix` proposer
documentation to remove claims that drafts are “high-confidence” or that draft
length scales with match length. Describe only that exact long suffix matches
may produce long drafts, subject to the independent `max_proposal_tokens` cap,
while preserving the documented `ngram_min` and `ngram_max` match controls.
🤖 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 `@docs/USAGE.md`:
- Around line 749-752: Update the N-gram settings documentation near the
request-local cache description to qualify the output-correctness statement:
describe target verification as the intended safeguard, and avoid claiming that
tuning these values is proven not to affect correctness until exact-greedy
equivalence is validated.

---

Outside diff comments:
In `@docs/USAGE.md`:
- Around line 754-763: Revise the `suffix` proposer documentation to remove
claims that drafts are “high-confidence” or that draft length scales with match
length. Describe only that exact long suffix matches may produce long drafts,
subject to the independent `max_proposal_tokens` cap, while preserving the
documented `ngram_min` and `ngram_max` match controls.
🪄 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: 647a354c-ba0c-4a95-b72a-4a40ef5a3ff8

📥 Commits

Reviewing files that changed from the base of the PR and between 05d0e09 and 165db93.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • crates/skippy-model-package/src/preflight.rs
  • docs/USAGE.md
  • docs/skippy/CONFIGURATION.md
  • docs/skippy/SUFFIX_NGRAM_PROPOSER.md
  • docs/specs/layer-package-repos.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/skippy/SUFFIX_NGRAM_PROPOSER.md
  • crates/skippy-model-package/src/preflight.rs

Comment thread docs/USAGE.md
Comment on lines +749 to +752
The request-local cache is limited to `ngram_max <= 4`. N-gram settings may run
standalone or, with native MTP, form one composite proposal. All combinations
are verified together by the target, so tuning these values changes speculative
work, not output correctness.

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

Qualify the output-correctness guarantee.

This states that tuning N-gram settings cannot affect output correctness, but the PR objectives explicitly say exact-greedy equivalence has not been proven. Document target verification as the intended safeguard, not as an established guarantee, until equivalence is validated.

🤖 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/USAGE.md` around lines 749 - 752, Update the N-gram settings
documentation near the request-local cache description to qualify the
output-correctness statement: describe target verification as the intended
safeguard, and avoid claiming that tuning these values is proven not to affect
correctness until exact-greedy equivalence is validated.

@micspiral

Copy link
Copy Markdown

Minor: a couple of stale doc comments still reference simple after this re-port dropped NgramProposerKind::Simple:

  • speculative/standalone.rs:21 — "Runs the configured standalone N-gram proposer (simple, cache, or suffix)"
  • speculative.rs:459from_config says "or None for simple/no proposer"

simple no longer exists as a kind, so these should just say cache/suffix. Trivial, non-blocking.

@i386 i386 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.

Requesting changes for the following issues:

  1. P1 — Standalone N-gram is silently ignored for single-stage/direct serving. speculative_mode_for_embedded enables N-gram regardless of staged, but a stage without downstream immediately delegates to generate_local_tokens, which has no N-gram path. Direct GGUF and one-stage package requests therefore run target-only despite reporting ngram-cache/ngram-suffix. Please implement local verification or reject this configuration outside multi-stage serving.

  2. P1 — strategy = "disabled" can be overridden by inherited N-gram settings. resolve_decode_config builds an N-gram plan from defaults without checking the requested strategy, after which the mode override re-enables speculation. A model-level disable must clear or ignore inherited proposer state. This corroborates the existing unresolved inline thread.

  3. P2 — Suffix lookup remains unbounded on ambiguous repetitive input. Every indexed occurrence is examined unless one reaches the full max_window. Repeated seeds whose preceding tokens differ scan the entire bucket on every decode step, yielding O(generated_tokens × candidate_occurrences × max_window) work for request-controlled prompts. Please add a hard candidate budget or another bounded index strategy.

  4. P2 — Existing speculative-plan JSON is no longer readable. NgramProposalConfig.kind is newly required, while plans produced by previous releases contain only min_ngram, max_ngram, and max_proposal_tokens. --openai-speculative-config will fail deserialization. A serde default of Cache would preserve prior behavior.

  5. P2 — Explicit standalone strategies can silently become disabled. With strategy = "ngram-cache" or "ngram-suffix", no package metadata, and either bound omitted, N-gram construction is skipped and resolution succeeds with no proposer. An explicitly requested strategy should fail with the documented “both bounds required” error.

  6. P2 — Two copy-paste configuration examples contain rejected keys. extension_initial_tokens and extension_tail_backoff_proposals appear in docs/skippy/SUFFIX_NGRAM_PROPOSER.md and docs/USAGE.md, but do not exist in SpeculativeConfigRaw, which denies unknown fields.

Reviewed head 165db9320e170a1334c376f6857cb8daa85a324f.

P1 fixes:
- Reject standalone N-gram on single-stage/direct serving. The no-downstream
  path (generate_local_tokens) has no N-gram verification, so ensure_embedded_
  openai_safe now errors for a standalone proposer when !staged instead of
  silently running target-only; speculative_mode_for_embedded is also gated on
  staged.
- strategy = "disabled" no longer inherits proposer state. resolve_decode_config
  short-circuits for a disabled request, clearing ngram/extension/native-MTP so
  the mode override cannot re-enable speculation from [defaults] or package
  metadata.

P2 fixes:
- Explicit ngram-cache / ngram-suffix strategies now force proposer construction,
  so omitting a bound fails with the both-bounds-required error instead of
  resolving to no proposer.
- NgramProposalConfig.kind gets a serde default of Cache, so speculative plans
  written before the field existed still deserialize (--openai-speculative-config).
- Bound the suffix candidate scan at 64 occurrences per lookup (most-recent-first)
  so ambiguous repetitive input can't scan the whole bucket every decode step.
- Remove the rejected extension_initial_tokens / extension_tail_backoff_proposals
  keys from the USAGE and suffix-proposer doc examples.

Adds tests for each: single-stage rejection, disabled-clears-inherited-ngram,
explicit-without-bounds error, legacy-JSON kind default, and the suffix scan cap.
@danielwinterw
danielwinterw requested a review from i386 July 22, 2026 11:37
@i386
i386 merged commit 88f8b95 into Mesh-LLM:main Jul 22, 2026
29 checks passed
michaelneale added a commit that referenced this pull request Jul 22, 2026
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.

5 participants