Skip to content

fix(SPEC-DFLASH2): key the draft context by request id, so a condensed batch row cannot take it from its owner (#2008) - #2010

Open
localai-bot wants to merge 7 commits into
mainfrom
row/FIX-DFLASH2-CONCURRENCY
Open

fix(SPEC-DFLASH2): key the draft context by request id, so a condensed batch row cannot take it from its owner (#2008)#2010
localai-bot wants to merge 7 commits into
mainfrom
row/FIX-DFLASH2-CONCURRENCY

Conversation

@localai-bot

@localai-bot localai-bot commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

DFlash2 served exactly one sequence. At concurrency 2 the engine refused on its own position invariant one request in, EngineCore stopped, and every later request on that server came back [request submitted to a stopped AsyncLLM]. This is the blocking defect for the #1574 three-engine comparison: vLLM and SGLang were both measured at c=1..32 with the same DFlash2 draft at k=8, and we could produce c=1 and nothing above it.

Closes #2008.

Why the accumulation desynchronises

The runner held the draft context in four arrays indexed by batch row. A row index is not stable for a request's lifetime here: InputBatch::condense slides a live request down into the hole a finished neighbour left (input_batch.cpp:686-706), and swap_states exchanges two live rows (:762-847). Both permute every per-slot array they own — req_ids, num_computed_tokens_cpu, num_accepted_tokens, last_sampled_tokens, prefill_len, the block-table rows, the index-keyed sampling maps — and neither knows these four exist, because they live on GPUModelRunner rather than in InputBatch.

So when the first of two concurrent requests finished, the survivor was moved onto the departed request's bookkeeping. The reused-slot test at runner.cpp:2895-2906 read a changed occupant, allocated a fresh empty store and set ctx_len = 0 — under a request still using it — and the invariant at :2939-2945 then refused rather than drafting from a foreign context. ok=1 at c=2 is that mechanism's signature, not an incidental count.

At c=1 the only row is row 0: a request finishes, the next is admitted into the same row, and the reset is correct, because the new occupant is a fresh prefill whose first position is 0. The condense move that breaks the state exists only when a second live request has to be slid over a departed one.

The operator's isolation had already settled the layer: with --speculative-config omitted and everything else identical, the same two concurrent requests both complete.

This tree already names the bug class, in a comment above the log written to solve it for a different array (input_batch.h:240-252): "Upstream needs no equivalent because it never condenses ... the slot index is stable for the request's lifetime. This log is the price of our condensed dense batch." The DFlash2 arrays never got that treatment.

Upstream

Read beyond our parity pin at b389ac2946 (the clone is shallow and checked out at the pin, so anchors were read with git show). Upstream has no analogue of the host-side counter at all. DFlash and DFlash2 address the draft KV by absolute position (dflash/speculator.py:562-590), re-read their one anchor from the target's own positions every step (:553), and index every cross-step tensor by the persistent request slot through req_state_idx = idx_mapping[req_idx] (:536, dflash2/speculator.py:95) — in a V2 runner that has no condense at all (gpu/states.py:29,100,132). Its legacy V1 runner does condense, and there the draft's block-table row moves with the request (gpu_input_batch.py:786block_table.py:367-373).

Both upstream shapes key the draft context to the request. Ours keyed it to neither.

The change

One unordered_map<string, DflashReqCtx> holding the store, the context length and the disabled flag. Every row permutation the batch can perform — today's two and any future one — is now a no-op for the draft context.

The reused-slot test goes away with it. "Has this row's occupant changed" was only ever a proxy for "is this state this request's", and the proxy is precisely what broke: after a condense move the answer was yes for a request whose context was perfectly valid. Keyed by id the question cannot be asked wrongly; a first sight of an id constructs the entry, and that construction is the reset. dflash_ctx_disabled_'s own comment — "the flag is a property of the REQUEST, not of the row" — becomes literally true. The decode-first reorder is fixed for free, and it is a second live trigger rather than a hypothetical one. reorder_batch_to_split_decodes_and_prefills runs unconditionally every step (runner.cpp:1324) and swaps live rows through swap_states. It emits no swap only while arrival order already puts decodes before prefills — the common case, since condense keeps older decoding requests at low rows and a new arrival appends at the end as a prefill — which is why the #2008 measurement met the condense move first. A batch whose regions are out of order does swap, and on the pre-change code that exchanged two live requests' draft contexts. (The commit body and an earlier draft of the spec called this path "inert for a Qwen3 target today"; that was wrong and the spec now says so — the reorder has no model-family gate at its call site.)

Entries are pruned each propose against InputBatch's membership — the authority on residency, unlike exec_state_.req_ids, which lists only the rows scheduled this step. Each entry owns a device allocation, so pruning is part of the change rather than a follow-up.

One detail a reviewer will want checked: the prune is guarded by size() > num_reqs(), and a departure paired with an arrival in the same step can leave the sizes equal with a stale entry present. That entry survives exactly one further step and cannot be read in the meantime, because lookup is by current request id. It is then reclaimed: the resolve loop inserts the newcomer, so the next prune sees size() > num_reqs() and fires. The lag is bounded at one step and is self-correcting; it does not accumulate.

Adopting upstream's paged shape — the draft context as a real KV cache group carried by MultiGroupBlockTable::move_row — is the right end state and is recorded as owed, not attempted. It replaces DflashDeviceKVStore, changes how #1919 sizes the draft budget and how #2007 splits the pools, and would land the concurrency repair behind a rewrite.

The gate

tests/vllm/v1/spec_decode/test_dflash2_concurrency.cpp drives two concurrent requests through the synchronous production front and reproduces the discontinuity on CPU, which is what converts this from something only a GPU run can see.

Its load-bearing leg is not "it did not throw". Marking the moved row disabled — the #1919 fallback, which continues before the invariant — also makes the throw stop and emits identical tokens, because the verify is lossless and a request that stops speculating just runs slower. So the gate asserts that the survivor keeps proposing, and at every step it is alive for.

Mutations

Each rival repair was built on the pre-change code and measured, not argued about.

Mutation Result
A′ — the fallback repair. On a position mismatch, disable the row instead of asserting. CAUGHT. Case 1 goes green and the propose count reads none_lines == 9 — the survivor stopped proposing for all nine remaining steps.
B′ — "silence the check." Delete the position invariant, let the reset context stand. NOT CAUGHT. Filed as #2009.
Reachability. Delete the propose_drafts_block call site in propose_drafts_dflash. Red, by name.

One leg was written, run, and removed rather than shipped. Comparing the drafted blocks against a solo control is a tautology on this fixture: with the invariant deleted and the context reset at every row move, the synthetic draft still emits 12 12 12 at every step of both runs — seeded-noise weights over a 24-token vocabulary, and the selector walk collapses to one id. Nine passing string comparisons measured nothing. The test file and the spec both record it, because the next person to reach for that leg will find the same fixture. That B′ is consequently ungated is #2009, not a thing this row quietly left out.

What this does not do, and what must be measured before it is trusted

A concurrency change does not merge here on a green compile. The end-to-end ladder is the operator's to run — rungs c = 1, 2, 4, 8, 16 at the #2008 flags. Predictions and falsifiers are in the spec under ## Gates. In short: c=1 must be inert (24.70 out tok/s, TPOT 37.90 ms), no rung may VOID or report failed > 0, and c=2 must not come in below c=1. A rung that lands low but completes is #2007 and the P == 1 capture gate being measured for the first time — this change is what lets a batch reach P > 1 at all — and those are named, owned and out of scope.

Gates run

Gate Result
cmake --build build -j 4 (CI's CPU configuration) rc 0, no error: lines
ctest --test-dir build --output-on-failure rc 0 — 628 tests, 0 failed, 5 pre-existing skips
test_dflash2_concurrency in that suite Passed, 37.73 s
test_dflash2_runner_reach (sibling DFlash2 gate) Passed, 45.78 s
scripts/agent-preflight.sh --staged rc 0, 0 gates failed
check-agent-record.py rc 0
check-commit-trailers.py / check-commit-style.py rc 0

Exit codes were captured explicitly rather than read off the tail, which is how the first preflight run's role-undeclared failure was caught behind a clean-looking output. The staged preflight's trailer check initially SKIPPED because origin/main had moved and was no longer an ancestor of HEAD — a diff-scoped gate that skips still exits zero. origin/main is merged (bringing #1999) and both checks were then rerun against the new base.

Found and not fixed

  • DFlash2's draft-context position invariant is ungated: deleting it leaves the whole suite green #2009 — the position invariant is ungated; deleting it leaves this row's own gate green. Closing it needs a fixture whose draft is sensitive to its context, which this one is not.
  • UNVERIFIED hypothesis, labelled rather than asserted: a prefix-cache hit or a resumed request may trip the same invariant at c=1. The reasoning is that such a request is admitted with num_computed_tokens > 0 and has no draft context for the cache-supplied tokens, so its first propose would read positions[rows[0]] > 0 against L == 0. It was not reproduced. A throwaway probe forced enable_prefix_caching = true and issued the same 20-token prompt twice; both completed, nothing threw, and the engine's own prefix_cache_metrics() reported queries=40 hits=0 — the cache never engaged, so the probe measured nothing about the hypothesis. The fixture's target is a GDN hybrid, the family upstream defaults prefix caching OFF for. Reported as a hypothesis, not a defect; confirming it needs a decoder-only DFlash2-capable target.
  • Attention and recurrent state are allocated from two separate pools, so c=32 at k=8 is unservable where vLLM and SGLang serve it #2007 — the two-pool allocation. Noted, not widened into.

Re-merge onto main (b758127ec)

origin/main moved to b758127ec (#1994) and GitHub reported this PR CONFLICTING. Re-merged; head is 2ea92d2fc. The sole conflict under GitHub's merge is .agents/issue-index.md — the merge=union driver resolves it locally and GitHub does not honour that driver. tests/CMakeLists.txt merges clean either way, and no source file conflicted, so no take-both resolution exists on this branch.

.agents/issue-index.md was union-appended and then checked by row-ID set difference rather than by reading the diff: base 730 + ours 2 (#2008, #2009) + main 15 = 747 expected, 747 actual, 0 lost / 0 invented / 0 duplicated, with every merged row byte-identical to a row in one of the three sources.

tests/CMakeLists.txt was rebuilt from origin/main (blob 6a616d781) with this row's edit re-applied after the test_dflash2_ctx_capacity source line, an anchor asserted to occur exactly once. The rebuild is byte-identical to the three-way result; git diff origin/main is 7 added, 0 deleted.

Nothing this row owns moved. Comparing the reviewed pre-merge delta against the post-merge delta, normalised for hunk offsets, the added and removed line sets are identical (792 added, 74 removed) — the only difference is two context lines, where main's #1982 and #1992 index rows now sit adjacent to this row's. Main's delta touches neither runner.h nor runner.cpp, and both, along with test_dflash2_concurrency.cpp, are blob-identical to the reviewed head 3d895a202, so the request-id keying of the draft context is untouched.

Re-merged again onto 2c27f0d57, which carries #2000 and #1997. Head is 53e144f37. #2000 is the first commit on main to touch runner.h and runner.cpp; #1997 touches neither. Both runner files three-way merged with no conflict because the edits are disjoint against the common base b758127ec#2000 at runner.h 357-474 and runner.cpp 334-823, this row at runner.h 836-870 and runner.cpp 2752-3148 — and the merge is verified in both directions, each side's per-file delta byte-identical to its own source diff at shifted offsets. Brace balance checked arithmetically anyway: runner.h expected 55, merged 55; runner.cpp expected 337, merged 337; both for { and }. Index re-checked against the current base: 745 + 2 ours (#2008, #2009) + 5 theirs (#1963, #1966, #1984, #1985, #2002) = 752 expected, 752 actual, 0 lost / 0 invented / 0 duplicated. tests/CMakeLists.txt rebuilt from blob d4d6cff71 at the same uniqueness-asserted anchor, 7 added / 0 deleted, with #2000's and #1997's own lines intact. The reviewed change is unchanged across all three re-merges: added/removed line sets identical to git diff 2a42cb369 3d895a202 (786/68 excluding file headers, 792/74 including them).

The full ctest suite has NO local verdict on this head, and CI carries it. The box was at 98% disk; a full build drove the root filesystem to 100% with 4.1 GiB free, which is the ENOSPC state that makes unrelated checkers emit false policy refusals, so it was aborted and the space reclaimed. The focused suites were re-run after the abort from a clean rebuild — ctest -R dflash 27/27, test_dflash2_concurrency 2/2 (21 assertions), test_dflash2_runner_reach 8/8, test_dflash2_ctx_capacity 6/6, test_runner 20/20, every one rc=0 — and scripts/agent-preflight.sh --staged is green.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE-CODE:claude-opus-5 [Claude Code]

mudler added 5 commits August 26, 2026 19:29
…ense moves a live request between rows (#2008)

DFlash2 serves exactly one sequence. At concurrency 2 the engine refuses on
its own invariant and EngineCore stops, taking every later request on that
server with it. This spec records the diagnosis and the shape of the repair
before any of it is written.

The operator's isolation settles which layer is at fault: with
`--speculative-config` omitted and everything else identical, the same two
concurrent requests both complete. So batching, scheduling, the paged KV cache,
the block tables and the sampler all serve two sequences correctly, and the
defect is confined to the draft's per-request context accumulation.

That accumulation lives in four `GPUModelRunner` arrays indexed by BATCH ROW,
and a row index is not stable for a request's lifetime here.
`InputBatch::condense` slides a live request down into the hole a finished
neighbour left, and `swap_states` exchanges two live rows. Both permute every
per-slot array they own, including the block-table rows, and neither knows the
runner's four exist. The survivor of a completed pair therefore meets the
departed request's bookkeeping, its store is reset to empty, and the invariant
correctly refuses rather than drafting from a foreign context. `ok=1` at c=2 is
that mechanism's signature rather than an incidental count.

Upstream has no analogue of the host-side counter at all. Read beyond our
parity pin at `b389ac2946`, DFlash and DFlash2 address the draft KV by absolute
position, re-read their one anchor from the target's own positions every step,
and index every cross-step tensor by the persistent request slot through
`idx_mapping` — in a V2 runner that has no `condense` at all. The legacy V1
runner does condense, and there the draft's block-table row moves with the
request. Both upstream shapes key the draft context to the REQUEST; ours keys it
to neither, which is the whole defect.

The spec also records what a repair must not do. Marking the moved row disabled
makes the throw go away and emits IDENTICAL TOKENS, because the verify is
lossless and a request that stops speculating just runs slower. A token gate
cannot see that, so the gate this row lands asserts the survivor keeps proposing
and that its drafts are the ones it would have proposed alone.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE-CODE:claude-opus-5 [Claude Code]
…d batch row cannot take it from its owner (#2008)

DFlash2 served exactly one sequence. At concurrency 2 the engine refused on its
own position invariant one request in, EngineCore stopped, and every later
request on that server came back `[request submitted to a stopped AsyncLLM]`.
The measured c=2 signature was ok=1 failed=7, and ok=1 is the mechanism rather
than an incidental count.

The runner held the draft context in four arrays indexed by batch row. A row
index is not stable for a request's lifetime here: `InputBatch::condense` slides
a live request down into the hole a finished neighbour left, and `swap_states`
exchanges two live rows. Both permute every per-slot array they own, including
the block-table rows, and neither knows these four exist, because they live on
`GPUModelRunner` rather than in `InputBatch`. So when the first of two
concurrent requests finished, the survivor was moved onto the departed request's
bookkeeping, the reused-slot test read a changed occupant, and the store was
reset to EMPTY under a request still using it. The invariant then refused, which
is what it is for.

Keying the state by request id is what upstream does on both of its paths, not a
workaround for what upstream does. Its V2 runner, where DFlash2 lives, has no
`condense` at all — a finished request's slot returns to a free list and stays
that request's — and every cross-step speculator tensor is indexed through
`req_state_idx = idx_mapping[req_idx]`; its legacy V1 runner does condense, and
there the draft's block-table row moves with the request. A request id is the key
that survives any reordering of our batch, so every permutation the batch can
perform is now a no-op for the draft context.

The reused-slot test goes away with it. "Has this row's occupant changed" was
only ever a proxy for "is this state this request's", and the proxy is precisely
what broke: after a condense move the answer was yes for a request whose context
was perfectly valid. Keyed by id the question cannot be asked wrongly, a first
sight of an id constructs the entry, and that construction is the reset. The
`swap_states` reorder is fixed for free; it is inert for a Qwen3 target today, so
it is not the reproduction, but it is the same defect.

The gate drives two concurrent requests through the synchronous production front
and reproduces the discontinuity on CPU, which is what converts this from
something only a GPU run can see. Its load-bearing leg is not "it did not throw":
marking the moved row disabled also makes the throw stop and emits IDENTICAL
tokens, because the verify is lossless and a request that stops speculating just
runs slower. Built on the pre-change code, that repair leaves the first case
green and turns the propose count to nine steps of `NO proposing rows`, which is
the leg that refuses it.

One leg was written and removed rather than shipped: comparing the drafted blocks
against a solo control is a tautology on this fixture, whose synthetic draft
emits `12 12 12` at every step regardless of context. The tail of the test file
records that, and the position invariant it leaves ungated is filed as #2009.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE-CODE:claude-opus-5 [Claude Code]
…t gated nothing (#2008, #2009)

Two corrections to the spec, both of which change what a reader would conclude.

The decode-first reorder is not "inert for a Qwen3 DFlash2 target today". It runs
unconditionally on every step and has no model-family gate at its call site. What
is true is narrower: it emits no swap while the batch's arrival order already
puts decodes before prefills, which is the common case because condense keeps
older decoding requests at low rows and a new arrival appends at the end as a
prefill. That is why the measurement met the condense move first, and not a
reason the reorder cannot desynchronise two live requests' draft contexts.

The block-comparison leg the spec claimed as its anti-masking gate is a tautology
on this fixture, and was written and run before that was found: with the position
invariant deleted and the context reset at every row move, the synthetic draft
still emits the same block at every step of both runs, because seeded-noise
weights over a 24-token vocabulary collapse the selector walk to one id. Nine
passing string comparisons measured nothing. The leg is removed, the mutation
table records what each rival repair actually did, and the invariant it leaves
ungated is filed as #2009 rather than left implicit.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE-CODE:claude-opus-5 [Claude Code]
…ed hypothesis (#2008, #2009)

The spec reported, beside the fix, "a distinct c=1 defect this found and did not
fix" — that a prefix-cache hit or a resumed request is admitted with
`num_computed_tokens > 0`, has no draft context for the cache-supplied tokens,
and trips the same position invariant. That was asserted from reasoning and had
never been reproduced.

It still has not been. A throwaway probe on the shared DFlash2 fixture forced
`EngineParams::enable_prefix_caching = true`, which `ResolveEnablePrefixCaching`
honours verbatim, and issued the same 20-token prompt twice through the
synchronous production front. Both requests completed, nothing threw, and the
engine's own `prefix_cache_metrics()` reported `queries=40 hits=0`.

The cache never engaged, so the probe measured nothing about the hypothesis
rather than refuting it: the fixture's target is a GDN hybrid, the family
upstream defaults prefix caching OFF for, and no prefix-cache hit is reachable on
it. A probe whose own precondition failed is not evidence in either direction, so
the claim is downgraded to a labelled hypothesis and the measurement that failed
to test it is recorded beside it. Confirming or killing it needs a decoder-only
DFlash2-capable target.

Also links #2009 from the spec's `## Owed`, which `check-agent-record.py`
requires of an issue filed without a fix, and repairs that issue's index row: its
Row column used an ASCII hyphen where the format wants an em dash for "no owning
row", which the same checker refuses.

Nothing about the #2008 fix, its gate or its mutation evidence changes.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE-CODE:claude-opus-5 [Claude Code]
`origin/main` moved to 2a42cb3 while this row was under gate, which made the
staged preflight's commit-trailer check SKIP rather than run: the recorded base
was no longer an ancestor of HEAD, and a diff-scoped gate that skips still exits
zero. Merging restores the ancestry so that check has something to measure.

What arrives is #1999, the recurrent-state pool bounded by the KV budget. It is
one of the four fixes the #2008 measurement ran on top of, so this branch now
carries it too. The merge is clean and touches nothing this row changes: #1999
adds `hybrid_kv_budget`, and this row edits only the DFlash2 draft context on the
runner. The append-only issue index union-merged with both of this row's rows
intact, which was verified rather than assumed.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: CLAUDE-CODE:claude-opus-5 [Claude Code]
mudler added 2 commits August 27, 2026 00:24
main advanced 45 files past this row's last merge base (2a42cb3) and
GitHub reported the PR `CONFLICTING`. Two of this row's six files
overlap that delta, and both were resolved by the AGENTS.md `## Records`
rule rather than by accepting the automatic merge.

`.agents/issue-index.md` was union-appended and then checked by row-ID
set difference rather than by reading the diff: 730 base + 2 ours
(#2008, #2009) + 15 theirs = 747 expected, 747 actual, 0 lost, 0
invented, 0 duplicated. Every merged row is byte-identical to a row in
one of the three sources, and the preamble is byte-identical to base on
all three sides. GitHub does not honour the `merge=union` driver, which
is why this file is the whole reason the PR read dirty.

`tests/CMakeLists.txt` was rebuilt rather than trusted: origin/main's
complete file (blob 6a616d7) with this row's seven lines re-applied
after the `test_dflash2_ctx_capacity` source line, an anchor asserted to
occur exactly once. The rebuild is byte-identical to the three-way
result.

Nothing in the delta touches `runner.h` or `runner.cpp`, so this row's
request-keyed draft context arrives unchanged: `git diff origin/main`
is this row's original six files, 786 insertions and 68 deletions, and
`git diff 3d895a2` reproduces origin/main's entire 45-file, 6785-
insertion, 333-deletion delta with nothing dropped.

No conflict region was reconstructed, so no take-both brace hoist is
possible here; the resolved tree was built and run anyway.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [Claude Code]
…URRENCY

Two commits landed while this row waited. #2000 is the first commit on main
ever to touch the two files this row rewrites, so the runner is now a real
interaction surface and is checked as one rather than accepted on git's word.
#1997 arrived during the resolution; the merge onto #2000 alone was never
pushed, so it was discarded and this is one merge onto the final head rather
than two stacked ones.

#1997 DOES NOT TOUCH THE RUNNER. Its 28 files are the sampling defaults, the
Gumbel kernels, config resolution and the OpenAI protocol surface; neither
`runner.h` nor `runner.cpp` is among them. The runner overlap is entirely
#2000's, and it is checked rather than assumed because a prediction about which
file would conflict was already wrong once on this branch.

BOTH RUNNER FILES THREE-WAY MERGED WITH NO CONFLICT, and the reason is that the
changes are disjoint by a wide margin rather than that git was lucky. Against
the common base b758127, #2000 edits `runner.h` at 357-474 and `runner.cpp`
at 334-823; this row edits `runner.h` at 836-870 and `runner.cpp` at 2752-3148.
The merge is verified in both directions: this row's delta for each file is
byte-identical to `git diff b758127 2ea92d2`, and main's delta for each file
is byte-identical to `git diff b758127 origin/main`, at shifted offsets only.

Brace balance is checked arithmetically for the take-both failure this branch
already recorded, even though no conflict region existed to reconstruct:
`runner.h` 53 base / 54 ours / 54 theirs, expected 55, merged 55; `runner.cpp`
335 / 337 / 335, expected 337, merged 337; both counts for `{` and for `}`.

The issue index was union-appended and checked by row-ID set difference against
the CURRENT base rather than by reusing either earlier round's numbers: 745 base
+ 2 ours (#2008, #2009) + 5 theirs (#1963, #1966 from #2000; #1984, #1985, #2002
from #1997) = 752 expected, 752 actual, 0 lost, 0 invented, 0 duplicated, every
merged row byte-identical to a row in one of the three sources and the preamble
unchanged on all three sides.

`tests/CMakeLists.txt` was rebuilt from the new origin/main (blob d4d6cff,
sha256 d4011b98eb1dedae75f2563400a408561cae89996d90d045279a0d38420a6dd5) with
this row's seven lines re-applied after the `test_dflash2_ctx_capacity` source
line, an anchor asserted to occur exactly once. The rebuild is byte-identical to
the three-way result, `git diff origin/main` is 7 added and 0 deleted, and both
#2000's `target_include_directories` line and #1997's `test_generation_config`
registration survive in it.

This row's reviewed change is unchanged across all three re-merges. Measured
against the original reviewed delta `git diff 2a42cb3 3d895a2`, the added
and removed line sets are identical: 786 added and 68 removed excluding file
headers, 792 and 74 including them. The spec and
`test_dflash2_concurrency.cpp` are blob-identical to the reviewed head.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [Claude Code]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DFlash2 speculative decoding serves only ONE sequence: at c=2 the draft context accumulation desynchronises and EngineCore stops

2 participants