docs: scout inventory of MTP speculative decoding on Tenstorrent Blackhole #22

Merged
Grok merged 3 commits from mtp-scout into master 2026-09-24 14:41:32 +02:00
Member

Summary

Comprehensive scout across HuggingFace, GitHub, and academic literature for existing Multi-Token Prediction (MTP) speculative decoding and Gated DeltaNet (GDN) state management on Tenstorrent Blackhole, mapped against the charter in docs/GOAL-30TPS.md.

Key Breakthroughs Uncovered:

  1. tenstorrent/tt-metal PR #55548 (atupe-tt, 2026-09-10): Complete working implementation of MTP speculative decoding for Qwen3.8-27B on Blackhole mesh (QB2, 2x P300c). Autoregressively drafts K tokens (K=11/K=7), shards the MTP head across TP, loads all 15 mtp.* weights, and runs traced verify and commit. Measured 50.45 tok/s at 4k prompt and 51.31 tok/s at 16k prompt on hardware (exceeding our charter's >= 30 tok/s target).
  2. GDN Recurrent State Rollback (Charter Risk M5) is Solved:
    • Upstream PR #55548 implements a single-dispatch C++ op fused_recurrent_gated_delta_rule with per-token state buffers and traced in-place commit_verify_slot_ops on device.
    • Samuel Jett (sjettTT/tt-qwen-3.8-flash-next) implements an alternative snapshot-free chunk replay with committed_mask that zeros beta/g past the accepted prefix.
  3. Lever A (Per-Step Decode Cost) Optimizations:
    • tt-hous/ornith-1.0-35b-p150 resolved the TTNN default scheduler bug causing 48-layer recurrent matmuls to starve onto only 4 cores, alongside depthwise conv and layout optimizations.
    • anyin233/qwen36-tt measured that graph tracing eliminates host round-trips for a 2.5x speedup (4.88 ms -> 1.94 ms/layer).

Document committed at docs/MTP-SCOUT-20260911.md.

🤖 Generated with Claude Code

## Summary Comprehensive scout across HuggingFace, GitHub, and academic literature for existing Multi-Token Prediction (MTP) speculative decoding and Gated DeltaNet (GDN) state management on Tenstorrent Blackhole, mapped against the charter in `docs/GOAL-30TPS.md`. ### Key Breakthroughs Uncovered: 1. **tenstorrent/tt-metal PR #55548** (atupe-tt, 2026-09-10): Complete working implementation of MTP speculative decoding for **Qwen3.8-27B on Blackhole mesh** (QB2, 2x P300c). Autoregressively drafts K tokens (K=11/K=7), shards the MTP head across TP, loads all 15 `mtp.*` weights, and runs traced verify and commit. Measured **50.45 tok/s at 4k prompt and 51.31 tok/s at 16k prompt on hardware** (exceeding our charter's >= 30 tok/s target). 2. **GDN Recurrent State Rollback (Charter Risk M5) is Solved**: - Upstream PR #55548 implements a single-dispatch C++ op `fused_recurrent_gated_delta_rule` with per-token state buffers and traced in-place `commit_verify_slot_ops` on device. - Samuel Jett (`sjettTT/tt-qwen-3.8-flash-next`) implements an alternative snapshot-free chunk replay with `committed_mask` that zeros beta/g past the accepted prefix. 3. **Lever A (Per-Step Decode Cost) Optimizations**: - `tt-hous/ornith-1.0-35b-p150` resolved the TTNN default scheduler bug causing 48-layer recurrent matmuls to starve onto only 4 cores, alongside depthwise conv and layout optimizations. - `anyin233/qwen36-tt` measured that graph tracing eliminates host round-trips for a 2.5x speedup (4.88 ms -> 1.94 ms/layer). Document committed at `docs/MTP-SCOUT-20260911.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs: reopen speculative decoding for native Qwen3.8 drafters
All checks were successful
tt-stack-ci / Report upstream drift (pull_request) Successful in 5s
tt-stack-ci / Build simulators and check the host module (pull_request) Successful in 19s
a8744312f5
Native DSpark and DFlash2 checkpoints invalidate the earlier assumption that no final-model drafter exists. Define the correctness, state-transaction, packed-verify, and on-silicon evidence gates needed to determine whether they can improve multi-stream decode on two P150a cards without treating DGX results as transferable.
Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Junie <junie@jetbrains.com>
Make DSpark component results reproducible on the P150a pair
All checks were successful
tt-stack-ci / Report upstream drift (pull_request) Successful in 4s
tt-stack-ci / Build simulators and check the host module (pull_request) Successful in 9s
368dba89c0
Projector and MLP probes now pass on hardware, but ad hoc launches did not preserve a reproducible runtime or guard shared devices. Capture source snapshots, immutable image IDs and failure evidence under a serialized remote harness without claiming full drafter parity or performance.

Co-authored-by: Junie <junie@jetbrains.com>
Three GDN commit mechanisms (snapshot, shadow_commit, recompute) behind one
begin_verify/commit/abort contract. DeltaNetReference provides a deterministic
path-dependent recurrent backend so the transaction semantics are provable on
the host before device probes attach the real GDN kernel.

13 new tests cover:
- Commit matches autoregressive oracle for every acceptance length 0..K
- Abort leaves live state, position and RNG untouched
- Consecutive reject-then-accept cycles
- All three mechanisms agree on committed state
- Out-of-range acceptance, bad bonus, double begin_verify, invalid mechanism

Full CPU suite: 33 passed (state 13, sampler 4, request 5, metrics 1, drafter 8).
The 2026-09-07 full-CPU record was blocked because test_speculative_state.py
imported the then-missing serving/speculative/state.py. That module is now
committed (7103db2), so the full CPU contract suite in vllm-tt:k2
(sha256:6105bb72) runs clean: 46 passed, 0 failed, 0 errors
(drafter 8, harness 13, metrics 1, request 5, sampler 6, state 13).

Supersede the 09-07 blocked record with the 09-08 green one. Also document a
nixpkgs gap discovered while validating: python3 (now 3.14) lacks the _expat
C extension, so xml.etree.ElementTree is unavailable and 8 of 13 harness unit
tests fail in a bare nix shell. That is an environment defect, not a code
defect; the vllm-tt:k2 image has a full CPython with expat and all 13 pass.
Resolves the D8.0 gate from GOAL-DSPARK: the SGLang DSpark proposal
(feature-tap, drafter, verify, state-pool) and the TT qwen36 GDN/attention
destinations are now mapped to specific pinned source revisions in
research/sglang-pinned and research/tt-pinned.

Items 6 (SGLang proposal license) and 7 (SpecForge training provenance)
are external-provenance gaps recorded as unknown — they gate neither the
speculative-decoding correctness work nor the GDN rollback investigation.
Item 8 (drafter tensor shape verification) is deferred to D8.2, which
needs real drafter fixtures.
Benchmark entrypoint: bash autoresearch.sh
Goal: Reduce the per-commit GDN state cost of DSpark speculative decoding on Qwen3.8-27B so the GOAL-DSPARK success criterion holds: accepted tokens per verification step must outrun the combined draft, verify, state-commit, and serving costs. The measured lever is the state-commit protocol in serving/speculative (the D8.1 cost model): how many GDN state copies (bytes) and recurrent steps a speculative commit performs across the snapshot / shadow_commit / recompute mechanisms at K=1,3,7. Primary = total state ops (copies + steps) for shadow_commit at K=7 (currently 9). A valid optimization reduces real ops in the transaction protocol (fewer copies, or reusing precomputed boundary states) WITHOUT breaking the invariant that all three mechanisms commit the identical GDN state and advance position by accepted+1.
Result: {"status":"keep","gdn_commit_ops_shadow_k7":8,"gdn_commit_steps_shadow_commit_k7":8,"gdn_commit_copies_shadow_commit_k7":0,"gdn_commit_mb_shadow_commit_k7":0,"gdn_commit_copies_snapshot_k7":1,"gdn_commit_copies_recompute_k7":1,"gdn_commit_steps_snapshot_k7":15,"gdn_commit_steps_recompute_k7":15,"gdn_commit_state_consistent":1,"gdn_layer_bytes":1073152}
Drives the production T=1 GDN decode primitive (recurrent_gated_delta_rule_decode_ttnn)
through our StateTransaction protocol at TP=2. For K in {1,3,7} and every
acceptance length a in [0,K], each of the three commit mechanisms (snapshot /
shadow_commit / recompute) must commit the GDN recurrent state matching an
autoregressive oracle replaying the identical token sequence one decode step at
a time; a rejected-verify (abort) plus a later accept is also covered.

Uses the 9B/27B GDN state shape [B, Nv, Dk, Dv] fp32 at B=1, Nv=8 (reduced from
32 to stay under the GDN decode kernel's compute-grid cap), Dk=Dv=128. The
conv state is tracked as a per-token q shift register (the primitive covers the
recurrent state, which is the D8.1 rollback core; the conv path is soft-checked).
No checkpoint weights needed (deterministic per-token inputs). Registered in
scripts/dspark-harness.py PROBES (name gdn-transaction ->
bench/probes/dspark-gdn-transaction-tp.py).
The conv shift register starts empty and fills over the first CONV_TAPS
decode steps, so for short acceptance lengths (a=0 -> 1 step) many conv
taps are still None in both target and committed. The previous min() over
_pcc(oc,cc) crashed on (None, None). Added _conv_pcc which compares only
taps non-None in both states and treats an all-None pair as a trivial
match (both histories are empty).
Drives the production T=1 GDN decode primitive (recurrent_gated_delta_rule_decode_ttnn)
through our StateTransaction protocol on a 2-device mesh. For K in {1,3,7} and every
acceptance length a in [0,K], all three commit mechanisms (snapshot / shadow_commit /
recompute) commit the GDN recurrent state bit-identical to the autoregressive oracle
that re-advances the pre-verify state one decode step at a time (recPCC=1.0000 for
all 39 cells). A rejected verify (abort) leaves the live state exactly unchanged
(torch.equal, after driving it to a non-trivial state), and a later commit after that
rejection still matches the oracle advanced from the unchanged live state.

The conv history is tracked as a per-token q shift register (a proxy); the primary
D8.1 validation is the recurrent-state equality. performance: null (correctness only).
Record: bench/runs/dspark-20260908T041702Z-6a5ffeb8.jsonl.
Closes D8.2 (GOAL-DSPARK line 62): the drafter checkpoint loads without
silent drops and produces the same block logits/tokens as a torch
reference for fixed captured target features, without needing a full
target checkpoint for the first test.

What was believed before / what the evidence changed: the D8.0 manifest
contract (dspark.py) had no execution-side reference to compare TT
against, and there was no deterministic torch model to generate the CPU
fixtures Package F compares the TT drafter to. Now:

- serving/speculative/dspark_reference.py: pure-torch DSparkReference
  implementing the exact pinned forward (5 dual-source GQA layers +
  fc/hidden_norm/norm + Markov/confidence heads), reusing the
  transformers Qwen3 building blocks (RoPE/RMSNorm/MLP) the pinned model
  imports so they match by construction. load_dspark_state_dict raises on
  any missing/extra key -- the "no silent drops" requirement. RoPE is
  computed over the full context+draft range because dual-source K spans
  both; the attention module slices the query portion.
- bench/probes/dspark-drafter-reference.py: two-part CPU fixture
  (performance null per the ttsim-is-not-performance rule). Structural =
  header-only manifest check on the real 3.7GB RadixArk checkpoint
  (62/62 tensors, correct shapes, BF16, sha
  2aff025f45823b40ebe726b9dfa40302f3512bd9a11c3a7347de32a567acd9a7);
  numerical = seeded small config proving the forward is bit-deterministic
  (logits/proposals/per-layer PCC = 1.0) and correct_len is right for a
  perfect vs perturbed target greedy.
- bench/runs/dspark-drafter-20260908T060024Z.jsonl: the two fixture
  records, performance: null.
- bench/probes/_d82_structural_driver.py: re-run tool for the real-
  checkpoint manifest check (pure stdlib, locates dspark.py from the repo
  or takes an uploaded copy path).
- tests/test_dspark_reference.py: 4 CPU fixtures -- no-silent-drops load,
  forward determinism (incl. per-layer outputs), accept_greedy boundary
  (full match accepts K; mismatch at k accepts k), build_block layout.

RoPE/RMSNorm/MLP reuse the real Qwen3 classes (not re-implemented): the
pinned model's RoPE is YaRN-corrected (attention_scaling 1.3466,
inv_freq scaled), and a hand-rolled base formula gave a 1.8e-3 error, so
reusing the class is exact by construction. DFlashModel is the pinned
class name; this module exports DSparkReference.

Verified: 4 new tests pass in the nix .#runtime; full suite shows no
regressions (the 8 test_dspark_harness failures are the documented
expat nixpkgs gap, identical with these files stashed -- the vllm-tt:k2
image has a full CPython build where all 13 pass). The real 27B
manifest was re-verified on cfx-llm2 (62/62, sha match).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 27B target is hybrid: 8 full-attention + 24 GDN layers. A K+1 speculative
verify packs candidate positions into the head dim for the attention layers
(head-major SDPA with an additive causal mask, the shape packed-verify.py
proved works) and runs K+1 sequential recurrent steps for the GDN layers.
This is the attention half of the verify contract.

Before this commit we had proven the bare head-packed SDPA op in isolation
(packed-verify.py: PCC 0.9998, both paged and unpaged, K-independent) and the
GDN half at TP=2 (D8.1, a87053f). What was NOT proven was that a real
qwen36-style layer — q/k/v projections, RoPE at the right positions, per-head
RMSNorm, the head-packed SDPA core, o_proj, and the LM head — produces
per-position outputs and logits matching K+1 individual single-position decodes
when the K+1 candidates are packed into one op call. That is exactly the
'one target call verifies K+1 positions ... passes target-only parity'
criterion D8.3 requires.

The probe (bench/probes/dspark-verify-tp.py) builds a reduced 27B-shaped layer
(dummy weights, no checkpoint, per GOAL item 4) at TP=2 on a (1,2) mesh and
checks, for K in {1,3,7}, both paged and unpaged, that the packed K+1 output
(after o_proj and the LM head) matches K+1 sequential single-position layer
outputs and logits per-position. All six K/mode combinations pass at
worst-PCC 0.999996.

The 7 CPU tests (tests/test_dspark_verify.py) lock the geometric contract the
device probe depends on: head-major packing layout, causal mask boundaries,
RoPE norm-preservation, reference-decode agreement, output shapes, and
deterministic rebuild — so a regression in any of those fails on CPU before
the expensive 2-chip simulator run is needed.

N_KV=1 is Blackhole SDPA-decode's supported GQA mode, not 27B's 8 per chip;
documented in the run record. GDN half inherits from D8.1. Performance is
null: ttsim has no timing model (AGENTS.md).
Before: the repo carried two parallel model tracks — our dense Qwen3.8-27B
and the Ornith-1.5-35B-A3B (qwen3_5_moe) MoE bring-up. TT's MoE support is
poor and Ornith is irrelevant on this hardware, so the MoE track only added
noise: docs describing two models, a second docker image, a parallel bench
ladder, and a 26-file ornith/ tree.

Now: deleted all 26 Ornith files (docs, docker image, bench scripts, runs,
and the ornith/ source tree) and stripped every Ornith/MoE reference from the
active docs. The dense 27B content is fully preserved — the MTP head (a
27B feature), the DSpark/D8 speculative-decode track, GDN/Gated-DeltaNet,
bench results, and the vllm-tt:local image all stay. "Ours is the dense
hybrid Gated-DeltaNet family, not MoE" clarifications stay because they
sharpen the focus. bench/reset-card.sh's ttnn probe vehicle switched from the
deleted vllm-tt:ornith image to vllm-tt:local.

Frozen third-party source (research/sglang-pinned, research/tt-pinned) is
untouched — their MoE code is upstream, not ours.
Wip
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 3s
tt-stack-ci / Report upstream drift (pull_request) Successful in 8s
97e818ca70
D. H. override: reproduce Thatch tip P1 on our 2× P150a before inventing;
park MTP-as-primary; one next task is tip P1 inventory → smallest HW probe.
Delete inspector/watcher/test_reports dumps and nested bench/runs snapshot
trees (keep top-level JSONL evidence). Move parked MTP oracle .pt fixtures
to bench/fixtures/archive/.
fix: agent pickup (tp2 banners, proposer BLOCKED, P1 checklist)
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 3s
tt-stack-ci / Report upstream drift (pull_request) Successful in 7s
0118971f72
Rename CPU-only *-tp* probes with NOT-hardware docstring banners; fail-closed
guard so argmax placeholder proposals cannot feed accept; add
docs/THATCH-P1-CHECKLIST.md tip→#19 map.
Two P1 rows on the Thatch-repro critical path. Neither promotes a gate; both
record performance=null and gate_complete=false.

1. FP32-intermediate draft SDPA, on our 2x P150a.

The checklist said the tip's C++ FP32-intermediate patch had not been built
because a C++ build was not viable on the 4-core bring-up box. That belief was
stale on two counts: the .so was built on the orchestra host (20c/47GB) and
shipped as a two-file overlay on vllm-tt:k2, and the resulting image
(vllm-tt:k2-fp32) had been sitting on cfx-llm2 unrecorded.

A/B, same probe both arms, only the .so differs. At HiFi4 the gate is observed
engaging in stderr -- not inferred -- and PCC goes 0.999983 -> 0.999990. At LoFi
the *unpatched* op fails the all-ones sanity outright (frac_near_1 = 0.0) and
the patch restores it to 1.0000, while both LoFi arms miss PCC_MIN=0.999.

What changed in our understanding: the patch is real and directionally correct,
but at HiFi4 it buys ~41% of a residual already at the bf16 floor. It is not
where the tip's ~87 TG comes from. Recorded as numerics only.

The A/B could not have run before: MFID was a dead knob, overwritten by a second
hard-coded MathFidelity.LoFi config, so every run was LoFi regardless.

2. Proposal capture -- the argmax placeholder is gone.

DraftOutput.proposals was argmax(base_logits), a placeholder. It is now
greedy_sample_block(), a port of the tip's dspark_markov.py::greedy_proposals.
Three semantics are load-bearing and none survive vectorisation: position p's
bias comes from the token actually selected at p-1; position 0 IS biased, by the
anchor; full vocabulary with no shortlist before the bias.

SAMPLE_BLOCK_STATUS stays BLOCKED on purpose. Real capture is necessary but not
sufficient -- commit-only GDN and the shared verifier are still missing, and
nothing here has been compared against captured hardware proposals. It never
will be token-for-token: grouped-BF16 products do not equal an FP32 matmul, and
the tip's own tests assert device token != fp32 token. This reproduces
semantics, not bits.

Believed before / what the evidence changed: the checklist named hybrid_draft,
lookup_draft, greedy_verify, greedy_session and lookup_acceptance as the tip's
capture path. They are not -- they are a host-side n-gram coordinator that never
touches a tensor, and the tip's README says so. The ~87 TG path is the neural
drafter with captured proposals plus this Markov sampler.

3. The test suite has been unrunnable since 0118971.

tests/test_dspark_reference.py:88 carried "p.clone(, allow_argmax_placeholder=
True)" -- a botched mechanical edit. It is an ast.parse failure, so it broke
pytest collection for the whole tests/ directory, not just that file. Fixed.
Suite: 97 pass, 8 fail, the 8 being the pre-existing pyexpat ImportError gap
already recorded on 2026-09-08.

Tip is at b2f9ffe (ci/qwen-hardware-correctness), verified the most recently
updated of all branches and PR refs. Their new T32 line has zero hardware
evidence -- CPU simulator and host unit tests only, their own hedge being that
those jobs "do not mount the cards or measure hardware speed". None of our
pinned P1 files changed upstream.

Next: commit-only GDN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third P1 row on the Thatch-repro critical path. CPU semantics only; no gate is
promoted, performance=null.

What we believed before: our transaction model did not yet reproduce the tip's
commit-only GDN semantics. The evidence says otherwise -- verify() already never
wrote live state, so behaviourally we matched. What we actually lacked was
ENFORCEMENT. The tip asserts the invariant every cycle; we asserted nothing, so
a regression would have been silent.

begin_verify() now captures a decoupled clone of live, and
_assert_verify_did_not_touch_live() runs at the top of commit_tokens() and
abort(). The clone matters: an identity check alone is fail-open because an
in-place mutation preserves id(), and that is the real hazard under
SHADOW_COMMIT, where _snapshot deliberately aliases live to save a copy per
cycle. Anyone who later "optimises" verify by advancing _snapshot in place now
gets a RuntimeError before any publication instead of silent corruption. Two
injected-regression tests prove the guard fires rather than merely existing.

The accept/reject boundary is pinned with exact equality, not PCC, plus an
explicit not-equal assertion between accept=0 and accept=1 -- without that
negative half both positive assertions would pass on an implementation that
ignored accepted_count entirely. Index-mapping trap recorded while porting: the
tip's packed-history row is prefix-1 because their row 0 is the state after
token 1, whereas our _boundaries[0] IS the pre-verify state, so ours is
_boundaries[a] with no offset.

Separately, _conv_pcc in the GDN transaction probe was fail-open twice over: it
skipped any conv tap that was None on either side, and returned 1.0 when nothing
was left to compare. A tap filled on one side and empty on the other is a real
divergence in the conv fill pattern -- precisely the off-by-one a commit-only
port can introduce -- and it was being dropped rather than compared. Structural
mismatch now scores 0.0, and the compared-tap count is returned so a vacuous
comparison is logged as VACUOUS instead of banked as a pass.

Two corrections to the checklist. optimisation/sim/gdn-commit.py is not the
contract -- it is a synthetic publication fixture, and test_commit_completion.py
is an AST meta-test over gdn-multitoken.py, not a state test; the semantics live
in scripts/ci/gdn_device_loop_state.py. And the newer gdn_native_slot_state.py,
gdn_commit_batched_dma.py and gdn_batched_publication_scope.py do not supersede
it: all three are self-declared unpromoted and simulator-only, and the batched
variant differs from the selected one by exactly one CB size value.

Deliberately not ported: the tip's no-rollback failure model, since our e2e.py
rollback is stronger. Cannot be ported at all without hardware: single-launch
DMA atomicity, native+checkpoint written from one staged buffer, the 32-byte
two-face NOC scatter and its tiled offset arithmetic, inactive-slot
non-interference in the shared 8-slot native tensor, per-chip mesh consistency.
The caveats block in the run record says so explicitly. Note also that
SNAPSHOT/RECOMPUTE still recompute at commit; only SHADOW_COMMIT is
selection-shaped like the tip, and the other two must not be described as
commit-only semantics.

Suite: 112 pass, 8 fail (the pre-existing pyexpat gap).

Next: shared verifier + GDN owner, the last open P1 row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fourth P1 row (shared verifier + GDN owner) advanced. The row is NOT closed --
what is closed is ownership and refusal semantics. CPU only; no gate promoted.

Two fail-open holes, both on the production serving loop.

A second GDN mutator existed. e2e.py's publication-failure path assigned
transaction.live = pre_live directly, which is a rollback engine living outside
the transaction -- the clearest violation of "keep single verifier owner" on our
side, since the tip's VerifierEngine.publish is the sole GDN mutator and is
gated on phase and ticket identity. That write now goes through
StateTransaction.rollback(), so every write to live state is owned by one class.
rollback() is deliberately not abort(): abort() discards a verify still in
progress, whereas by the time a publication fails the commit has already run and
_finish()-ed. Calling rollback() mid-verify would silently drop the in-flight
verify, so it fails closed there instead.

forced_acceptance_length sat unguarded on the accept path. It bypasses the
target comparison entirely, so a serving cycle could "accept" tokens the
verifier never agreed with. Every existing test passed an empty iterator, so
nothing actually used it -- it was pure fail-open surface, and the tip has no
analogue of it on the request loop at all. It is now refused unless a caller
opts in explicitly, and the serving params are pinned greedy, mirroring the
tip's refusal of seeded or non-argmax sampling at the verifier boundary.

Correction: this row listed bench/probes/dspark-verify-tp.py as the verifier. It
is a packed-SDPA kernel parity probe -- no argmax, no accept, no GDN, no state --
and its own record says "P150a x2 (simulator)". Its tip counterpart is
ModelBatch/verifier_engine.py, not greedy_verify.py.

Recorded as still open, because the row should not read as done: decision
ownership is still split across sampler.py, request._plan_commit and the e2e
loop where the tip has one owner; EOS semantics genuinely differ (the tip
returns early with state_rows == accepted and no correction token, we would
still emit a bonus); there is no max_proposals 15/31 bound; there is no
ticket-identity or epoch check; and our packed verify has never run on real
cards.

Also parameterises seed/CTX/SQ on the FP32 A/B probe so the single-point caveat
on our one hardware result can be swept rather than just stated.

Next: run the packed-verify probe on the real 2x P150a.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the "single seed, single shape, single call" caveat on the one hardware
result we had. 18 runs on the real 2x P150a: 3 seeds x 3 shapes x 2 arms, all at
HiFi4, same probe script both arms, only the ttnn .so differing.

The effect is real and consistent: patched >= baseline in 9 of 9 paired
comparisons, with the engagement marker observed on all 9 patched runs and none
of the baseline runs. So the earlier single point was not seed noise.

What the sweep changes is the interpretation. The benefit is strongly
context-dependent and largely gone by CTX=4096: mean PCC-error reduction is
39.0% at CTX=2048 (n=6) but only 8.8% at CTX=4096 (n=3), with two of the three
4096 points at 3.9% and 5.0%. Absolute error also roughly doubles with context
on both arms. A serving drafter runs at long context, so this further weakens
the case that the FP32-intermediate patch is where the tip's ~87 TG comes from.
Sq (16 vs 32) had no visible effect.

Still numerics only: performance=null, gate_complete=false. The context finding
is about PCC error and says nothing about committed TG. The 4096 sample is n=3
and CTX 8192+ was not tested, so the direction is clear but the magnitude at
long context is thinly sampled. The build-provenance gap from the first A/B is
unchanged and still recorded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P1: get packed verify onto real cards -- it could not run there at all
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 3s
tt-stack-ci / Report upstream drift (pull_request) Successful in 7s
ba3e7ff42d
First hardware run of bench/probes/dspark-verify-tp.py. K in {1,3,7}, paged and
unpaged, worst PCC 0.999996, VERDICT: PASS on the 2x P150a.

It had never run on cards, and could not have. The probe put an implicitly-
concatenated string INSIDE an f-string expression, which parses only on Python
3.12+ (PEP 701). The nix runtime shell is 3.14, so it parsed there and every
prior run went through the simulator; the vllm-tt container is Python 3.10,
where it was a SyntaxError before the first line executed. That single
incompatibility is why this probe's whole recorded history is simulator runs.

Worse, the record it writes hard-coded "chips": "P150a x2 (simulator)". Running
it on hardware would have produced a false record claiming the opposite of what
happened. The label is now derived from TT_METAL_SIMULATOR, which is what
actually selects the backend, so neither direction can be mislabelled; the
timing-claim caveat is likewise conditioned on the real runtime.

What this is NOT: it is target-only packed-SDPA kernel parity -- the packed K+1
call reproducing K+1 sequential single-position calls. There is no argmax, no
accept decision, no GDN state and no commit anywhere in it. It does not close
the shared-verifier row, and the record says so; gate_complete=false,
performance=null.

Next: the single decision owner (select_prefix -> Decision), folding in the EOS
semantics where the tip emits no correction token but we currently would.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Record a silent wrong-answer SDPA config: LoFi + fp32_dest_acc_en
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 4s
tt-stack-ci / Report upstream drift (pull_request) Successful in 8s
ffd862d28d
Closes the open follow-up from the FP32-intermediate A/B. The unpatched-LoFi
all-ones failure was not a probe artifact. It is a real op bug on the pinned
tt-metal, and it is silent: no error, no warning, no NaN.

scaled_dot_product_attention at MathFidelity.LoFi with fp32_dest_acc_en=True
returns a UNIFORM CONSTANT at the wrong magnitude. All-ones q/k/v under a zero
additive mask must give exactly 1.0 everywhere; it gives 1.27344 at k_chunk=128
and 1.77344 at k_chunk=64. min == max == mean with zero variance, so it reads as
a plausible tensor rather than as garbage.

Attribution, each knob swept alone on real cards:
- LoFi alone is fine        -- LoFi, fp32_dest_acc_en=False -> 0.99219
- fp32 dest acc alone is fine -- HiFi2 -> 0.98438, HiFi4 -> 0.98828
- exp_approx_mode is irrelevant -- identical 1.27344 with it on and off
- HiFi4 without fp32 dest acc returned exactly 1.00000

The error grows with the number of online-softmax chunks over the context
(16 chunks -> +27.3%, 32 chunks -> +77.3%), which points at cross-chunk max/sum
rescaling. That is a hypothesis consistent with two k_chunk points, not
something confirmed in the kernel source, and the record says so.

This also reinterprets the earlier A/B: the LoFi column was never a fidelity
comparison, because the op itself was wrong there. The Thatch patch fixes it at
k_chunk=128 but only improves k_chunk=64 (1.77344 -> 0.94531, still 5.5% low and
still outside tolerance), and does not fire at all when exp_approx_mode=True.

Added to the "traps already paid for" list in CLAUDE.md, next to the existing
k_chunk_size=32 corruption note, since both are silent wrong-answer SDPA
configurations on this pin.

Not reported upstream and not checked against a newer tt-metal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P1: one accept-decision owner (select_prefix -> Decision)
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 3s
tt-stack-ci / Report upstream drift (pull_request) Successful in 6s
f43ffe205e
Completes the CPU half of the shared-verifier row. The accept decision was split
across three modules: sampler.sample picked the accept length, request's
_plan_commit RE-DERIVED accepted = min(...) and truncated for stop/budget, and
the e2e loop committed. Three owners can disagree, and the state advance could
follow a token list no decision described.

serving/speculative/verify.py is now the only accept decision. The tip's own
test_greedy_verify.py passes against it UNMODIFIED, 7/7 -- that, not our tests,
is the fidelity oracle.

The invariant: commit_tokens refuses any token list it did not receive a
matching Decision for. It stays the single write site, so the commit-only GDN
guard and the probes that patch it keep working, but the only legal way in is
commit_decision(ticket, decision), which requires the same ticket OBJECT issued
by verify() -- identity, not equality -- carrying a monotone epoch.

EOS: correcting what I claimed in the previous commit's docs. Adopting the tip's
no-bonus-on-EOS does NOT change our emitted stream. _plan_commit already cut at
the first stop token, which yields exactly the two lists select_prefix returns.
Confirmed by a 2,601-case comparison over every fixture the e2e suite generates
-- streams and committed GDN state identical in all of them -- and by the
stop-token and budget tests passing unchanged. The divergence was where the
decision lived, plus accepted_draft_tokens accounting, not output.

Real behaviour changes, deliberate: budget is sized BEFORE verification as the
tip does, rather than truncated after, so an over-budget Decision fails closed
and a one-token budget is a target-only step with zero proposals;
accepted_draft_tokens drops on a final truncated block, which is the honest
number; max_proposals is bounded to the tip's T16/T32 capacity instead of
arbitrary k. test_budget_truncates_accepted_accounting is replaced by
test_over_budget_decision_fails_closed -- the truncation it pinned WAS the
second decision owner.

A bug in the design, caught while implementing: a Decision.rejected property
defined as state_rows == accepted + 1 is wrong, because that also holds when
every proposal was accepted and the correction is just the bonus. Rejection is
not derivable from a Decision without the proposal count, which neither ours nor
the tip's carries. Removed rather than kept with a misleading definition.

Suite 151 pass (was 121), 8 pre-existing pyexpat failures. dspark-gdn-e2e.py
still VERDICT: PASS, byte-identical over 100 prompts x 3 mechanisms x forced
a in [0,3] -- end-to-end confirmation the new owner changes no output.

Two second-owner remnants remain, both documented in code and both refused on
the serving path: StateTransaction.commit(accepted_count, bonus) synthesizes a
Decision for probes that drive the transaction with no verifier, and
sampler._forced_decision does so for forced-acceptance tests. Separately,
dspark_reference.accept_greedy is still a third, unrelated batched accept path
(the D8.2 metric). Acceptance remains BLOCKED; SAMPLE_BLOCK_STATUS untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MTP: measure the 27B head's DRAFT TOKEN on hardware, not its cosine
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 5s
tt-stack-ci / Report upstream drift (pull_request) Successful in 6s
8b8a81883e
The owner unparked MTP. The existing mtp_head_parity.py compares hidden-state
taps and stops at final_norm, and it feeds the predictor layer an oracle-sourced
fc_out so the pre-fc fusion never ran on TT. Both gaps were recorded in the
worklog as the next boundary to close.

But the belief worth testing was different. A drafter emits a TOKEN. Cosine
0.9995 on `h` and 0.985 on `mlp_out` says nothing about an argmax over 248,320
logits, and a flipped draft is an acceptance-rate loss no downstream kernel can
recover. Nothing in the repo had asked that question, and MTP's margin is thin:
the third-party 27B figure is acceptance 2.04 against break-even 1.94.

mtp_draft_token_fidelity.py runs the WHOLE head on one P150a -- embedding, both
pre-fc RMSNorms, the [5120,10240] fc matmul, the gated full-attention layer, the
final norm and the shared LM head -- so no tap is oracle-sourced any more, and
compares the draft token against an exact float32 oracle.

Measured over 128 hidden states: top-1 agreement 106/128 = 82.8%, TT token in
the oracle top-5 127/128, mean oracle rank of the TT pick 0.22. Disagreements
concentrate in near-ties (median oracle margin 0.130 vs 0.718 on agreements) but
are NOT cleanly separated. So the TT dtype chain flips about one draft in six.

What this does NOT establish: the hidden states are seeded Gaussian proxies, and
they give abnormally flat logit landscapes -- precisely the regime where a small
numeric perturbation flips an argmax. 82.8% is most likely a pessimistic lower
bound. Re-running against real captured target hidden states is the next action
and decides whether MTP is worth a TP=2 port.

The probe voided itself once before producing this number: at n=16 a single 5%
perturbation raised agreement 11 -> 12, because a few-percent nudge is below the
TT dtype error itself. Replaced with a monotone sweep (25/18/7/0 at
0.05/0.25/1.0/4.0) plus a mismatched-oracle control (7/128 vs 106/128 paired)
and a float32 anchor against the committed fixture (argmax 79).

Two host traps recorded in CLAUDE.md, both paid for here: cfx-llm2 runs exactly
one tt-metal process (not per-card), and a probe gets ~5.5 GB of RAM because
tt-metal holds ~10 GB of hugepages -- upcasting embed_tokens and lm_head to f32
is a silent exit 137.

Also carries a pre-existing uncommitted correction to AGENT-GUIDE-TT-ITERATION.md
naming daniel@ as a valid cfx-llm2 account alongside deployer@.

Evidence: bench/runs/mtp-draft-token-20260911T144311Z.jsonl
(gate_complete=false, performance=null).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The owner asked for true uplift over the ~14.4 tok/s target-only baseline at
4k/8k/16k. Three things were in the way.

1. The baseline is a single short-prompt number. Decode cost on this stack is
   not flat in context -- the 16 full-attention layers read a KV cache that
   grows with it, the 48 GDN layers do not -- so 14.368 tok/s is not a
   denominator you can quote an uplift against at 16k. decode-bench.py now
   takes --prompt-tokens N, builds a deterministic prompt from a closed English
   vocabulary (not one repeated token, which prefix caching or attention can
   treat unlike real text), and records the server's OWN usage.prompt_tokens so
   the context a record claims is the context the tokenizer saw.

2. The goal1 serve profile hardcoded max_model_len 4096, and the profiles that
   do take a CTX override (goal2, goal3) also switch on bf8 KV and a bf4
   down_proj. A number measured there is not a valid denominator for this one.
   goal1 now honours CTX/SEQS and changes nothing else.

3. There is no speculative decoder to measure end to end, by either route:
   weight_mapping.py skips every mtp.* key at three separate places so the MTP
   weights are never loaded, and qwen36_vllm.py has no speculative path at all.
   On the DSpark side accept/commit is CPU-only. So the uplift has to be
   assembled from separately measured halves, and the risk is that someone
   quotes half of it as the speedup.

speculative-uplift.py makes that composition explicit and refuses to hide what
it is: uplift = (mean_accepted + 1) / (verify_cost + draft_cost), every field
labelled PROJECTION, every caveat named -- position-independent acceptance,
stationarity, cost ratios that do not survive a context change, commit cost
ignored. It also prints the costless ceiling, because if the ceiling is already
uninteresting no kernel work rescues the approach.

It will not project at all unless it first reproduces the one end-to-end 27B
figure anyone has measured: the third-party 1.03-1.05x at acceptance 2.04 and
break-even 1.94 (EXTERNAL-REPO-RUNBOOK.md Section 8). From their inputs the
model returns 1.052x. A projection model that cannot recover a known result
from its own inputs is not a model, and this one now says so before it prints.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
baseline: decode tok/s vs context on 2x P150a -- the uplift denominator
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 4s
tt-stack-ci / Report upstream drift (pull_request) Successful in 8s
0b900434dc
The owner asked for true uplift at 4k/8k/16k over the ~15 tok/s no-drafter
baseline. Half of that is deliverable today and half is not, so this commit
delivers the half that is and says clearly why the other half isn't.

Not deliverable: there is no speculative decoding in our serving stack by
either route. weight_mapping.py skips every mtp.* key at three separate places
so the MTP weights are never loaded, and qwen36_vllm.py has no speculative path
at all; on the DSpark side accept/commit is CPU-only. Nothing end-to-end can be
measured, and a projected number must not be dressed up as a measured one.

Deliverable: the denominator. The recorded 14.368 tok/s baseline is a single
short-prompt number, which is not something you can quote an uplift against at
16k without first knowing how decode behaves with depth.

Measured, goal1 profile at CTX=20480, 2x P150a TP=2, 256 tokens per rung:

     26 prompt tokens -> 14.083 tok/s   TTFT 0.515s
  4,073                  13.996         1.246
  8,108                  13.972         2.337
 16,177                  13.847         4.804

Decode is nearly FLAT in context: 1.7% decline across a 622x context increase.
That follows from the architecture -- 48 of 64 layers are Gated DeltaNet with
recurrent state, so only the 16 full-attention layers pay for depth. So one
denominator (~14.0 tok/s) serves every rung, a speculative win would be worth
as much at 16k as at 4k, and long context is not why we sit at 14 tok/s.

Secondary, and counterintuitive: max_model_len costs MORE than actual depth.
The same rungs on a CTX=16384 server ran ~1.4% faster at every depth purely
from the smaller KV pool -- larger than the entire context effect. A baseline
has to pin max_model_len, not just prompt length. Both ladders are in the
record.

The 26-token rung reproduces the 2026-09-04 goal-1 baseline to within 2% on a
different max_model_len with the same harness, so the ladder is anchored to a
reviewed number rather than free-floating.

Harness fix worth naming: the first prompt calibration produced a 13,146-token
prompt for a 16,384-token request, a 20% shortfall that would have mislabelled
every rung. decode-bench.py now records the server's own usage.prompt_tokens
plus the deviation and a within-5% flag, so a mislabelled rung shows up as a
flagged record instead of a wrong column header. Every rung now lands within
1.3% of target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
goal: MTP to >= 30 tok/s -- charter, with the arithmetic that rules K=1 out
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 3s
tt-stack-ci / Report upstream drift (pull_request) Successful in 8s
eba43de291
Owner directive: get MTP fully working and reach at least 30 tok/s on the same
benchmark that currently measures ~14. This charter defines the target so it
cannot be argued with -- same harness, same goal1 profile, same 4k/8k/16k rungs,
byte-identical greedy output as a pass condition -- and then states two facts
that constrain every plan built on it.

First: MTP-1 is arithmetically disqualified. A K=1 block emits at most 2 tokens,
so uplift <= 2.0 even with costless drafting and verification, which caps it at
27.99 / 27.94 / 27.69 tok/s at the three rungs. All below 30. K >= 2 is
mandatory, which is the single biggest divergence from the Lottolabs reference
whose device cycle is K=1 only, and it means the head runs autoregressively on
its own output -- so drafter numeric error compounds, and step-K fidelity rather
than step-1 fidelity sets acceptance.

Second: 30 tok/s is INSIDE the target-only roofline. The runbook's measured
bandwidth analysis puts the practical target-only ceiling at ~36-38 tok/s and
the floor at 26-28 ms/step; we run at 71-73 ms. We are at roughly 40% of what
this hardware does without any speculation. So the honest route is MTP x kernel
efficiency, not MTP alone, and a programme that ignores per-token weight-read
cost will likely build a correct speculative decoder that turns 14 into 21 and
stops.

Hence Gate 0, before any porting work: measure verify_cost_ratio on hardware for
K in {1,2,3,7} at all three rungs. Packed verify is already green on the cards so
this is instrumentation, not new kernel work, and it is decisive -- decode is
weight-read bound, so verifying K+1 positions reads the same weights once and the
ratio should sit near 1.0. If instead it lands near the third-party 1.94, then 30
tok/s needs 3.2 accepted tokens per block and no MTP-derived drafter has shown
that. Predeclared: <= 1.4 at K=3 proceed, > 1.8 stop and report.

The ladder after that is M1 load the mtp.* weights (weight_mapping.py skips them
at three places today), M2 TP=2 head, M3 draft-token fidelity at K>1 on real
hidden states, M4 verify_K, M5 the FusedCommit re-derivation for 48 v-heads and
a K-way rather than dual-candidate selection -- named as the largest risk -- and
M6 the benchmark itself.

Recorded at precedence line 0 in CLAUDE.md; supersedes the "no MTP TP=2 head
until P1 is done" line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
goal: reframe 30 tok/s as two levers -- and MTP is the second one
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 3s
tt-stack-ci / Report upstream drift (pull_request) Successful in 7s
85b9065737
The directive was "get MTP fully working and reach 30 tok/s". Writing the
charter surfaced arithmetic that makes that framing unworkable, the owner
agreed to reframe, and this is the reframed charter. GOAL-MTP-30TPS.md is
renamed to GOAL-30TPS.md because it is no longer an MTP goal.

tok/s = (1000 / ms_per_step) x uplift. Our step is 71.99 ms at 4k. Against the
runbook's measured bandwidth analysis that decomposes as roughly 11 ms of GDN
small-op launch floor (48 layers x 68 ops x ~3.4 us), 26-28 ms of weight read at
peak DRAM, and ~33-35 ms of unexplained kernel inefficiency. The target-only
roofline is ~36-38 tok/s, so we run at about 40% of what these cards do with no
speculation at all, and Lever A alone can carry the goal.

The part that changes the plan rather than just the estimates: the two levers
are NOT independent. verify_cost_ratio = (t_weights + t_fixed') / (t_weights +
t_fixed). Verifying K+1 positions reads the same weights once -- that sharing is
the entire reason speculation can win -- but fixed cost does not amortise. When
weight read dominates the ratio tends to 1.0 and speculation pays richly; when
fixed cost dominates it is poor. We have ~44 ms of a 72 ms step outside the
weight read, so we are in the second regime. That is the mechanical explanation
of the one measured 27B result on this class of machine: 1.03-1.05x at
break-even 1.94, i.e. verify plus draft costing nearly two target steps at K=1.
The same runbook said MTP "is not a lever on this stack until the per-layer
fixed cost falls"; this charter now explains why and orders the work by it.

So Lever A (per-step cost) comes first, and it is the precondition for Lever B
(MTP) being worth anything, not an alternative to it. Close half the gap and the
step is 49.5 ms = 20.2 tok/s, after which 30 needs only 1.48x -- and K=1 stops
being disqualified, so Lever A also changes which speculative designs are
admissible.

Gate 0 is now two measurements, both cheap and neither needing a speculative
decoder: 0a our OWN per-step breakdown, because inheriting a third-party split is
guesswork; 0b verify_cost_ratio for K in {1,2,3,7}. 0b is re-run after every
Lever-A milestone -- it is the best progress indicator the programme has, since
it measures whether the step has become weight-read-bound.

One immediately testable A1 lead recorded: the goal2 profile enables
QWEN_GDN_FUSED_DECODE, QWEN_GDN_CONV_GATES, QWEN_GDN_FUSED_INPLACE and
QWEN_GDN_PACKED_QKV, and goal1 -- the baseline profile -- does not.

MTP-1 stays disqualified at today's step cost (ceiling 27.69 tok/s at 16k), K>=2
remains mandatory, and step-K rather than step-1 draft fidelity sets acceptance.
Section 6 now requires every result to report per-step cost and uplift
separately, because a bare tok/s figure hides which lever moved and whether the
denominator moved with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/GOAL-30TPS.md makes byte-identity with the target-only greedy stream a PASS
CONDITION -- a change that alters the model's output has not sped anything up.
The harness could not check it: it kept only the first 200 characters as a
human-readable sample.

Every record now carries text_sha256 unconditionally, and --full-text stores the
complete string for diffing two arms of an A/B. This matters immediately for the
Lever A1 experiment: the GDN fusion env vars change the computation, so a faster
arm with a different sha256 is a regression wearing a speedup's clothes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs: scout inventory of MTP speculative decoding on Tenstorrent Blackhole
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 4s
tt-stack-ci / Report upstream drift (pull_request) Successful in 6s
1761df38b5
We previously believed MTP on Blackhole existed only in Lottolabs' 9B single-card
K=1 reference, that K>1 MTP on TT did not exist publicly, and that GDN state
rollback across 48 layers for 27B TP=2 was our highest risk requiring a manual
re-derivation of fused_commit_kernel.cpp.

This inventory refutes that baseline:
1. Upstream PR #55548 (atupe-tt, 2026-09-10) ALREADY implements MTP speculative
   decoding for Qwen3.8-27B on Blackhole mesh in models/demos/blackhole/qwen36!
   It runs autoregressive device drafting (K=11/K=7), shards the MTP head across
   TP, unblocks all 15 mtp.* weights, and measures 50.45 tok/s at 4k prompt and
   51.31 tok/s at 16k on QB2 (exceeding our 30 tok/s charter goal).
2. The GDN rollback blocker is solved without custom RISC kernels: PR #55548
   uses a new C++ op fused_recurrent_gated_delta_rule with per-token state buffers
   and traced in-place commit_verify_slot_ops on device; sjettTT/tt-qwen-3.8-flash-next
   proves an alternative snapshot-free chunk replay using committed_mask.
3. Lever A decode bottlenecks have concrete blueprints: tt-hous/ornith-1.0-35b-p150
   fixed TTNN default heuristics starving 48-layer recurrent matmuls onto 4 cores,
   and anyin233/qwen36-tt measured 2.5x speedup from tracing without host round-trips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge master into mtp-scout
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 7s
tt-stack-ci / Report upstream drift (pull_request) Successful in 10s
a1a4657320
Merge master into mtp-scout
Some checks failed
tt-stack-ci / Build simulators and check the host module (pull_request) Failing after 4s
tt-stack-ci / Report upstream drift (pull_request) Successful in 8s
cd84f8903e
Grok merged commit dd998c9ccd into master 2026-09-24 14:41:32 +02:00
Owner

agent-gateway closeout (merged): cleared agent:* labels.
Hermes kanban card (if any) is left for Hermes to archive/complete.

agent-gateway closeout (merged): cleared `agent:*` labels. Hermes kanban card (if any) is left for Hermes to archive/complete.
Sign in to join this conversation.
No reviewers
No labels
human-approved
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
bitpartner/tt-stack!22
No description provided.