spark_runtime/buffers/sizes.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Byte sizes for the per-pass GPU buffer arena.
4
5use atlas_core::config::ModelConfig;
6use atlas_core::device::sm121::NUM_SMS;
7
8use super::sizes_q12::{Q12_SIZING_STREAMS, q12_batched_scratch_bytes};
9
10/// Byte sizes of each buffer, derived from ModelConfig.
11#[derive(Debug, Clone)]
12pub struct BufferSizes {
13 pub hidden_states: usize,
14 pub residual: usize,
15 pub norm_output: usize,
16 pub qkv_output: usize,
17 pub attn_output: usize,
18 pub gate_logits: usize,
19 /// FP32 gate logits [m, num_experts] for the ATLAS_FP32_GATE routing path.
20 /// Keeps the router GEMM accumulator unrounded into top-K so near-tied
21 /// experts don't flip on a BF16 store. Allocated whenever num_experts > 0.
22 pub gate_logits_f32: usize,
23 /// FP32 MoE-input norm output [m, hidden] for ATLAS_FP32_ROUTING — the
24 /// full-precision router_in the gate GEMM consumes. Allocated when experts > 0.
25 pub moe_router_in_f32: usize,
26 pub moe_output: usize,
27 pub logits: usize,
28 pub ssm_qkvz: usize,
29 pub ssm_ba: usize,
30 pub ssm_deinterleaved: usize,
31 pub ssm_gates: usize,
32 pub ssm_conv_out_f32: usize,
33 pub scratch: usize,
34 pub expert_gate_out: usize,
35 pub expert_up_out: usize,
36 pub expert_down_out: usize,
37 pub splitk_workspace: usize,
38 /// GDN FLA chunked-prefill scratch (single buffer, sub-divided W|U|S|uc).
39 /// 0 unless the model is a 128-dim-linear-head GDN model (ATLAS_GDN_FLA path).
40 pub gdn_fla_scratch: usize,
41 /// Mamba-2 SSD chunked-scan scratch (single buffer, sub-divided dt | dA_cumsum | CB).
42 /// 0 unless the model has Mamba-2 SSM layers. Shared across layers: they run
43 /// sequentially on one stream, so one allocation serves all 40.
44 pub ssd_scratch: usize,
45 /// Grouped O-projection latent: `[M, o_groups*o_lora_rank]` BF16 (V4-Flash).
46 /// 256 (placeholder) when `o_groups == 0`.
47 pub o_latent: usize,
48 /// Zero-filled BF16 weight (length max_dim) for unweighted RMSNorm under the
49 /// offset-from-1 kernel convention (scale = 1+weight → 1.0). DeepSeek-V4 q_b_norm.
50 pub norm_unit_w: usize,
51 /// HC residual streams: `[M, hc_mult, hidden]` BF16 (DeepSeek-V4 mHC).
52 /// 256 (placeholder) when `hc_mult == 0`.
53 pub hc_streams: usize,
54 /// HC `post` mixing weights: `[M, hc_mult]` F32.
55 pub hc_post: usize,
56 /// HC `comb` Sinkhorn matrix: `[M, hc_mult, hc_mult]` F32.
57 pub hc_comb: usize,
58 /// Low-rank mHC split-collapse scratch (Qwen3.8-Flash-Next): the staged
59 /// normed vector `[T, hc_mult*hidden]` F32 plus the rank vector
60 /// `[T, hc_lowrank]` F32, for SMALL T only — decode runs the collapse as
61 /// three multi-block launches because `grid=[1]` starves the fused kernel
62 /// (measured 2.0 ms/call, one SM's bandwidth). Sized for 64 tokens; the
63 /// dispatcher falls back to the fused kernel above that.
64 pub hc_lowrank_scratch: usize,
65 /// QSA stage-2 prefill-selection scratch (Qwen3.8-Flash-Next), SHARED
66 /// across the 12 indexer layers (they run serially). Layout, slabbed at
67 /// 2048 selective rows: qk [2048, (n_heads+1)*hd] BF16, q_post
68 /// [2048, n_heads, hd] F32, scores [2048, max_seq/ratio] F32, lists
69 /// [2048, topk] i32. 256 (placeholder) when no indexer.
70 pub qsa_select_scratch: usize,
71 /// Token IDs `[M]` u32 for the current pass — stable across the layer loop
72 /// so DeepSeek-V4 hash-MoE layers can read `tid2eid[token_id]`. Always
73 /// allocated (small); unused by models without hash routing.
74 pub token_ids: usize,
75 /// Dense-FFN activation-quant scratch, SHARED across all layers by the
76 /// MMQ (Q4_K), int8 (W4A8), and NVFP4 (W4A4) prefill paths. Was previously a
77 /// per-`DenseFfnLayer` field → 64× duplication (18 GB on Qwen3.6-27B) that
78 /// OOM'd chunked prefill layer-by-layer. Sized for the largest projection K.
79 /// `ffn_act_q8`: q8_1_mmq activations `m*kpad*4 + 1MB` (Q4_K path).
80 /// `ffn_act_a`: int8 `[m,K]` / NVFP4 packed `[m,K/2]` activations.
81 /// `ffn_act_scale`: int8 `[m,K/32]*4` / NVFP4 `[m,K/16]` group scales.
82 /// 0 for MoE models (dense FFN prefill path is Dense-only).
83 pub ffn_act_q8: usize,
84 pub ffn_act_a: usize,
85 pub ffn_act_scale: usize,
86 /// FP8 block-scaled activation scratch for prefill projections (qkv / o /
87 /// ssm-qkvz). Persistent so the W8A8+FP32-epilogue path stops doing a
88 /// per-projection cuMemAlloc + cuStreamSynchronize + cuMemFree. 1 byte/elem.
89 pub fp8_act: usize,
90 /// Per-128-block FP32 scales paired with `fp8_act` (one f32 per 128 elems).
91 pub fp8_act_scale: usize,
92 /// LoRA shrink output `xa = x@Aᵀ`: [m, adapter_max_rank] BF16.
93 /// 0 (→ NULL alloc) when no adapter is configured (adapter_max_rank == 0).
94 pub lora_xa: usize,
95 /// LoRA expand output `delta = xa@Bᵀ`: [m, max target n_out] BF16, where
96 /// max n_out = max(hidden, intermediate) — covers k/v/o/gate/up/down in
97 /// v0 (q_proj is excluded). 0 (→ NULL) when no adapter.
98 pub lora_delta: usize,
99 /// LoRA hidden-activation scratch [m, intermediate_size] BF16 for the
100 /// runtime delta path on FFN projections. 0 (→ NULL) when no adapter.
101 pub lora_hact: usize,
102 /// LoRA per-request routing slots `[m]` i32 — one adapter SLOT index per
103 /// prefilling token (all equal for a single-request prefill; resolves
104 /// `-1`→active before upload). Dedicated buffer (not a packed meta offset)
105 /// so the m-element prefill slot array never collides with the per-path
106 /// positions/slots/block_table region. 0 (→ NULL) when no adapter
107 /// (adapter_max_rank == 0).
108 pub lora_seq_slot: usize,
109 /// Native keep-packed Q2_0 prefill transient-dequant scratch
110 /// (`ATLAS_GGUF_NATIVE_Q2=1`). ONE persistent BF16 `[N,K]` buffer sized to
111 /// the LARGEST keep-packed projection, REUSED for every per-projection
112 /// dequant so prefill stops doing a per-matmul cuMemAlloc +
113 /// cuStreamSynchronize + cuMemFree (the multi-second fixed cost behind the
114 /// 3.7 s / 28-token TTFT regression). 0 (→ NULL) unless the flag is set.
115 pub q2_dequant_scratch: usize,
116 /// Native Q2_0 MMQ prefill q8_1 activation scratch (`ATLAS_GGUF_NATIVE_Q2_MMQ=1`).
117 /// ONE persistent q8_1_mmq buffer (`m*kpad*4 + 1MB`) shared by every kept-packed
118 /// projection (FFN gate/up/down, attn q/k/v/o, GDN qkvz): each seam quantizes
119 /// its BF16 activation into this buffer then runs the packed MMQ GEMM — so the
120 /// 2-bit weight is never dequantized to a BF16 scratch (kills the ~2s dequant
121 /// tax AND the shared-`q2_dequant_scratch` co-dispatch race). Sized to the
122 /// widest projection K = max(hidden, intermediate, q_heads*head_dim).
123 /// 0 (→ NULL) unless the MMQ sub-flag is set.
124 pub q2_act_q8: usize,
125}
126
127impl BufferSizes {
128 /// Compute all buffer sizes from model config and max batch tokens.
129 ///
130 /// All sizes in bytes. BF16 = 2 bytes per element.
131 /// Logits buffer is capped: only needed for decode (1 token) or
132 /// speculative verification (K tokens), never for full prefill.
133 ///
134 /// `max_seq_len` and `kv_block_size` are needed to size the scratch
135 /// buffer for block table metadata during batched decode / verify.
136 pub fn from_config(
137 config: &ModelConfig,
138 max_batch_tokens: usize,
139 max_seq_len: usize,
140 kv_block_size: usize,
141 max_batch_size: usize,
142 ) -> Self {
143 // Derived batched-decode metadata layout (rows = max(32, bs)).
144 // Byte-identical sizing for every bs <= 32; see `decode_meta.rs`.
145 let decode_meta = super::DecodeMetaLayout::for_max_batch_size(max_batch_size);
146 let bf16 = 2;
147 let m = max_batch_tokens;
148 let h = config.hidden_size;
149
150 // Q projection output: gated models produce [Q, gate] (2× nq*hd),
151 // ungated models (VL) produce only [Q] (nq*hd).
152 let q_heads = config.num_attention_heads;
153 let kv_heads = config.num_key_value_heads;
154 let hd = config.head_dim;
155 let q_proj_mul = if config.attn_gated { 2 } else { 1 };
156 let qkv_dim = (q_heads * q_proj_mul + 2 * kv_heads) * hd;
157
158 let top_k = config.num_experts_per_tok;
159
160 // Scratch layout (two users, take max):
161 //
162 // A) Prefill chunk metadata (after MoE routing data):
163 // [0 .. moe_scratch): MoE topK routing indices+weights
164 // [moe_scratch .. ): positions(m*4) + slots(m*8) + block_table(max_blocks*4) + seq_len(4)
165 //
166 // B) Batched decode/verify metadata:
167 // [0 .. 32768): fixed metadata region
168 // [32768 .. 32768+24R): decode metadata (positions, seq_slot,
169 // slots, seq_lens; R = decode-meta rows, `decode_meta.rs` —
170 // 24R = 768 at the 32-row floor)
171 // [32768+24R .. ): decode block table (padded_n × max_blocks × 4 B)
172 // Batched MTP verify (verify_e.rs) overlays the SAME base with
173 // VERIFY_ROW_CAP-row gaps at derived offsets (verify_e.rs VMETA_*),
174 // bt at +24R (bt_rows mirrors the cap).
175 // Each path re-uploads its own layout pre-dispatch; sizing takes
176 // the wider (verify) envelope.
177 //
178 // MoE scratch: 2 * M * top_k * 4 (indices [M*top_k] u32 + weights [M*top_k] f32)
179 let moe_scratch = 2 * m * top_k * 4;
180 let max_blocks = max_seq_len
181 .checked_div(kv_block_size)
182 .map(|q| q + 1)
183 .unwrap_or(256);
184 // Prefill metadata: mirrors exact layout in prefill_chunk(). MRoPE
185 // (Qwen3-VL / Qwen3.6) uploads THREE u32 position streams packed
186 // back-to-back (T, H, W); every other model uploads ONE. Sizing the
187 // scratch region for 1× with MRoPE active caused `cuMemcpyHtoDAsync_v2
188 // status 1` failures on long-context prefills (observed: 16k Qwen3.6
189 // failed, 8k passed because the extra 64 KB of write overflow happened
190 // to still land inside the over-provisioned `moe_scratch + meta`
191 // aggregate).
192 let pos_streams = if config.mrope_interleaved { 3 } else { 1 };
193 let pos_bytes = m * 4 * pos_streams;
194 let slot_offset = (pos_bytes + 7) & !7;
195 let slot_end = slot_offset + m * 8;
196 let bt_offset = (slot_end + 3) & !3;
197 let bt_end = bt_offset + max_blocks * 4;
198 let sl_offset = (bt_end + 3) & !3;
199 let prefill_meta = sl_offset + 4;
200 // Block table metadata: the widest user is the batched MTP verify
201 // (verify_e.rs) at R = bt_rows (mirrors VERIFY_ROW_CAP; was 96, the wave-11
202 // depth-at-width envelope — 32:2), whose bt staging sits at
203 // meta_base+2048 (wider 96-row gaps: positions 384 | seq_slot 384 |
204 // slots 768 | seq_lens 384). Batched decode (padded_n ≤ 32) and
205 // DFlash K=γ+1=17 verify keep the narrow +768 layout — strictly
206 // inside this envelope.
207 let bt_rows = 160usize; // batched verify R cap (VERIFY_ROW_CAP, verify_e2.rs)
208 // Envelope = max(verify 96-row overlay, DERIVED decode layout).
209 // The decode layout (`decode_meta.rs`, rows = max(32, bs)) sits
210 // strictly inside the verify overlay for every rows <= 64 (bt at
211 // 24R <= 1536 < 2048, rows <= 96), so this max() changes NOTHING
212 // for bs <= 64; it only grows the scratch once rows > ~85.
213 let bt_meta = 32768
214 + (bt_rows * 24 + bt_rows * max_blocks * 4).max(decode_meta.meta_bytes(max_blocks));
215 let scratch_min = 64 * 1024;
216 // Q12 kernel-batched prefill stages N per-stream meta blocks plus a
217 // stacked BatchedAttnMetadata block — a strictly larger footprint than
218 // the single-stream `prefill_meta`. Provision for `Q12_SIZING_STREAMS`
219 // streams splitting the full token arena so the fast path stays
220 // available for deep-context concurrent prefills without overrunning
221 // scratch (#110: the unprovisioned N-stream multiplication overran the
222 // buffer, producing an out-of-range HtoD → sticky CUDA-700).
223 let q12_chunk = m.div_ceil(Q12_SIZING_STREAMS).max(1);
224 let q12_batched = q12_batched_scratch_bytes(
225 Q12_SIZING_STREAMS,
226 q12_chunk,
227 top_k,
228 config.mrope_interleaved,
229 );
230 let scratch = scratch_min
231 .max(moe_scratch + prefill_meta)
232 .max(bt_meta)
233 .max(q12_batched);
234
235 // Batched expert output buffers for MoE (or dense FFN).
236 // Sized for max(K=3 verify, prefill chunk) × top_k experts.
237 let k_max = m.max(3); // prefill chunk or K=3 verify, whichever larger
238 let expert_inter = if config.num_experts > 0 {
239 let routed = config.num_experts_per_tok * config.moe_intermediate_size;
240 k_max * routed.max(config.intermediate_size)
241 } else {
242 k_max * config.intermediate_size
243 };
244 let expert_gate_out = expert_inter * bf16;
245 let expert_up_out = expert_inter * bf16;
246 // Routed expert down output: [k_max * top_k, moe_input_size].
247 // For LatentMoE (Super 120B), routed experts output in latent space.
248 let moe_out_dim = config.moe_input_size();
249 let expert_down_out = if config.num_experts > 0 {
250 k_max * config.num_experts_per_tok * moe_out_dim * bf16
251 } else {
252 k_max * h * bf16
253 };
254
255 // Logits: only last token used during prefill. Cap at 160 tokens —
256 // the batched MTP verify's R = Σ ks row cap (n=32 × k=3 rows, the
257 // wave-11 depth-at-width envelope; VERIFY_ROW_CAP in verify_e2.rs).
258 // This also covers decode=1, batched decode padded_n<=32 PLUS the
259 // run_standard mixed path (`decode_b2`) parking prefill logits at
260 // row `padded_n` = 32 (the old 33-row bound), spec_verify≤5, and
261 // DFlash K=γ+1=17. ~45 MB at vocab 248320 (was ~30 MB at 64 rows,
262 // ~16 MB at 33).
263 // Derived floor for wide native batches: the run_standard mixed path
264 // (`decode_b2`) parks prefill logits at row `padded_n`, which can be
265 // as high as `decode_meta.rows()` — so the arena must hold rows+1.
266 // Inert (160) for every rows <= 159, i.e. all bs <= 159.
267 let logits_tokens = m.min(160.max(decode_meta.rows() + 1));
268
269 // Mamba-2 d_inner may exceed hidden_size; norm_output and attn_output must fit.
270 let mamba2_d_inner = config.mamba2_d_inner();
271 let max_dim = h.max(mamba2_d_inner);
272
273 // Split-K decode workspace: NUM_SMS * (head_dim + 2) * sizeof(f32).
274 // Partials from split CTAs are stored as [o[head_dim], m, l] per split.
275 // Total slots = num_seqs * num_splits ≤ NUM_SMS, so this is constant ~48 KB.
276 // Read NUM_SMS rather than repeating its value: run_paged_decode derives
277 // num_splits from the same constant, so a literal here is a second source
278 // of truth that under-allocates — silently, into out-of-bounds device
279 // writes — the moment the constant moves.
280 let splitk_workspace = NUM_SMS as usize * (hd + 2) * 4;
281
282 // The residual stream is always BF16.
283 let residual_elem = bf16;
284
285 // FP8 block-scaled activation scratch for prefill projections. The
286 // widest contract dim across call sites is hidden (qkv / ssm-qkvz) or
287 // q_heads*head_dim (o_proj). 1 byte/elem fp8 + one f32 per 128-block.
288 // Mamba-2 out_proj contracts over d_inner (may exceed hidden), and its
289 // prefill input is FP8-precast into this buffer.
290 let max_proj_k = h.max(q_heads * hd).max(mamba2_d_inner);
291 let fp8_act = m * max_proj_k;
292 let fp8_act_scale = m * max_proj_k.div_ceil(128) * 4;
293 // LoRA scratch — only when an adapter is configured (adapter_max_rank
294 // set programmatically pre-build). Widest target n_out =
295 // max(hidden, intermediate, q_proj): covers k/v, o/down (hidden),
296 // gate/up (intermediate), and gated q_proj (2*q_heads*head_dim, which
297 // can exceed both — e.g. 35B 2*16*256=8192 > hidden 4096).
298 let (lora_xa, lora_delta, lora_hact, lora_seq_slot) = if config.adapter_max_rank > 0 {
299 let max_n = h
300 .max(config.intermediate_size)
301 .max(q_proj_mul * q_heads * hd);
302 (
303 m * config.adapter_max_rank * bf16,
304 m * max_n * bf16,
305 m * config.intermediate_size * bf16,
306 m * 4, // [m] i32 per-request routing slots (prefill path)
307 )
308 } else {
309 (0, 0, 0, 0)
310 };
311
312 // GDN FLA chunked-prefill scratch — ONE buffer holding W|U|S|uc back-to-back,
313 // sized for the chunked-prefill arena (nt = ceil(max_batch_tokens / CHUNK)).
314 // Only the 128-dim-linear-head GDN path uses it (the FLA kernels are compiled
315 // for K_DIM=V_DIM=128); 0 otherwise so BufferArena allocs NULL and the
316 // ATLAS_GDN_FLA dispatch stays disabled. Layout per region:
317 // W [nt*nv][CHUNK][kd] bf16 ; U,uc [nt*nv][CHUNK][vd] bf16 ;
318 // S [nt*nv][kd][vd] bf16 ; gc [nt*nv][CHUNK] f32.
319 const FLA_CHUNK: usize = 64;
320 // SSD chunked scan (mamba2_ssd_*): dt[H][nc][L] f32 + dA_cs[H][nc][L] f32
321 // + CB[nc][G][L][L] f32, L = 64.
322 const SSD_L: usize = 64;
323 let ssd_scratch = if config.mamba_num_heads > 0 && config.ssm_state_size > 0 {
324 let nc = m.div_ceil(SSD_L) + 1;
325 let hh = config.mamba_num_heads;
326 let gg = config.n_groups.max(1);
327 (hh * nc * SSD_L * 4) * 2 + nc * gg * SSD_L * SSD_L * 4
328 } else {
329 0
330 };
331
332 let gdn_fla_scratch = if config.linear_num_value_heads > 0
333 && config.linear_key_head_dim == 128
334 && config.linear_value_head_dim == 128
335 {
336 // +margin: the batched FLA path (ATLAS_GDN_BATCHED_FLA) sizes its
337 // regions by total_nt = batch*ceil(chunk_len/64), which can exceed
338 // ceil(m/64) by up to `batch` chunks due to per-stream last-chunk
339 // rounding. 16 covers the co-dispatch max-seqs.
340 let nt = m.div_ceil(FLA_CHUNK) + 16;
341 let nv = config.linear_num_value_heads;
342 let kd = config.linear_key_head_dim;
343 let vd = config.linear_value_head_dim;
344 let w = nt * nv * FLA_CHUNK * kd * bf16;
345 let u = nt * nv * FLA_CHUNK * vd * bf16;
346 let s = nt * nv * kd * vd * bf16;
347 let uc = nt * nv * FLA_CHUNK * vd * bf16;
348 let gc = nt * nv * FLA_CHUNK * 4;
349 w + u + s + uc + gc
350 } else {
351 0
352 };
353
354 // Native keep-packed Q2_0 prefill scratch (Tier-1 transient-dequant +
355 // Tier-2 MMQ q8_1 activation); env-gated, 0 unless the flags are set.
356 // Sizing rationale + bounds live on `sizes_q2::q2_scratch_sizes`.
357 let (q2_dequant_scratch, q2_act_q8) = super::sizes_q2::q2_scratch_sizes(config, m, h, hd);
358
359 // Dense-FFN activation-quant scratch, shared across all layers (SSOT).
360 // Sized for the largest projection K = max(hidden, intermediate); the
361 // dense_ffn prefill paths pass `h.max(inter)` to the requant kernels.
362 // 0 for MoE (num_experts>0) — those never take the dense_ffn MMQ path.
363 let (ffn_act_q8, ffn_act_a, ffn_act_scale) = if config.num_experts == 0 {
364 let kmax = h.max(config.intermediate_size);
365 let kpad = kmax.div_ceil(256) * 256;
366 (
367 m * kpad * 4 + (1 << 20), // q8_1_mmq: m*kpad*4 + 1MB (matches q8_1_scratch_bytes)
368 m * kmax, // int8 a_i8 [m,K] ≥ NVFP4 packed [m,K/2]
369 m * (kmax / 32) * 4, // int8 a_scale [m,K/32]*4 ≥ NVFP4 scale [m,K/16]
370 )
371 } else {
372 (0, 0, 0)
373 };
374
375 Self {
376 hidden_states: m * h * residual_elem,
377 residual: m * h * residual_elem,
378 norm_output: m * max_dim * bf16,
379 qkv_output: m * qkv_dim * bf16,
380 attn_output: (m * config.num_attention_heads * config.head_dim * bf16)
381 .max(m * mamba2_d_inner * bf16)
382 // MLA absorbed: attention output is [M, nq, mla_cache_dim=kv_lora+rope]
383 .max(if config.kv_lora_rank > 0 {
384 m * config.num_attention_heads
385 * (config.kv_lora_rank + config.qk_rope_head_dim)
386 * bf16
387 } else {
388 0
389 }),
390 gate_logits: if config.num_experts > 0 {
391 // LongCat zero-experts: the router scores (routed + zero)
392 // logits even though only `num_experts` expert FFNs exist.
393 m * (config.num_experts + config.zero_expert_num) * bf16
394 } else {
395 256
396 },
397 gate_logits_f32: if config.num_experts > 0 {
398 m * (config.num_experts + config.zero_expert_num) * 4
399 } else {
400 256
401 },
402 moe_router_in_f32: if config.num_experts > 0 {
403 m * h * 4
404 } else {
405 256
406 },
407 moe_output: m * h * bf16,
408 logits: logits_tokens * config.vocab_size * bf16, // BF16 from LM head kernel
409 // SSM buffers are also reused by attention prefill/multi-seq as scratch:
410 // ssm_qkvz: K+V contiguous storage in prefill [M, 2*kv_dim]
411 // Mamba-2 in_proj output [M, in_proj_size]
412 // ssm_deinterleaved: Q contiguous copy [M, nq*hd]
413 // Mamba-2 conv1d output [M, d_xBC]
414 // Use max across all uses with minimum 256 to avoid 0-byte alloc.
415 ssm_qkvz: (m * config.ssm_qkvz_size() * bf16)
416 .max(m * config.mamba2_in_proj_size() * bf16)
417 .max(m * 2 * kv_heads * hd * bf16)
418 .max(m * config.shared_expert_intermediate_size * bf16) // MoE shared up scratch
419 .max(256),
420 ssm_ba: (m * config.ssm_ba_size() * bf16)
421 .max(m * config.moe_latent_size * bf16) // LatentMoE latent buffer
422 // MLA reuses ssm_ba for two separate buffers:
423 // - q_latent [M, q_lora_rank] BF16 — output of wq_a GEMM
424 // - k_rope_buf [M, qk_rope_head_dim] BF16 — output of wkv_a_rope GEMM
425 // Both are written sequentially (q_latent is consumed before
426 // k_rope_buf is allocated). Size for the larger of the two.
427 .max(if config.kv_lora_rank > 0 {
428 (m * config.qk_rope_head_dim * bf16).max(m * config.q_lora_rank * bf16)
429 } else {
430 0
431 })
432 .max(256),
433 ssm_deinterleaved: (m * config.ssm_qkvz_size() * bf16)
434 .max(m * config.mamba2_d_xbc() * bf16)
435 .max(m * q_heads * hd * bf16)
436 // MLA absorbed: Q_absorbed buffer is [M, nq, mla_cache_dim=kv_lora+rope]
437 .max(if config.kv_lora_rank > 0 {
438 m * q_heads * (config.kv_lora_rank + config.qk_rope_head_dim) * bf16
439 } else {
440 0
441 })
442 .max(256),
443 ssm_gates: (m * config.linear_num_value_heads * 2 * 4).max(256),
444 // FP32 conv output for SSM recurrent path precision (4 bytes/element).
445 // Uses ssm_qkvz_size as upper bound (includes Q+K+V+Z).
446 // Also reused by MLA as q_rope contiguous buffer: [M, nq * qk_rope_head_dim] BF16.
447 ssm_conv_out_f32: (m * config.ssm_qkvz_size() * 4)
448 .max(if config.kv_lora_rank > 0 {
449 m * q_heads * config.qk_rope_head_dim * bf16
450 } else {
451 0
452 })
453 .max(256),
454 scratch,
455 expert_gate_out,
456 expert_up_out,
457 expert_down_out,
458 splitk_workspace,
459 gdn_fla_scratch,
460 ssd_scratch,
461 // Grouped O-projection latent (V4-Flash): [M, o_groups*o_lora_rank].
462 o_latent: (m * config.o_groups * config.o_lora_rank * bf16).max(256),
463 // Zero-filled weight for unweighted RMSNorm (q_b_norm).
464 norm_unit_w: max_dim * bf16,
465 // HC buffers: only allocated for DeepSeek-V4 (hc_mult > 0).
466 hc_streams: if config.hc_mult > 0 {
467 // FP32 mHC highway: the residual streams grow large across the
468 // blocks (the manifold-mixing is norm-preserving, eigenvalue 1),
469 // so BF16 storage swamps the small per-layer signal at scale and
470 // collapses generation. Store the streams in FP32 (4 bytes).
471 m * config.hc_mult * h * 4
472 } else {
473 256
474 },
475 hc_post: if config.hc_mult > 0 {
476 (m * config.hc_mult * 4).max(256)
477 } else {
478 256
479 },
480 hc_comb: if config.hc_mult > 0 {
481 (m * config.hc_mult * config.hc_mult * 4).max(256)
482 } else {
483 256
484 },
485 hc_lowrank_scratch: if config.hc_mult > 0 && config.hc_lowrank > 0 {
486 // Two exclusive layouts share this region:
487 // - decode split path (T <= 64): normed FP32 [64, hc*H] then
488 // low FP32 [64, rank];
489 // - prefill GEMM path (T > 64, slabbed at <= 2048 tokens):
490 // normed BF16 [Ts, hc*H], up_pre BF16 [Ts, hc*H],
491 // low BF16 [Ts, rank], inj_pre BF16 [Ts, hc].
492 let t = m.min(64);
493 let split = t * (config.hc_mult * h + config.hc_lowrank) * 4;
494 let ts = m.min(2048);
495 let gemm = ts * (2 * config.hc_mult * h + config.hc_lowrank + config.hc_mult) * 2;
496 split.max(gemm)
497 } else {
498 256
499 },
500 qsa_select_scratch: if config.index_topk > 0 && config.index_compress_ratio > 0 {
501 const ROWS: usize = 2048;
502 let qkw = (config.index_n_heads + 1) * config.index_head_dim;
503 let n_blocks = max_seq_len.div_ceil(config.index_compress_ratio);
504 let topk = config.index_topk / config.index_compress_ratio;
505 ROWS * qkw * 2
506 + ROWS * config.index_n_heads * config.index_head_dim * 4
507 + ROWS * n_blocks * 4
508 + ROWS * topk * 4
509 } else {
510 256
511 },
512 // Token IDs [M] u32 (stable across the layer loop for hash-MoE).
513 token_ids: (m * 4).max(256),
514 ffn_act_q8,
515 ffn_act_a,
516 ffn_act_scale,
517 fp8_act,
518 fp8_act_scale,
519 lora_xa,
520 lora_delta,
521 lora_hact,
522 lora_seq_slot,
523 q2_dequant_scratch,
524 q2_act_q8,
525 }
526 }
527
528 /// Total bytes across all buffers.
529 pub fn total_bytes(&self) -> usize {
530 self.hidden_states
531 + self.residual
532 + self.norm_output
533 + self.qkv_output
534 + self.attn_output
535 + self.gate_logits
536 + self.gate_logits_f32
537 + self.moe_router_in_f32
538 + self.moe_output
539 + self.logits
540 + self.ssm_qkvz
541 + self.ssm_ba
542 + self.ssm_deinterleaved
543 + self.ssm_gates
544 + self.ssm_conv_out_f32
545 + self.scratch
546 + self.expert_gate_out
547 + self.expert_up_out
548 + self.hc_lowrank_scratch
549 + self.qsa_select_scratch
550 + self.expert_down_out
551 + self.splitk_workspace
552 + self.gdn_fla_scratch
553 + self.ssd_scratch
554 + self.hc_streams
555 + self.hc_post
556 + self.hc_comb
557 + self.token_ids
558 + self.ffn_act_q8
559 + self.ffn_act_a
560 + self.ffn_act_scale
561 + self.fp8_act
562 + self.fp8_act_scale
563 + self.lora_xa
564 + self.lora_delta
565 + self.lora_hact
566 + self.lora_seq_slot
567 + self.q2_dequant_scratch
568 + self.q2_act_q8
569 }
570}