raahemnabeel commited on
Commit
786c11d
·
verified ·
1 Parent(s): c83f310

tt-model push qwen3-coder-next (container)

Browse files
README.md CHANGED
@@ -6,7 +6,6 @@ tags:
6
  - p300
7
  - p300x2
8
  - tt-model-cache
9
- - tt-model-catalog
10
  - tt-model-container
11
  - vllm-plugin
12
  pipeline_tag: text-generation
@@ -78,6 +77,13 @@ Point an OpenAI-compatible client at `http://127.0.0.1:20000` (or the port `tt-m
78
  (`--enable-auto-tool-choice` is on). Without sampling parameters a request uses the checkpoint's defaults
79
  (temperature 1.0, top_p 0.95, top_k 40).
80
 
 
 
 
 
 
 
 
81
  ### Profiles
82
  `tt-model serve raahemnabeel/qwen3-coder-next --profile <name>`:
83
  - `p300x2` (default) / `p150x4` - four dies: the faster profile.
@@ -216,6 +222,21 @@ Concurrency, 1024 input / 256 output tokens: v1 -> current
216
  | 16 | 64 | 4,161 -> 4,161 | 97.7 -> 31.0 | 10.2 -> 32.3 | 140.9 -> **338.8** (+140 %) | 29.07 -> 12.06 |
217
  | 32 | 128 | 7,135 -> 7,126 | 154.6 -> 65.3 | 6.5 -> 15.3 | 175.9 -> **344.0** (+96 %) | 46.55 -> 23.89 |
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  ## Limitations
220
 
221
  - Two dies (`p300`, `p150x2`) run without the fabric layer boundary (its persistent receive buffers do not fit next to
@@ -224,6 +245,9 @@ Concurrency, 1024 input / 256 output tokens: v1 -> current
224
  32 rather than moved to host sampling.
225
  - Agentic benchmarks (SWE-Bench Verified, LiveCodeBench), which Qwen publishes for this checkpoint, were not run; the
226
  evals ran on the p300x2 profile only.
 
 
 
227
  - The p150x4 and p150x2 profiles run the same meshes as p300x2 / p300 but were not run on p150 boards.
228
 
229
  ## Related packages
@@ -240,9 +264,9 @@ The exact sources the image was built from — `code/` in this repo is byte-iden
240
 
241
  | component | built from |
242
  | --- | --- |
243
- | tt-metal | [`4233e67db986d6cea6c0e98fd7d4593fc501c98e`](https://github.com/rnabeelTT/tt-metal/commit/4233e67db986d6cea6c0e98fd7d4593fc501c98e) |
244
  | vLLM | [`v0.26.0`](https://github.com/vllm-project/vllm/releases/tag/v0.26.0) |
245
  | vllm-tt-plugin | a local checkout — commit not published |
246
- | `code/` digest | `a62de654cd6c783c` (sha256, first 16 hex digits) |
247
- | built | 2026-10-27T19:31:06+00:00 by tt-model 0.1.0 |
248
 
 
6
  - p300
7
  - p300x2
8
  - tt-model-cache
 
9
  - tt-model-container
10
  - vllm-plugin
11
  pipeline_tag: text-generation
 
77
  (`--enable-auto-tool-choice` is on). Without sampling parameters a request uses the checkpoint's defaults
78
  (temperature 1.0, top_p 0.95, top_k 40).
79
 
80
+ ### Prefix caching
81
+ On by default (every profile, together with chunked prefill). When a prompt starts with the same 2,048 or more tokens as a recent one (a document sent
82
+ again, the next turn of a chat, a long shared system prompt), the server resumes from the last 2,048-token boundary
83
+ the two share instead of prefilling from the start: vLLM reuses the attention KV blocks, and the server restores the
84
+ Gated DeltaNet state it saved at that boundary (32 snapshots on four dies, 16 on two; each snapshot 27 MiB per die on four dies, 0.84 GiB per die in all, taken from the KV pool, least recently used evicted). A prompt seen for the first time
85
+ runs exactly the prefill it ran without caching. Turn it off with `tt-model serve ... -- --no-enable-prefix-caching`.
86
+
87
  ### Profiles
88
  `tt-model serve raahemnabeel/qwen3-coder-next --profile <name>`:
89
  - `p300x2` (default) / `p150x4` - four dies: the faster profile.
 
222
  | 16 | 64 | 4,161 -> 4,161 | 97.7 -> 31.0 | 10.2 -> 32.3 | 140.9 -> **338.8** (+140 %) | 29.07 -> 12.06 |
223
  | 32 | 128 | 7,135 -> 7,126 | 154.6 -> 65.3 | 6.5 -> 15.3 | 175.9 -> **344.0** (+96 %) | 46.55 -> 23.89 |
224
 
225
+ ### Prefix caching
226
+ Measured 2026-10-28 on p300x2, one user, temperature 0: the same build served with caching off and on. TTFT of a
227
+ prompt (Python source from this repo) sent a second time, and of each turn of a chat that opens with an 8k-token code
228
+ document and resends the conversation so far (as every chat client does):
229
+
230
+ | Request | caching off | caching on |
231
+ | --- | ---: | ---: |
232
+ | 7,160-token prompt, sent again | 1.79 s | 0.37 s (4.8x) |
233
+ | 22,815-token prompt, sent again | 5.81 s | 0.36 s (16x) |
234
+ | chat turns 2-6 (7.6k-8.2k tokens) | 1.94-2.08 s | 0.52-0.68 s |
235
+
236
+ Prompts seen for the first time are unchanged: no-hit rows at 128 / 2,048 / 8,192 input tokens (1 user) and 128/128 at
237
+ 32 users, TTFT and TPOT within 1 % (1 user 128/128: TTFT 171 / 174 ms, TPOT 5.59 / 5.61 ms). Two dies (p300): a 25,113-token prompt sent again 7.70 -> 0.53 s, chat turns 2-4 0.62-0.63 s (turn 1, uncached, 2.35 s). Greedy output with caching on matched caching off on every resent prompt and on
238
+ 9 of 10 chat turns; the one that differs (a thinking-on turn) diverges at a near-tie token (the reused KV blocks were computed in a different prefill pass).
239
+
240
  ## Limitations
241
 
242
  - Two dies (`p300`, `p150x2`) run without the fabric layer boundary (its persistent receive buffers do not fit next to
 
245
  32 rather than moved to host sampling.
246
  - Agentic benchmarks (SWE-Bench Verified, LiveCodeBench), which Qwen publishes for this checkpoint, were not run; the
247
  evals ran on the p300x2 profile only.
248
+ - Prefix caching resumes only at 2,048-token boundaries: a prompt sharing fewer than 2,048 tokens with an earlier one
249
+ gets no benefit, and the shared tokens past the last boundary are prefilled again. Requests with images are not
250
+ cached.
251
  - The p150x4 and p150x2 profiles run the same meshes as p300x2 / p300 but were not run on p150 boards.
252
 
253
  ## Related packages
 
264
 
265
  | component | built from |
266
  | --- | --- |
267
+ | tt-metal | [`de8e0fb2ba6f77a63d501f4009de1ab540ae96b5`](https://github.com/rnabeelTT/tt-metal/commit/de8e0fb2ba6f77a63d501f4009de1ab540ae96b5) |
268
  | vLLM | [`v0.26.0`](https://github.com/vllm-project/vllm/releases/tag/v0.26.0) |
269
  | vllm-tt-plugin | a local checkout — commit not published |
270
+ | `code/` digest | `e17bebd3bcea0db1` (sha256, first 16 hex digits) |
271
+ | built | 2026-10-28T16:39:18+00:00 by tt-model 0.1.0 |
272
 
code/models/autoports/qwen_qwen3_coder_next/tt/generator.py CHANGED
@@ -660,7 +660,9 @@ class Qwen35MoeGenerator(ReadinessGenerator):
660
  t = ttnn.to_torch(lg, mesh_composer=ttnn.ConcatMeshToTensor(self.mesh_device, dim=0))
661
  return t.float().reshape(-1, self.args.vocab_size)[:1].reshape(1, 1, -1)
662
 
663
- def _prefill_single(self, tokens, page_table, segment=None, want_sharded=False, start=0, vision=None):
 
 
664
  """B=1 prefill of any logical length through the model's masked fixed-bucket path (pads each segment to one of
665
  the pre-warmed bucket lengths, masks the GDN state to the exact valid length): full 2048-token segments carry the
666
  GDN/KV state forward through ``chunk_start``; the tail runs at the smallest bucket >= its length. Returns host
@@ -668,9 +670,15 @@ class Qwen35MoeGenerator(ReadinessGenerator):
668
  vocab-sharded logits [1,1,32,vocab/TP] of the last segment)`` for on-device sampling (row 0)."""
669
  segment = segment or int(getattr(self.model, "PREFILL_SEGMENT", 2048))
670
  T = tokens.shape[1]
671
- # `start` (chunked prefill continuation): ``tokens`` are positions start .. start+T-1 of the request; the GDN
672
- # state is reset by the bucket only at chunk_start 0, so a continuation runs on the state restored by the caller
673
- assert start % segment == 0, f"chunk start {start} must be a multiple of the {segment}-token segment"
 
 
 
 
 
 
674
  # multimodal request: ``vision`` = (packed image rows on device [n, H/TP], image_grid_thw). The M-RoPE table is
675
  # staged once over the WHOLE prompt (the segments slice it by position) and every segment splices its own
676
  # image rows (row offset = placeholders before the segment). Chunked continuations of image prompts are not
@@ -684,9 +692,9 @@ class Qwen35MoeGenerator(ReadinessGenerator):
684
  self.model._build_request_rope(tokens, vision_tokens)
685
  vkw = {"rope_staged": True}
686
  logits, sh = None, None
687
- for cs in range(start, start + T, segment):
688
- seg = tokens[:, cs - start : min(cs - start + segment, T)]
689
- last = cs + segment >= start + T
690
  if vision_tokens is not None:
691
  vkw["vision_tokens"] = vision_tokens
692
  vkw["vis_row_offset"] = self.model._vis_row_offset_for(tokens, cs)
@@ -696,9 +704,65 @@ class Qwen35MoeGenerator(ReadinessGenerator):
696
  )
697
  else:
698
  lg = self.model.prefill_masked_bucket(seg, page_table, actual_len=seg.shape[1], chunk_start=cs, **vkw)
 
 
699
  logits = self._host_logits(lg)
700
  return (logits, sh) if want_sharded else logits
701
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  # ------------------------------------------------------------------ chunked prefill (turn 7)
703
  def _pending_key(self, pt_row):
704
  return int(
@@ -736,7 +800,7 @@ class Qwen35MoeGenerator(ReadinessGenerator):
736
  for t in taps:
737
  ttnn.deallocate(t)
738
 
739
- def _batched_groups(self, lens, starts, finals, vision, pt):
740
  """Groups (lists of request indices, same bucket, 2..B each) of the requests the batched pass can admit: final
741
  text prompts starting at 0 whose length fits a batched bucket. Stale carried state on a reused first block is
742
  dropped here (as the single path does)."""
@@ -747,11 +811,11 @@ class Qwen35MoeGenerator(ReadinessGenerator):
747
 
748
  by_bucket = {}
749
  for u in range(len(lens)):
750
- if not (finals[u] and starts[u] == 0 and (vision is None or vision[u] is None)):
751
  continue
752
  if lens[u] > MAX_BATCHED_BUCKET:
753
  continue
754
- key = self._pending_key(pt[u])
755
  if key in self._pending_prefill:
756
  self._pending_free(self._pending_prefill.pop(key))
757
  by_bucket.setdefault(self.model._mask_bucket_for(lens[u]), []).append(u)
@@ -764,7 +828,17 @@ class Qwen35MoeGenerator(ReadinessGenerator):
764
  return groups
765
 
766
  def prefill_serving(
767
- self, tokens, page_table, prompt_lens, slots, sampler=None, start_pos=None, final=None, vision=None, mtp=None
 
 
 
 
 
 
 
 
 
 
768
  ):
769
  """vLLM prefill: ``tokens`` [N, max_T] host ids (row u valid up to ``prompt_lens[u]``), ``page_table`` [N, width]
770
  (row u = request u's blocks), ``slots`` = the decode slot of each request. Runs each request through the warmed
@@ -772,6 +846,10 @@ class Qwen35MoeGenerator(ReadinessGenerator):
772
  the prefill runs on the model's B=1 scratch). ``sampler(logits_sharded, u)`` -> host token (the adapter applies
773
  the request's sampling params to the on-device sampler and samples row 0 of the [1,1,32,vocab/TP] logits while
774
  the bucket output is still live); without it returns the host logits ``[N, 1, vocab]``.
 
 
 
 
775
  Returns ``[N, 1]`` int64 tokens (sampler) or ``[N, 1, vocab]`` float logits."""
776
  N = tokens.shape[0]
777
  lens = [int(prompt_lens[u]) for u in range(N)] # chunked prefill: this chunk's END per request
@@ -785,10 +863,16 @@ class Qwen35MoeGenerator(ReadinessGenerator):
785
  A = int(getattr(self.model, "PREFILL_SEGMENT", 2048))
786
  V = int(self.args.vocab_size)
787
  outs_by_u = {}
 
 
 
 
 
 
788
  # batched short-prompt admissions first: one pass per group of 2..4 same-bucket final text prompts; their GDN
789
  # state is written from the B-row scratch, their MTP head rows are skipped (MtpServing.note_batched marks the
790
  # slot: plain decode even when it ends up alone)
791
- for g in self._batched_groups(lens, starts, finals, vision, pt):
792
  g_lens = [lens[u] for u in g]
793
  g_slots = [slots[u] for u in g]
794
  lg, sh, hid = self.batched_prefill.prefill(
@@ -813,8 +897,13 @@ class Qwen35MoeGenerator(ReadinessGenerator):
813
  # request's own processed end s0 (the plugin's s may lie inside a segment: its tokens are resent with the next
814
  # chunk), carries the GDN state in device clones keyed by the request's first block, and returns zero logits;
815
  # the final chunk runs to e, samples and writes the decode slot as a whole prefill does.
816
- key = self._pending_key(pt[u])
817
  pending = self._pending_prefill.get(key)
 
 
 
 
 
818
  if pending is not None and starts[u] == 0:
819
  # a request starting from scratch on a first block that still carries another request's state (that
820
  # request was aborted / preempted mid-prefill and vLLM reused its blocks): drop the stale entry
@@ -823,15 +912,21 @@ class Qwen35MoeGenerator(ReadinessGenerator):
823
  s0 = pending[0] if pending is not None else 0
824
  if pending is None:
825
  assert (
826
- starts[u] == 0 or starts[u] % A == 0
827
  ), f"request {u}: chunk start {starts[u]} without carried state"
828
  s0 = 0 if starts[u] == 0 else starts[u]
829
  E = lens[u] if finals[u] else (lens[u] // A) * A
 
 
 
 
830
  batched = self.max_batch_size > 1
831
  prev = self.model._bind_gdn_prefill_scratch() if batched else None
832
  try:
833
  if pending is not None:
834
  self._pending_restore(key)
 
 
835
  elif batched:
836
  self.model._reset_gdn_state_for_new_sequence()
837
  vis_u = vision[u] if vision is not None else None
@@ -842,6 +937,8 @@ class Qwen35MoeGenerator(ReadinessGenerator):
842
  want_sharded=finals[u],
843
  start=s0,
844
  vision=vis_u,
 
 
845
  )
846
  lg, sh = res if finals[u] else (res, None)
847
  if finals[u]:
 
660
  t = ttnn.to_torch(lg, mesh_composer=ttnn.ConcatMeshToTensor(self.mesh_device, dim=0))
661
  return t.float().reshape(-1, self.args.vocab_size)[:1].reshape(1, 1, -1)
662
 
663
+ def _prefill_single(
664
+ self, tokens, page_table, segment=None, want_sharded=False, start=0, vision=None, cuts=(), on_cut=None
665
+ ):
666
  """B=1 prefill of any logical length through the model's masked fixed-bucket path (pads each segment to one of
667
  the pre-warmed bucket lengths, masks the GDN state to the exact valid length): full 2048-token segments carry the
668
  GDN/KV state forward through ``chunk_start``; the tail runs at the smallest bucket >= its length. Returns host
 
670
  vocab-sharded logits [1,1,32,vocab/TP] of the last segment)`` for on-device sampling (row 0)."""
671
  segment = segment or int(getattr(self.model, "PREFILL_SEGMENT", 2048))
672
  T = tokens.shape[1]
673
+ # `start` (chunked prefill continuation / prefix-cache hit): ``tokens`` are positions start .. start+T-1 of the
674
+ # request; the GDN state is reset by the bucket only at chunk_start 0, so a continuation runs on the state
675
+ # restored by the caller. Segments run [start, start + segment), ... and also end at every position in ``cuts``
676
+ # (prefix caching: ``on_cut(pos)`` snapshots the GDN state there); every segment start stays block aligned.
677
+ block = self.block_size
678
+ assert start % block == 0, f"chunk start {start} must be a multiple of the {block}-token KV block"
679
+ cuts = sorted(int(c) for c in cuts if start < int(c) <= start + T)
680
+ assert all(c % block == 0 for c in cuts), f"cut points {cuts} must be block aligned"
681
+ bounds = sorted({*range(start, start + T, segment), *(c for c in cuts if c < start + T)}) + [start + T]
682
  # multimodal request: ``vision`` = (packed image rows on device [n, H/TP], image_grid_thw). The M-RoPE table is
683
  # staged once over the WHOLE prompt (the segments slice it by position) and every segment splices its own
684
  # image rows (row offset = placeholders before the segment). Chunked continuations of image prompts are not
 
692
  self.model._build_request_rope(tokens, vision_tokens)
693
  vkw = {"rope_staged": True}
694
  logits, sh = None, None
695
+ for cs, ce in zip(bounds[:-1], bounds[1:]):
696
+ seg = tokens[:, cs - start : ce - start]
697
+ last = ce == start + T
698
  if vision_tokens is not None:
699
  vkw["vision_tokens"] = vision_tokens
700
  vkw["vis_row_offset"] = self.model._vis_row_offset_for(tokens, cs)
 
704
  )
705
  else:
706
  lg = self.model.prefill_masked_bucket(seg, page_table, actual_len=seg.shape[1], chunk_start=cs, **vkw)
707
+ if on_cut is not None and ce in cuts:
708
+ on_cut(ce)
709
  logits = self._host_logits(lg)
710
  return (logits, sh) if want_sharded else logits
711
 
712
+ # ------------------------------------------------------------------ prefix caching (GDN state snapshots)
713
+ _STATE_BYTES = {"BFLOAT16": 2, "FLOAT32": 4, "BFLOAT8_B": 1.0625, "BFLOAT4_B": 0.5625}
714
+
715
+ def alloc_prefix_state_pool(self, n_slots):
716
+ """Device pool of ``n_slots`` GDN state snapshots for vLLM prefix caching (the plugin's ``prefix_state_cache``):
717
+ one slot = the recurrent state + conv taps of every GDN layer in the B = 1 prefill layout. The plugin's index
718
+ chooses the slots; :meth:`prefill_serving` saves into and restores from them. Allocated with the KV cache (before
719
+ any trace capture) and warmed with one save / restore. Returns the bytes per device of one slot."""
720
+ prev = self.model._bind_gdn_prefill_scratch() if self.max_batch_size > 1 else None
721
+ try:
722
+ dn_layers = [layer.attention for layer in self.model.layers if not layer.is_full_attention]
723
+ self._prefix_pool = [
724
+ (
725
+ [ttnn.clone(dn.rec_state) for dn in dn_layers],
726
+ [[ttnn.clone(c) for c in dn.conv_states] for dn in dn_layers],
727
+ )
728
+ for _ in range(int(n_slots))
729
+ ]
730
+ self._prefix_save(0)
731
+ self._prefix_restore(0)
732
+ finally:
733
+ if prev is not None:
734
+ self.model._unbind_gdn_prefill_scratch(prev)
735
+ recs, convs = self._prefix_pool[0]
736
+ tensors = recs + [t for taps in convs for t in taps]
737
+ return int(
738
+ sum(
739
+ math.prod(t.padded_shape) * self._STATE_BYTES.get(str(t.dtype).split(".")[-1].upper(), 2)
740
+ for t in tensors
741
+ )
742
+ )
743
+
744
+ def _prefix_save(self, slot):
745
+ """GDN state of the bound B = 1 prefill scratch (B = 1 build: the decode buffers) -> pool slot."""
746
+ recs, convs = self._prefix_pool[slot]
747
+ dn_layers = [layer.attention for layer in self.model.layers if not layer.is_full_attention]
748
+ for dn, rec, taps in zip(dn_layers, recs, convs):
749
+ ttnn.copy(dn.rec_state, rec)
750
+ for c, t in zip(dn.conv_states, taps):
751
+ ttnn.copy(c, t)
752
+
753
+ def _prefix_restore(self, slot):
754
+ """Pool slot -> the bound B = 1 prefill scratch, then the chunk-outer conv left context from the taps (as
755
+ :meth:`_pending_restore`)."""
756
+ from models.autoports.qwen_qwen3_coder_next.tt.mtp_rows import rebuild_conv_carry
757
+
758
+ recs, convs = self._prefix_pool[slot]
759
+ dn_layers = [layer.attention for layer in self.model.layers if not layer.is_full_attention]
760
+ for dn, rec, taps in zip(dn_layers, recs, convs):
761
+ ttnn.copy(rec, dn.rec_state)
762
+ for t, c in zip(taps, dn.conv_states):
763
+ ttnn.copy(t, c)
764
+ rebuild_conv_carry(dn)
765
+
766
  # ------------------------------------------------------------------ chunked prefill (turn 7)
767
  def _pending_key(self, pt_row):
768
  return int(
 
800
  for t in taps:
801
  ttnn.deallocate(t)
802
 
803
+ def _batched_groups(self, lens, starts, finals, vision, pt, keys=None, single=()):
804
  """Groups (lists of request indices, same bucket, 2..B each) of the requests the batched pass can admit: final
805
  text prompts starting at 0 whose length fits a batched bucket. Stale carried state on a reused first block is
806
  dropped here (as the single path does)."""
 
811
 
812
  by_bucket = {}
813
  for u in range(len(lens)):
814
+ if not (finals[u] and starts[u] == 0 and (vision is None or vision[u] is None)) or u in single:
815
  continue
816
  if lens[u] > MAX_BATCHED_BUCKET:
817
  continue
818
+ key = keys[u] if keys is not None else self._pending_key(pt[u])
819
  if key in self._pending_prefill:
820
  self._pending_free(self._pending_prefill.pop(key))
821
  by_bucket.setdefault(self.model._mask_bucket_for(lens[u]), []).append(u)
 
828
  return groups
829
 
830
  def prefill_serving(
831
+ self,
832
+ tokens,
833
+ page_table,
834
+ prompt_lens,
835
+ slots,
836
+ sampler=None,
837
+ start_pos=None,
838
+ final=None,
839
+ vision=None,
840
+ mtp=None,
841
+ prefix_state=None,
842
  ):
843
  """vLLM prefill: ``tokens`` [N, max_T] host ids (row u valid up to ``prompt_lens[u]``), ``page_table`` [N, width]
844
  (row u = request u's blocks), ``slots`` = the decode slot of each request. Runs each request through the warmed
 
846
  the prefill runs on the model's B=1 scratch). ``sampler(logits_sharded, u)`` -> host token (the adapter applies
847
  the request's sampling params to the on-device sampler and samples row 0 of the [1,1,32,vocab/TP] logits while
848
  the bucket output is still live); without it returns the host logits ``[N, 1, vocab]``.
849
+ ``prefix_state`` (vLLM prefix caching, the plugin's ``prefix_state_cache``): per request ``{"req_id", "restore":
850
+ (pool slot, pos) | None, "saves": [(pos, pool slot), ...]}`` - a hit restores the GDN state saved at ``pos`` (==
851
+ its start) and runs only the uncached suffix; every save position becomes a segment end whose GDN state is
852
+ copied into its pool slot. Every save must happen: the plugin marks the slot valid once this step returns.
853
  Returns ``[N, 1]`` int64 tokens (sampler) or ``[N, 1, vocab]`` float logits."""
854
  N = tokens.shape[0]
855
  lens = [int(prompt_lens[u]) for u in range(N)] # chunked prefill: this chunk's END per request
 
863
  A = int(getattr(self.model, "PREFILL_SEGMENT", 2048))
864
  V = int(self.args.vocab_size)
865
  outs_by_u = {}
866
+ ps = list(prefix_state) if prefix_state is not None else [None] * N
867
+ restores = [p.get("restore") if p else None for p in ps]
868
+ saves = [sorted((int(q), int(k)) for q, k in (p.get("saves") or ())) if p else [] for p in ps]
869
+ # carried chunked-prefill state is keyed by request when the plugin names it (prefix caching shares first blocks)
870
+ keys = [p["req_id"] if p and p.get("req_id") is not None else self._pending_key(pt[u]) for u, p in enumerate(ps)]
871
+ single = {u for u in range(N) if restores[u] is not None or saves[u]}
872
  # batched short-prompt admissions first: one pass per group of 2..4 same-bucket final text prompts; their GDN
873
  # state is written from the B-row scratch, their MTP head rows are skipped (MtpServing.note_batched marks the
874
  # slot: plain decode even when it ends up alone)
875
+ for g in self._batched_groups(lens, starts, finals, vision, pt, keys=keys, single=single):
876
  g_lens = [lens[u] for u in g]
877
  g_slots = [slots[u] for u in g]
878
  lg, sh, hid = self.batched_prefill.prefill(
 
897
  # request's own processed end s0 (the plugin's s may lie inside a segment: its tokens are resent with the next
898
  # chunk), carries the GDN state in device clones keyed by the request's first block, and returns zero logits;
899
  # the final chunk runs to e, samples and writes the decode slot as a whole prefill does.
900
+ key = keys[u]
901
  pending = self._pending_prefill.get(key)
902
+ restore = restores[u]
903
+ if restore is not None:
904
+ assert pending is None and int(restore[1]) == starts[u] > 0, (
905
+ f"request {u}: prefix-cache restore {restore} at start {starts[u]} (carried state {pending is not None})"
906
+ )
907
  if pending is not None and starts[u] == 0:
908
  # a request starting from scratch on a first block that still carries another request's state (that
909
  # request was aborted / preempted mid-prefill and vLLM reused its blocks): drop the stale entry
 
912
  s0 = pending[0] if pending is not None else 0
913
  if pending is None:
914
  assert (
915
+ starts[u] == 0 or starts[u] % A == 0 or restore is not None
916
  ), f"request {u}: chunk start {starts[u]} without carried state"
917
  s0 = 0 if starts[u] == 0 else starts[u]
918
  E = lens[u] if finals[u] else (lens[u] // A) * A
919
+ # an intermediate chunk runs at least to its last save
920
+ E = max(E, max((q for q, _ in saves[u]), default=0))
921
+ assert all(s0 < q <= E for q, _ in saves[u]), f"request {u}: saves {saves[u]} outside [{s0}, {E}]"
922
+ cut_slot = dict(saves[u])
923
  batched = self.max_batch_size > 1
924
  prev = self.model._bind_gdn_prefill_scratch() if batched else None
925
  try:
926
  if pending is not None:
927
  self._pending_restore(key)
928
+ elif restore is not None:
929
+ self._prefix_restore(int(restore[0]))
930
  elif batched:
931
  self.model._reset_gdn_state_for_new_sequence()
932
  vis_u = vision[u] if vision is not None else None
 
937
  want_sharded=finals[u],
938
  start=s0,
939
  vision=vis_u,
940
+ cuts=list(cut_slot),
941
+ on_cut=lambda q: self._prefix_save(cut_slot[q]),
942
  )
943
  lg, sh = res if finals[u] else (res, None)
944
  if finals[u]:
code/models/autoports/qwen_qwen3_coder_next/tt/generator_vllm.py CHANGED
@@ -145,6 +145,22 @@ if os.environ.get("QWEN35_CHUNKED_PREFILL", "0") == "1":
145
 
146
  _MTP_K = 0 # Qwen3-Coder-Next ships no mtp.* weights
147
  _VISION = False # text-only checkpoint
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  _VISION_MAX_IMAGES = 0
149
 
150
 
@@ -163,7 +179,19 @@ class Qwen3NextForCausalLM(Qwen36ForCausalLM):
163
  # is implemented (Qwen35MoeGenerator.decode_forward(read_from_device=False) -> read_decode_output -> process_...).
164
  # Overlap stays off until the page-growth refresh of the plugin revision is proven (doc/vllm_integration/README.md).
165
  model_capabilities = {
166
- "supports_prefix_caching": False,
 
 
 
 
 
 
 
 
 
 
 
 
167
  "supports_async_decode": True,
168
  # Not read by this plugin revision (5961e48): overlap = vLLM's async scheduling (on by default, "Asynchronous
169
  # scheduling is enabled" in every served log) + supports_async_decode; the plugin's steady-decode fast path then
@@ -329,6 +357,8 @@ class Qwen3NextForCausalLM(Qwen36ForCausalLM):
329
  # and templates). The masked-bucket prefill programs allocate transients while a request runs (the expert-sorted
330
  # dispatch of the 2048-token bucket asked for one 512 MiB buffer during its trace capture) -> 1.25 GiB headroom.
331
  _PER_SLOT_GIB = 36.5 / 1024
 
 
332
  _TRANSIENT_HEADROOM_GIB = 1.25
333
  _MTP_HEAD_GIB = 1.5 # QWEN35_MTP: head weights + step buffers + slack for the head K/V (also budgeted as a 13th KV layer; the first full server saw two 1017 MB head K/V buffers - the log line below reports the real size)
334
  _KV_LAYERS = 12 # attention layers holding paged K/V
@@ -356,6 +386,7 @@ class Qwen3NextForCausalLM(Qwen36ForCausalLM):
356
  c = contract.get(f"serving_tp{int(num_devices)}") or contract["datatype_sweep_stage8"]
357
  B = int(max_num_seqs or 1)
358
  nonkv = float(c["non_kv_footprint_gib"]) + (B - 1) * cls._PER_SLOT_GIB
 
359
  kv_bytes_per_token = int(c["kv_bytes_per_token_per_device"])
360
  if _MTP_K > 0:
361
  # the MTP head: one more attention layer's paged K/V over the same blocks (13 instead of 12 KV layers) plus
@@ -394,6 +425,13 @@ class Qwen3NextForCausalLM(Qwen36ForCausalLM):
394
  kv = self._gen.model.allocate_kv_caches(shape, self._gen.kv_dtype, batch_size=self._gen.max_batch_size)
395
  width = min(math.ceil(self._context / block), num_blocks) # == the plugin's max_num_blocks_per_req
396
  self._gen.bind_kv_cache(kv, num_blocks, width)
 
 
 
 
 
 
 
397
  if self._mtp is not None:
398
  self._mtp.allocate_kv(shape, self._gen.kv_dtype)
399
  hk = self._mtp.head.kv_cache[0]
@@ -472,8 +510,10 @@ class Qwen3NextForCausalLM(Qwen36ForCausalLM):
472
  N = tokens.shape[0]
473
  start_pos = kwargs.get("start_pos")
474
  starts = [int(v) for v in torch.as_tensor(start_pos).reshape(-1).tolist()] if start_pos is not None else [0] * N
475
- if any(starts) and not self.model_capabilities["supports_chunked_prefill"]:
476
- raise NotImplementedError("chunked / cached-prefix prefill is not supported (model_capabilities)")
 
 
477
  # which rows end their prompt in this chunk: the plugin's intermediate mask (added to the kwargs by the chunked
478
  # prefill shim when QWEN35_CHUNKED_PREFILL=1); without it every row is final - the plugin never chunks a prompt
479
  # when chunked prefill is off. (A former heuristic "device sampling params present <=> final" marked every
@@ -525,6 +565,8 @@ class Qwen3NextForCausalLM(Qwen36ForCausalLM):
525
  f"Prefilling {N} request(s) into slots {slots} (chunk ends {plens}, starts {starts}, final {finals})"
526
  )
527
  chunked = dict(start_pos=starts, final=finals) if (any(starts) or not all(finals)) else {}
 
 
528
  if self._mtp is not None:
529
  if chunked:
530
  raise NotImplementedError(
@@ -549,7 +591,7 @@ class Qwen3NextForCausalLM(Qwen36ForCausalLM):
549
  self._mtp.end_prefill(tok0[0], plens[0])
550
  self._live_slots |= set(slots)
551
  return out
552
- if vision is not None and chunked:
553
  raise NotImplementedError("image prompts are prefilled whole (no chunked prefill)")
554
  out = self._gen.prefill_serving(tokens, pt, plens, slots, sampler=sampler, vision=vision, **chunked)
555
  self._live_slots |= set(slots)
 
145
 
146
  _MTP_K = 0 # Qwen3-Coder-Next ships no mtp.* weights
147
  _VISION = False # text-only checkpoint
148
+
149
+ # vLLM prefix caching (plugin prefix_state.py): QWEN35_PREFIX_STATE_SLOTS GDN state snapshots on device (~27 MiB per die on p300x2 each)
150
+ # let a request resume from a cached KV prefix; 0 = off. A snapshot is saved at every prefill segment boundary and at the
151
+ # last block boundary of each prompt of at least QWEN35_PREFIX_STATE_MIN tokens. Not wired with MTP.
152
+ _PREFILL_SEGMENT = int(os.environ.get("QWEN35_PREFILL_SEGMENT", "2048")) # the model's PREFILL_SEGMENT
153
+ # dies the GDN state is sharded over (MESH_DEVICE): a snapshot per die is 4 / dies times the p300x2 size, so the default
154
+ # slot count scales with the mesh (32 on four dies, 16 on two, 8 on one: the same ~0.7 GiB pool per die)
155
+ _PREFIX_DEVICES = {"P150": 1, "N150": 1, "P100": 1, "P300": 2, "P150X2": 2}.get(
156
+ os.environ.get("MESH_DEVICE", "P300x2").strip().upper(), 4
157
+ )
158
+ _PREFIX_STATE_SLOTS = int(os.environ.get("QWEN35_PREFIX_STATE_SLOTS", str(8 * _PREFIX_DEVICES))) if _MTP_K == 0 else 0
159
+ _PREFIX_STATE_MIN = int(os.environ.get("QWEN35_PREFIX_STATE_MIN", "1024"))
160
+ # QWEN35_PREFIX_STATE_PROMPT_END=1 also snapshots each prompt's last block boundary: hits reach the end of the previous
161
+ # turn, but every cold prompt pays one more prefill pass (+57..133 ms on Ornith p300x2) and its segment split (numerics)
162
+ # changes. Off: snapshots only at segment boundaries, cold prefill unchanged and a hit is bit-identical to a cold run.
163
+ _PREFIX_STATE_PROMPT_END = os.environ.get("QWEN35_PREFIX_STATE_PROMPT_END", "0") == "1"
164
  _VISION_MAX_IMAGES = 0
165
 
166
 
 
179
  # is implemented (Qwen35MoeGenerator.decode_forward(read_from_device=False) -> read_decode_output -> process_...).
180
  # Overlap stays off until the page-growth refresh of the plugin revision is proven (doc/vllm_integration/README.md).
181
  model_capabilities = {
182
+ "supports_prefix_caching": _PREFIX_STATE_SLOTS > 0,
183
+ **(
184
+ {
185
+ "prefix_state_cache": {
186
+ "slots": _PREFIX_STATE_SLOTS,
187
+ "segment": _PREFILL_SEGMENT,
188
+ "min_tokens": _PREFIX_STATE_MIN,
189
+ "prompt_end": _PREFIX_STATE_PROMPT_END,
190
+ }
191
+ }
192
+ if _PREFIX_STATE_SLOTS > 0
193
+ else {}
194
+ ),
195
  "supports_async_decode": True,
196
  # Not read by this plugin revision (5961e48): overlap = vLLM's async scheduling (on by default, "Asynchronous
197
  # scheduling is enabled" in every served log) + supports_async_decode; the plugin's steady-decode fast path then
 
357
  # and templates). The masked-bucket prefill programs allocate transients while a request runs (the expert-sorted
358
  # dispatch of the 2048-token bucket asked for one 512 MiB buffer during its trace capture) -> 1.25 GiB headroom.
359
  _PER_SLOT_GIB = 36.5 / 1024
360
+ # one prefix-cache GDN snapshot per die, p300x2 size scaled to the mesh (allocate_kv_cache checks the real size)
361
+ _PREFIX_SLOT_GIB = 28.0 / 1024 * 4 / _PREFIX_DEVICES
362
  _TRANSIENT_HEADROOM_GIB = 1.25
363
  _MTP_HEAD_GIB = 1.5 # QWEN35_MTP: head weights + step buffers + slack for the head K/V (also budgeted as a 13th KV layer; the first full server saw two 1017 MB head K/V buffers - the log line below reports the real size)
364
  _KV_LAYERS = 12 # attention layers holding paged K/V
 
386
  c = contract.get(f"serving_tp{int(num_devices)}") or contract["datatype_sweep_stage8"]
387
  B = int(max_num_seqs or 1)
388
  nonkv = float(c["non_kv_footprint_gib"]) + (B - 1) * cls._PER_SLOT_GIB
389
+ nonkv += _PREFIX_STATE_SLOTS * cls._PREFIX_SLOT_GIB
390
  kv_bytes_per_token = int(c["kv_bytes_per_token_per_device"])
391
  if _MTP_K > 0:
392
  # the MTP head: one more attention layer's paged K/V over the same blocks (13 instead of 12 KV layers) plus
 
425
  kv = self._gen.model.allocate_kv_caches(shape, self._gen.kv_dtype, batch_size=self._gen.max_batch_size)
426
  width = min(math.ceil(self._context / block), num_blocks) # == the plugin's max_num_blocks_per_req
427
  self._gen.bind_kv_cache(kv, num_blocks, width)
428
+ if _PREFIX_STATE_SLOTS > 0:
429
+ slot_bytes = self._gen.alloc_prefix_state_pool(_PREFIX_STATE_SLOTS)
430
+ logger.info(
431
+ f"prefix cache: {_PREFIX_STATE_SLOTS} GDN state snapshots x {slot_bytes / 2**20:.1f} MiB per device "
432
+ f"(budgeted {self._PREFIX_SLOT_GIB * 1024:.1f} MiB)"
433
+ )
434
+ assert slot_bytes <= self._PREFIX_SLOT_GIB * 2**30, "prefix-cache snapshot larger than its KV-pool budget"
435
  if self._mtp is not None:
436
  self._mtp.allocate_kv(shape, self._gen.kv_dtype)
437
  hk = self._mtp.head.kv_cache[0]
 
510
  N = tokens.shape[0]
511
  start_pos = kwargs.get("start_pos")
512
  starts = [int(v) for v in torch.as_tensor(start_pos).reshape(-1).tolist()] if start_pos is not None else [0] * N
513
+ prefix_state = kwargs.get("prefix_state")
514
+ restored = [bool(p and p.get("restore")) for p in prefix_state] if prefix_state is not None else [False] * N
515
+ if any(s and not r for s, r in zip(starts, restored)) and not self.model_capabilities["supports_chunked_prefill"]:
516
+ raise NotImplementedError("chunked prefill is not supported (model_capabilities)")
517
  # which rows end their prompt in this chunk: the plugin's intermediate mask (added to the kwargs by the chunked
518
  # prefill shim when QWEN35_CHUNKED_PREFILL=1); without it every row is final - the plugin never chunks a prompt
519
  # when chunked prefill is off. (A former heuristic "device sampling params present <=> final" marked every
 
565
  f"Prefilling {N} request(s) into slots {slots} (chunk ends {plens}, starts {starts}, final {finals})"
566
  )
567
  chunked = dict(start_pos=starts, final=finals) if (any(starts) or not all(finals)) else {}
568
+ if prefix_state is not None:
569
+ chunked["prefix_state"] = prefix_state
570
  if self._mtp is not None:
571
  if chunked:
572
  raise NotImplementedError(
 
591
  self._mtp.end_prefill(tok0[0], plens[0])
592
  self._live_slots |= set(slots)
593
  return out
594
+ if vision is not None and (any(starts) or not all(finals)):
595
  raise NotImplementedError("image prompts are prefilled whole (no chunked prefill)")
596
  out = self._gen.prefill_serving(tokens, pt, plens, slots, sampler=sampler, vision=vision, **chunked)
597
  self._live_slots |= set(slots)