metrale_model_engine/model/
types.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `TransformerModel`, the state one loaded model owns, and its teardown (`release_pools`).
4//!
5//! Owner: model-engine.
6//! Invariants:
7//! - `release_pools` attempts every release even after an earlier one fails, and returns the first error.
8
9#![allow(unused_imports, dead_code)]
10
11use parking_lot::Mutex;
12use std::collections::HashMap;
13use std::sync::Arc;
14
15use anyhow::{Result, bail};
16use metrale_cache::kv_cache::PagedKvCache;
17use metrale_config::{LayerType, ModelConfig};
18use metrale_gpu_runtime::buffers::BufferArena;
19use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, GraphHandle, KernelHandle};
20
21use super::ssm_pool::SsmStatePool;
22use super::ssm_snapshot::SsmSnapshotPool;
23use crate::traits::{ChunkedPrefillPageMetadata, Model, SequenceState};
24use metrale_model_layers::layer::{
25    AttnMetadataDev, ForwardContext, GdnPrefillBuffers, LayerState, SsmLayerState, TransformerLayer,
26};
27use metrale_model_layers::layers::ops;
28use metrale_model_layers::speculative::DraftProposer;
29use metrale_model_layers::weight_map::{DenseWeight, Fp8DenseWeight, MtpWeights, QuantizedWeight};
30
31mod pinned_meta_staging;
32mod staging;
33mod teardown;
34
35pub(crate) use pinned_meta_staging::PinnedMetaStaging;
36
37#[allow(dead_code)]
38/// 2026-09-25: Rows in `mtp_catchup_ring`. Position `p` is stored in ring row
39/// `p % MTP_CATCHUP_RING_ROWS`.
40pub(super) const MTP_CATCHUP_RING_ROWS: usize = 512;
41
42/// 2026-09-25: One loaded model: its layers, weights, kernel handles, KV and
43/// SSM pools and CUDA-graph caches. `trait_impl` implements `Model` for it.
44pub struct TransformerModel {
45    pub(super) config: ModelConfig,
46    /// 2026-09-25: Which GEMM implementation each projection takes, resolved
47    /// from the environment when this model was built
48    /// (`GemmDispatch::from_env`). Every `ForwardContext` this model creates
49    /// borrows it.
50    pub(super) dispatch: metrale_model_layers::layers::ops::GemmDispatch,
51    /// 2026-09-25: Per-model memo of derived weight encodings, released by
52    /// `release_pools`.
53    pub(super) derived: metrale_model_layers::layers::ops::DerivedWeights,
54    /// 2026-09-25: The weight ledger this model was built from, installed by
55    /// `adopt_weight_store` after construction and held for teardown:
56    /// `release_pools` releases it after every other pool. `None` before
57    /// adoption and after release.
58    pub(super) weight_store: Option<metrale_model_weights::weights::WeightStore>,
59    pub(super) levers: metrale_model_layers::layers::ops::ModelLevers,
60    /// 2026-09-25: Diagnostic counters and one-shot dump latches for this model.
61    pub(super) stats: metrale_model_layers::layers::ops::ModelStats,
62    pub(super) embed_tokens: DenseWeight,
63    /// 2026-09-25: Fused n-gram input embedding, installed by
64    /// `set_ngram_embedding`. A `Mutex` because the forward path has `&self`
65    /// while `NgramEmbedding::embed` takes `&mut self`.
66    pub(super) ngram_embed:
67        Option<std::sync::Mutex<metrale_model_layers::layers::ngram_embed::NgramEmbedding>>,
68    pub(super) final_norm: DenseWeight,
69    pub(super) lm_head_weight: DenseWeight,
70    pub(super) lm_head_nvfp4: Option<QuantizedWeight>,
71    /// 2026-09-25: Transposed `[K/2, ldb]` copy of `lm_head_nvfp4` for the tile
72    /// GEMM, and its row stride.
73    ///
74    /// The stride is the vocab rounded up to a multiple of 128
75    /// (`transpose_concat_for_gemm_padded(.., 16, 128)`) so that every row
76    /// starts 16-byte aligned for the GEMM's B loads, including for an odd
77    /// vocab. It is an additional buffer: `lm_head_nvfp4` keeps its row-major
78    /// layout. `None` when there is no NVFP4 head or `METRALE_NO_LMHEAD_TGEMM=1`.
79    pub(super) lm_head_nvfp4_t: Option<(QuantizedWeight, u32)>,
80    /// 2026-09-25: Runtime FP8 E4M3 LM head with per-row scales, read by the
81    /// `dense_gemv_fp8w` kernels. `Some` only on the `--lm-head-dtype fp8`
82    /// path of `setup_lm_heads`, which returns `lm_head_nvfp4 = None`.
83    pub(super) lm_head_fp8: Option<Fp8DenseWeight>,
84    /// 2026-09-25: The output head as raw Q6_K blocks, run by the K-quant GEMV.
85    /// Installed by `set_lm_head_q6k` when the store's `lm_head.weight` is
86    /// Q6_K; `None` otherwise.
87    pub(super) lm_head_q6k: Option<super::lm_head_q6k::LmHeadQ6k>,
88    /// 2026-09-28: FP8 head multi-row/W8A8 kernels; 2026-10-02 the NVFP4 W4A16 row-tile head.
89    pub(super) lm_head_rows: super::lm_head_fp8_rows::LmHeadRows,
90    pub(super) layers: Vec<Box<dyn TransformerLayer>>,
91    /// 2026-09-25: `true` when any layer's `decode_graph_unsupported()` is true,
92    /// so decode stays eager. Computed once at construction from `layers`.
93    pub(super) decode_graph_veto: bool,
94    pub(super) buffers: BufferArena,
95    /// 2026-09-25: The LoRA adapter pool and pointer tables, installed by
96    /// `set_lora_weights`, which also installs the active adapter's per-layer
97    /// pairs into the layers. `None` = no adapter.
98    pub(super) lora: Option<metrale_model_layers::lora::LoraWeights>,
99    /// 2026-09-25: Runtime adapter rotation is armed: `METRALE_LORA_ROTATE` is
100    /// on, or `METRALE_LORA_PEER` is non-empty. Set by `set_lora_weights`; the
101    /// rotate and swap entry points refuse while it is false.
102    pub(super) lora_rotatable: bool,
103    pub(super) kv_cache: Mutex<PagedKvCache>,
104    pub(super) gpu: Box<dyn GpuBackend>,
105    /// 2026-09-25: TQ+ InnerQ calibration driver, when `TURBO_INNERQ` is a
106    /// positive integer. The scheduler reaches it through `Model::poll_innerq`.
107    #[cfg(feature = "cuda")]
108    pub(super) innerq: Option<metrale_model_layers::layers::qwen3_attention::InnerQDriver>,
109    pub(super) rms_norm_kernel: KernelHandle,
110    pub(super) dense_gemv_kernel: KernelHandle,
111    /// 2026-09-25: Always `KernelHandle(0)`: `use_fp32_logits` is always false.
112    pub(super) dense_gemv_fp32out_kernel: KernelHandle,
113    pub(super) w4a16_gemv_kernel: KernelHandle,
114    pub(super) w4a16_gemv_logits_kernel: KernelHandle,
115    /// 2026-09-25: Tile GEMM over `lm_head_nvfp4_t`. 0 when `tgemm_probe_ok`
116    /// rejects the model type.
117    pub(super) w4a16_gemm_t_kernel: KernelHandle,
118    /// 2026-09-25: BF16-MMA tile GEMM over `lm_head_nvfp4_t`
119    /// (`w4a16_gemm_t_m128_bf16_v2`). 0 unless `METRALE_LMHEAD_LOSSLESS` is set
120    /// (any value) and the target has the kernel.
121    pub(super) w4a16_gemm_t_bf16_kernel: KernelHandle,
122    pub(super) w4a16_gemm_kernel: KernelHandle,
123    pub(super) w4a16_gemv_batch2_kernel: KernelHandle,
124    /// 2026-09-25: The narrow batched-GEMV tiers; a tier the target lacks is a
125    /// 0 handle.
126    pub(super) w4a16_batchm: metrale_model_layers::layers::w4a16_gemv_tiers::W4a16BatchmTiers,
127    pub(super) w4a16_gemv_batch16_kernel: KernelHandle,
128    /// 2026-09-25: FP8 E4M3 GEMV for `lm_head_fp8`. Loaded unconditionally;
129    /// launched only on the FP8-head path.
130    pub(super) dense_gemv_fp8w_kernel: KernelHandle,
131    /// 2026-09-25: FP8-weight GEMV over two rows in one weight pass. 0 when the
132    /// target lacks the kernel.
133    pub(super) dense_gemv_fp8w_batch2_kernel: KernelHandle,
134    pub(super) dense_gemm_kernel: KernelHandle,
135    /// 2026-09-25: Batched BF16 GEMV (M rows, one weight pass). 0 = absent.
136    pub(super) dense_gemv_batchm_kernel: KernelHandle,
137    /// 2026-09-25: Tensor-core BF16 GEMM with a 16-row M tile
138    /// (`dense_gemm_m16_bf16`), the BF16 LM-head arm that
139    /// `trait_impl/lm_head_batched.rs::lm_head_m16_tc_route` selects when the
140    /// `lm_head_m16_tc` lever is on. 0 when the target lacks the kernel.
141    pub(super) lm_head_m16_tc_kernel: KernelHandle,
142    /// 2026-09-25: The `N_TILE=64` variant (`dense_gemm_m16_bf16_n64`), selected
143    /// by `METRALE_LM_HEAD_M16_TC_NTILE=64`. 0 when the target lacks it.
144    pub(super) lm_head_m16_tc_n64_kernel: KernelHandle,
145    pub(super) argmax_kernel: KernelHandle,
146    /// 2026-09-25: Batched argmax (one block per row). 0 when the kernel set lacks it.
147    pub(super) argmax_batch_kernel: KernelHandle,
148    pub(super) argmax_logits_kernel: KernelHandle,
149    pub(super) batched_embed_kernel: KernelHandle,
150    pub(super) fill_slots_kernel: KernelHandle,
151    /// 2026-09-25: The device token feed (`trait_impl/feed.rs`): the masked feed
152    /// argmax and the source resolver of the `argmax_feed` module. Both are 0
153    /// when the target lacks the module, and then the feed is not used.
154    pub(super) argmax_feed_kernel: KernelHandle,
155    pub(super) feed_resolve_kernel: KernelHandle,
156    /// 2026-09-25: Feed buffers, `[feed_rows]` u32 each: the cells a step's
157    /// argmax writes and the next step reads, the resolved input ids, and the
158    /// per-row source words. The masks are `[feed_rows * 2]` u32. All four are
159    /// NULL when either feed kernel is 0.
160    pub(super) feed_cells: DevicePtr,
161    pub(super) feed_ids: DevicePtr,
162    pub(super) feed_sources: DevicePtr,
163    pub(super) feed_masks: DevicePtr,
164    pub(super) feed_rows: usize,
165    /// 2026-09-25: Batched-graph keys of fed steps still in flight, in launch
166    /// order (`pin_fed_graph_key` / `fed_step_settled_dispatch`). The batched
167    /// decode graph LRU never evicts these keys.
168    pub(super) inflight_fed_graph_keys: Mutex<std::collections::VecDeque<Vec<u32>>>,
169    /// 2026-09-25: CUDA graphs for n=1 decode, keyed by `seq.slot_idx`. A
170    /// captured graph has the slot's SSM `h_state`/`conv_state` pointers baked
171    /// in as kernel arguments, so it is only replayed for the same slot.
172    pub(super) decode_graph:
173        Mutex<std::collections::HashMap<super::trait_impl::SlotGraphKey, GraphHandle>>,
174    /// 2026-09-25: CUDA graphs for batched decode, keyed by the per-row SSM pool
175    /// slot vector (`trait_impl/decode_graph_key.rs`). Value =
176    /// `(graph, last_use_tick)`; the `u64` beside the map is the tick counter.
177    /// At `batch_decode_graph_cap` entries the least-recently-used graph is
178    /// destroyed.
179    pub(super) batch_decode_graphs: Mutex<(HashMap<Vec<u32>, (GraphHandle, u64)>, u64)>,
180    /// 2026-09-25: Pre-allocated SSM state pool, so state addresses stay fixed
181    /// across graph replays. `Arc` so each `SequenceState` can hold the
182    /// `SlotGuard` from `SsmStatePool::claim_guarded`, which returns the slot
183    /// to the free list when the sequence is dropped.
184    pub(super) ssm_pool: Arc<SsmStatePool>,
185    /// 2026-09-25: SSM state snapshots for Marconi prefix caching.
186    pub(super) ssm_snapshots: SsmSnapshotPool,
187    /// 2026-09-25: SSM snapshot spill tier. `Some` only when `METRALE_SSM_TIER`
188    /// is set and the model has SSM layers (`build_ssm_tier_store`).
189    pub(super) ssm_tier_store: Option<Arc<dyn super::ssm_tier::SnapshotBlobStore>>,
190    /// 2026-09-25: `max_seq_len / block_size + 1`, the fixed block-table stride
191    /// in attention metadata, so a captured graph's stride stays valid.
192    pub(super) max_blocks_per_seq: u32,
193    /// 2026-09-25: A KV block allocated and zeroed at construction, never freed,
194    /// used for padding rows in batched decode and verify.
195    pub(super) dummy_kv_block: u32,
196    pub(super) profile: bool,
197    /// 2026-09-25: One-shot profile of the next prefill, armed by
198    /// `METRALE_PROFILE_FIRST` (any value). The prefill layer loop
199    /// (`prefill_b/forward_layers.rs`) swaps it to false when it consumes it.
200    pub(super) profile_first_pending: std::sync::atomic::AtomicBool,
201    /// 2026-09-25: When true, CUDA graph capture and replay are skipped. Set at
202    /// construction during FP8 KV calibration or under a graph-disabling
203    /// diagnostic env, and around the per-sequence decode loop in `decode_a2`.
204    pub(super) suppress_graphs: std::sync::atomic::AtomicBool,
205    /// 2026-09-25: Draft proposer: built in `new` from the MTP weights, or
206    /// installed later by `set_dflash_proposer`.
207    pub(super) proposer: Option<Arc<dyn DraftProposer>>,
208    /// 2026-09-25: One FP32 hidden row (`hidden_size * 4` bytes): the target
209    /// hidden that `save_hidden_for_mtp` copies out before the MTP head runs.
210    pub(super) mtp_hidden_save: DevicePtr,
211    /// 2026-09-25: Batched-verify hidden stash, `[VERIFY_WY_TABLE_SEQS,
212    /// hidden_size]` BF16, one row per sequence of a batched verify.
213    /// `stash_verify_hidden_rows` copies each sequence's accepted-row hidden
214    /// here and `save_hidden_for_mtp_from_stash` reads it back for the drafter.
215    /// NULL without a proposer at construction.
216    pub(super) verify_hidden_stash: DevicePtr,
217    /// 2026-09-25: `ModelLevers::mtp_kv_exact`: verify-forward rows of accepted
218    /// drafts, `[VERIFY_WY_TABLE_SEQS * MTP_CATCHUP_MAX, hidden_size]` BF16.
219    /// NULL unless the lever is on and a proposer exists at construction.
220    pub(super) verify_catchup_stash: DevicePtr,
221    /// 2026-09-25: `METRALE_MTP_CATCHUP`: ring of per-position final hiddens
222    /// captured during serial decode, BF16 rows, row = position %
223    /// `MTP_CATCHUP_RING_ROWS`. Feeds the drafter catch-up on the next propose.
224    /// NULL when `mtp_catchup_enabled()` is false at construction.
225    pub(super) mtp_catchup_ring: DevicePtr,
226    /// 2026-09-25: `(first_position, count)` of the contiguous position range in
227    /// the ring. A write that does not extend the range restarts it at that
228    /// position; `count` never exceeds `MTP_CATCHUP_RING_ROWS`.
229    pub(super) mtp_catchup_meta: parking_lot::Mutex<(usize, usize)>,
230    /// 2026-09-25: Whole-prompt final-layer hidden capture, `[rows,
231    /// hidden_size]` BF16 with `rows = mtp_prefill_capacity`. Written per
232    /// prefill chunk and read by the drafter prefill and the drafter carry
233    /// (`drafter_prefill.rs`). NULL unless an MTP proposer exists, its head supports the
234    /// drafter prefill, and the drafter-prefill lever is on.
235    pub(super) mtp_prefill_hidden: DevicePtr,
236    /// 2026-09-25: Row capacity of `mtp_prefill_hidden`: `max_seq_len` capped by
237    /// the proposer's `prefill_hidden_rows` and, with DFlash, by the DFlash
238    /// context cap. 0 when the buffer is NULL. The capture bounds check
239    /// compares against this value.
240    pub(super) mtp_prefill_capacity: usize,
241    /// 2026-09-25: Rows of `mtp_prefill_hidden` captured contiguously from
242    /// position 0 for the sequence that owns the capture. Reset to 0 by
243    /// `alloc_sequence`. A chunk that does not extend the contiguous range
244    /// leaves it short, and the propose-site coverage check then skips the
245    /// drafter prefill.
246    pub(super) mtp_prefill_capture_len: std::sync::atomic::AtomicUsize,
247    /// 2026-09-25: Generation of the single-slot capture above. A chunk-0
248    /// prefill bumps it and stamps the new value on its sequence
249    /// (`SequenceState::mtp_capture_gen`). Appends and the drafter-prefill
250    /// consume require the sequence's stamp to equal the current value and to
251    /// be non-zero, so a sequence whose capture another sequence's prefill has
252    /// restarted skips the drafter prefill.
253    pub(super) mtp_prefill_capture_gen: std::sync::atomic::AtomicU64,
254    /// 2026-09-25: Ticket counter for `mtp_store_range` ownership
255    /// (`SequenceState::mtp_store_gen`), drawn once per `alloc_sequence`.
256    ///
257    /// It is separate from `mtp_prefill_capture_gen` because a draw on every
258    /// admission would advance that counter, and `owns_capture` requires the
259    /// capture generation to be unchanged between a sequence's capture and its
260    /// propose.
261    pub(super) mtp_store_gen_seq: std::sync::atomic::AtomicU64,
262    /// 2026-09-25: The previous turn's drafter KV, held so the next turn of the
263    /// same session can adopt it. Single slot: `mtp_carry::carry_armed_with`
264    /// turns the carry off in multi-sequence MTP mode. `None` when the carry is
265    /// off or nothing has been carried.
266    pub(super) mtp_carry:
267        parking_lot::Mutex<Option<metrale_model_layers::mtp_carry::CarriedDrafter>>,
268    /// 2026-09-25: Absolute position interval of `mtp_prefill_hidden` rows and
269    /// the sequence ticket that wrote them. Updated only while the drafter
270    /// carry is armed.
271    ///
272    /// The ticket (`owner`) is what ties the interval to one sequence:
273    /// `mtp_carry::stamped_merge` replaces rather than extends the interval
274    /// when a different owner writes. `alloc_sequence` also resets it.
275    pub(super) mtp_store_range: parking_lot::Mutex<metrale_model_layers::mtp_carry::StoreRange>,
276    /// 2026-09-25: DFlash per-layer hidden capture, allocated when the config
277    /// names DFlash capture layers. Row-major: `dflash_hidden_save_rows` rows,
278    /// each `dflash_capture_layers.len() * hidden_size` BF16, with capture
279    /// layer `s` at element offset `s * hidden_size` inside a row. `None` for
280    /// non-DFlash runs.
281    pub(super) dflash_hidden_save: Option<DevicePtr>,
282    /// 2026-09-25: Lazily allocated 16 KiB metadata staging for the
283    /// sliding-attention per-token verify loop (`verify_attention_per_token`).
284    /// The loop cannot stage at `scratch + 32768`: the verify bodies'
285    /// multi-seq metadata overlay lives there, and the interleaved
286    /// FullAttention layers still read it between the per-token calls. Layout:
287    /// position at +0, slot at +8, seq_len at +16, seq_slot at +128, block
288    /// table from +256; the loop refuses a block table that would not fit.
289    pub(super) verify_ptok_meta: std::sync::OnceLock<DevicePtr>,
290    /// 2026-09-25: Layer indices to capture for DFlash, from
291    /// `config.dflash_capture_layers`. Empty when DFlash is off.
292    pub(super) dflash_capture_layers: Vec<usize>,
293    /// 2026-09-25: Row capacity of `dflash_hidden_save`,
294    /// `dflash_kgamma.max(2) * max_batch_size.max(1)`, or 0 without DFlash.
295    /// `try_dflash_capture_all_at` clamps its writes to it.
296    pub(super) dflash_hidden_save_rows: usize,
297    /// 2026-09-25: Rows per per-sequence capture band in `dflash_hidden_save`:
298    /// the drafter γ + 1, or 17 when γ is unknown, or 0 without DFlash.
299    /// Sequence `i` of a batched verify writes rows from `i * dflash_kgamma`;
300    /// single-sequence paths use band 0. The scheduler passes this stride to
301    /// `commit_ctx` as `scratch_row`.
302    pub(super) dflash_kgamma: usize,
303    /// 2026-09-25: CUDA graphs for K=2 verify, keyed by `seq.slot_idx` for the
304    /// same reason as `decode_graph`.
305    pub(super) verify2_graph:
306        Mutex<std::collections::HashMap<super::trait_impl::SlotGraphKey, GraphHandle>>,
307    /// 2026-09-25: CUDA graphs for K=3 verify, keyed by `seq.slot_idx`.
308    pub(super) verify3_graph:
309        Mutex<std::collections::HashMap<super::trait_impl::SlotGraphKey, GraphHandle>>,
310    /// 2026-09-25: CUDA graphs for K=4 verify, keyed by `seq.slot_idx`.
311    pub(super) verify4_graph:
312        Mutex<std::collections::HashMap<super::trait_impl::SlotGraphKey, GraphHandle>>,
313    /// 2026-09-25: CUDA graphs for the batched verify (`verify_e.rs`), keyed by
314    /// `verify_key::verify_graph_key`: each sequence's `(ssm slot, row count)`
315    /// in dispatch order, then a sentinel word for the WY-tables-present and
316    /// write-on-accept bits. A graph bakes every sequence's state pointers and
317    /// the row counts, so it replays only for an equal key. Value =
318    /// `(graph, last_use_tick)`; at `VERIFY_BATCHED_GRAPH_CAP` entries the
319    /// least-recently-used graph is destroyed.
320    pub(super) verify_batched_graphs:
321        Mutex<(std::collections::HashMap<Vec<u32>, (GraphHandle, u64)>, u64)>,
322    /// 2026-09-25: Batched-verify WY pointer tables at a fixed device address:
323    /// one `VERIFY_WY_LAYER_STRIDE_BYTES` slice per GDN layer, holding
324    /// `VERIFY_WY_TABLES_PER_LAYER` tables of `VERIFY_WY_TABLE_SEQS` u64
325    /// entries. Zeroed at allocation and written by `upload_verify_wy_tables`.
326    /// NULL without a proposer at construction or without SSM layers.
327    pub(super) verify_wy_tables: DevicePtr,
328    /// 2026-09-25: Write-on-accept: device `u32[VERIFY_WY_TABLE_SEQS]` of
329    /// accepted row counts for the post-verdict fold, and the SSM slots the
330    /// most recent fold committed (`async_chkpt.rs` consumes an entry to skip
331    /// that slot's h restore). The table is NULL when `verify_wy_tables` is.
332    pub(super) gdn_woa_na_tab: DevicePtr,
333    pub(super) gdn_woa_folded_slots: Mutex<Vec<usize>>,
334    /// 2026-09-25: Cleared at the start of every batched verify, set at the end
335    /// of one that ran under a write-on-accept request with its tables staged,
336    /// and consumed by the fold (`gdn_fold_accepted_dispatch`), which declines
337    /// unless it was set.
338    pub(super) gdn_woa_eligible: std::sync::atomic::AtomicBool,
339    /// 2026-09-25: Write-on-accept `(flags, stash, seqs)` for every GDN layer,
340    /// allocated and bound by `gdn_woa_bind` on the first write-on-accept
341    /// request, before any capture, and never moved. `(NULL, NULL, 0)` until
342    /// then.
343    pub(super) gdn_woa_bound: Mutex<(DevicePtr, DevicePtr, usize)>,
344    /// 2026-09-26: Carried-state GDN verify buffers and pending rows (`gdn_carry.rs`).
345    pub(super) gdn_carry: super::gdn_carry::GdnCarry,
346    /// 2026-09-25: Key of the bytes currently staged in `verify_wy_tables`
347    /// (`verify_wy_cache_key`), or `None` when nothing has been staged. A step
348    /// whose key matches skips the host build and the H2D. Presence of
349    /// `METRALE_NO_VERIFY_WY_CACHE` disables the cache.
350    pub(super) verify_wy_cache: Mutex<Option<Vec<u64>>>,
351    /// 2026-09-25: CUDA graphs for the DFlash K=γ verify, keyed by
352    /// `(seq.slot_idx, tokens.len())`.
353    pub(super) verify_kgamma_graph: Mutex<std::collections::HashMap<(usize, usize), GraphHandle>>,
354    /// 2026-09-25: CUDA graphs for the DFlash decode+verify fused pass
355    /// (`verify_fused.rs`), keyed by `(seq.slot_idx, M)` with
356    /// `M = tokens.len()`.
357    pub(super) fused_graph: Mutex<std::collections::HashMap<(usize, usize), GraphHandle>>,
358    pub(super) prefix_cache: Box<dyn metrale_telemetry::prefix_cache::PrefixCache>,
359    /// 2026-09-25: Second CUDA stream and its event, used by the asynchronous
360    /// SSM checkpoint copies (`async_chkpt.rs`).
361    pub(super) secondary_stream: u64,
362    pub(super) secondary_event: u64,
363    /// 2026-09-25: Orders SSM-snapshot saves before a later warm Marconi
364    /// restore that runs on another stream. A save records it after enqueueing
365    /// its D2D copies (`record_snapshot_save_dispatch`), and a restore waits on
366    /// it before reading the snapshot region (`wait_snapshot_saves_dispatch`),
367    /// so a restore never reads a slot whose save copy is still in flight.
368    pub(super) snapshot_event: u64,
369    /// 2026-09-25: Communication backend for multi-rank (EP/TP) collectives.
370    /// `None` on a single GPU.
371    pub(super) comm: Option<std::sync::Arc<dyn metrale_comm::CommBackend>>,
372    /// 2026-09-25: 4-byte device buffer for the EP token broadcast.
373    pub(super) ep_cmd_buf: DevicePtr,
374    /// 2026-09-25: EP wire protocol: true when `METRALE_EP_PROTOCOL=v2`, which
375    /// precedes every command broadcast with a `seq_id` broadcast so the
376    /// worker can route slot-bound work to the right `SequenceState`. With a
377    /// comm backend, `new` checks that the ranks agree on it
378    /// (`rank_agree::assert_ranks_agree`).
379    pub(super) ep_protocol_v2: bool,
380    pub(super) self_speculative: bool,
381    /// 2026-09-25: Last token index `save_hidden_for_mtp` copied from, broadcast
382    /// to the other EP rank before an MTP propose.
383    pub(super) last_mtp_hidden_idx: std::sync::atomic::AtomicUsize,
384    pub(super) vision_encoder: Option<metrale_model_layers::layers::VisionTower>,
385    /// 2026-09-25: Number of patches encoded by the last `prepare_vision_embed`
386    /// call. 0 means no vision embeddings are pending.
387    pub(super) vision_embed_patches: Mutex<usize>,
388    /// 2026-09-25: Per-item `(t_len, grid_h_post_merge, grid_w_post_merge)` from
389    /// the most recent vision prepare, used by MRoPE prefill to assign
390    /// `(t, h, w)` position ids to each vision pad token. Empty when no vision
391    /// input is pending. `t_len` is the number of temporal groups the item
392    /// spans (1 for a still image), because a video occupies one contiguous
393    /// pad run whose T advances.
394    pub(super) vision_image_grids: Mutex<Vec<(usize, usize, usize)>>,
395    /// 2026-09-25: Where the next prefill's vision slice starts inside a batched
396    /// vision prepare: the first row of the packed output, the first
397    /// `vision_image_grids` index, and how many items it owns. Set by
398    /// `Model::set_vision_slice_base`; all 0 reads from row 0 and grid 0.
399    pub(super) vision_row_base: Mutex<usize>,
400    pub(super) vision_grid_base: Mutex<usize>,
401    pub(super) vision_owned_images: Mutex<usize>,
402    /// 2026-09-25: Page-locked host staging for batched metadata H2D transfers,
403    /// allocated in `new` and freed on drop (`drop_pinned_staging`).
404    ///
405    /// An `UnsafeCell`, not a `Mutex`, because the model is used only from the
406    /// scheduler thread after construction (see the `Sync` impl below).
407    pub(super) pinned_staging: std::cell::UnsafeCell<PinnedMetaStaging>,
408    /// 2026-09-25: Save an SSM snapshot every N blocks during chunked prefill;
409    /// 0 disables the intermediate checkpoints (`prefill_b/save_checkpoint.rs`).
410    pub(super) ssm_checkpoint_interval: usize,
411    /// 2026-09-25: `ssm_state_clamp_norm_fused`, 0 when the target lacks it.
412    pub(super) ssm_state_norm_kernel: KernelHandle,
413    /// 2026-09-25: The FP16 h-state variant (`ssm_state_clamp_norm_fused_f16`),
414    /// for sequences whose `SsmLayerState::h_is_f16` is set.
415    pub(super) ssm_state_norm_f16_kernel: KernelHandle,
416    /// 2026-09-25: Device pointer table `[num_ssm_layers]` for
417    /// `ssm_state_clamp_norm_fused`.
418    pub(super) ssm_norm_ptrs_buf: DevicePtr,
419    /// 2026-09-25: FP32 → FP16 h-state converter (`METRALE_SSM_H_FP16`).
420    pub(super) ssm_h_f32_to_f16_kernel: KernelHandle,
421    /// 2026-09-25: FP16 → FP32 h-state converter, used by the batched prefill
422    /// path under the FP16-sized pool (`--ssm-h-dtype f16-pool`).
423    pub(super) ssm_h_f16_to_f32_kernel: KernelHandle,
424    /// 2026-09-25: Staging for the FP32 → FP16 conversion
425    /// (`ssm_h_to_f16_dispatch`), one layer wide (`h_bytes / 2`), allocated on
426    /// first use. The narrowing cannot run in place: output element `i` is
427    /// written into the bytes that hold input element `i / 2`.
428    pub(super) ssm_h_f16_scratch: std::sync::OnceLock<DevicePtr>,
429
430    /// 2026-09-25: Fixed-address device buffer for the batched-decode MoE
431    /// per-row adapter map, `[max_batch_size]` i32 (`< 0` = base row, `>= 0` =
432    /// fold that adapter). Allocated in `new` and refreshed per decode step by
433    /// `upload_moe_row_adapter`, which does not touch it when `lora` is `None`.
434    pub(super) moe_row_adapter_buf: DevicePtr,
435
436    // 2026-09-25: Two-phase SSM prefill buffers, `gdn_buf_max_len` rows each,
437    // shared by every layer and sequence. NULL when the model has no GDN
438    // layers (`build_gdn_prefill_buffers`).
439    /// 2026-09-25: Packed QKV, `[rows, conv_dim]` BF16, per token
440    /// `[Q(key_dim) | K(key_dim) | V(value_dim)]`.
441    pub(super) gdn_buf_qkv: DevicePtr,
442    /// 2026-09-25: Gate and beta, `[rows, 2 * num_v_heads]` FP32.
443    pub(super) gdn_buf_gate_beta: DevicePtr,
444    /// 2026-09-25: GDN output, `[rows, value_dim]` BF16.
445    pub(super) gdn_buf_out: DevicePtr,
446    /// 2026-09-25: Z gate for the gated RMS norm, `[rows, value_dim]` BF16.
447    pub(super) gdn_buf_z: DevicePtr,
448    /// 2026-09-25: Row count of the buffers above:
449    /// `min(max_batch_tokens, max_seq_len)`.
450    pub(super) gdn_buf_max_len: usize,
451
452    /// 2026-09-25: Logit softcapping, `logits = cap * tanh(logits / cap)`.
453    /// `KernelHandle(0)` when the model has no softcapping.
454    pub(super) logit_softcap_kernel: KernelHandle,
455    /// 2026-09-25: Always `KernelHandle(0)`: `use_fp32_logits` is always false.
456    pub(super) logit_softcap_fp32_kernel: KernelHandle,
457    /// 2026-09-25: Always false: `new` sets it so, and the BF16 logits path is
458    /// always taken.
459    pub(super) use_fp32_logits: bool,
460    /// 2026-09-25: FP32 logits scratch, allocated only when `use_fp32_logits`;
461    /// NULL otherwise.
462    pub(super) logits_fp32_buf: DevicePtr,
463    /// 2026-09-25: Embedding scale by `config.embed_scale`.
464    /// `KernelHandle(0)` when the model does not scale embeddings.
465    pub(super) embed_scale_kernel: KernelHandle,
466    /// 2026-09-25: Per-adapter-slot token overlay tables (embed and lm_head row
467    /// overrides), built by `set_lora_weights` only when some adapter
468    /// overrides a row. `None` makes every overlay hook return early.
469    pub(super) overlays: Option<metrale_model_layers::lora::TokenOverlaySet>,
470    /// 2026-09-25: Token overlay kernels, resolved in `new`.
471    pub(super) overlay_kernels: metrale_model_layers::layers::ops::token_overlay::OverlayKernels,
472    /// 2026-09-25: The overlay route of the current forward: the request's
473    /// `adapter_slot`, stamped at each `Model` entry
474    /// (`stamp_overlay_route` / `stamp_overlay_route_batch`). `i32::MIN` marks
475    /// a batch that mixes adapters; the hooks then skip.
476    pub(super) overlay_route_slot: std::sync::atomic::AtomicI32,
477    /// 2026-09-25: MoE LoRA route of the current decode batch, read through
478    /// `decode_moe_route()`: 0 = Skip, 1 = Fold, 2 = Refuse (any other value
479    /// reads as Fold). Initialised to 1.
480    pub(super) decode_moe_route: std::sync::atomic::AtomicI32,
481    /// 2026-09-28: The circuit executor decode runs (`ModelCircuit::set_forward`); `None` runs
482    /// the legacy layer loops.
483    pub(super) circuit:
484        parking_lot::RwLock<Option<metrale_model_layers::circuit_exec::CircuitExec>>,
485}