spark_runtime/buffers/sizes_q2.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Native keep-packed Q2_0 buffer sizing, split out of `sizes.rs` (≤500 LoC
4//! cap). Both scratch buffers are env-gated — 0 (→ NULL) unless the respective
5//! `ATLAS_GGUF_NATIVE_Q2*` flag is set, so non-Q2 models pay nothing.
6
7use atlas_core::config::ModelConfig;
8
9/// Bytes for the native keep-packed Q2_0 prefill transient-dequant scratch: the
10/// LARGEST keep-packed projection `[N, K]` expanded to BF16 (2 bytes/elem). The
11/// prefill dequant writes `N*K` BF16 elements into this buffer, which is then
12/// consumed by the same-stream GEMM and reused by the next projection.
13///
14/// Every keep-packed projection has exactly one dimension equal to
15/// `hidden_size` (FFN gate/up `[inter, h]`, FFN down `[h, inter]`, attention
16/// q/k/v/o `[·, h]` or `[h, ·]`, fused GDN `in_proj_qkvz [qkvz, h]`), so
17/// `N*K = max_other_dim * hidden` — an EXACT bound, not an over-estimate, for
18/// the covered families (the k/v and gated-q terms are safe upper bounds).
19/// Independent of batch tokens (this dequants the WEIGHT, not activations).
20pub fn q2_dequant_scratch_bytes(config: &ModelConfig) -> usize {
21 let bf16 = 2;
22 let hd = config.head_dim;
23 let q_proj_mul = if config.attn_gated { 2 } else { 1 };
24 let max_n = config
25 .intermediate_size
26 .max(config.ssm_qkvz_size())
27 .max(config.mamba2_in_proj_size())
28 .max(config.num_attention_heads * q_proj_mul * hd)
29 .max(2 * config.num_key_value_heads * hd);
30 max_n * config.hidden_size * bf16
31}
32
33/// `(q2_dequant_scratch, q2_act_q8)` sizes for the arena. `m` = max batch
34/// tokens, `h` = hidden_size, `hd` = head_dim.
35///
36/// - `q2_dequant_scratch` (Tier-1, `ATLAS_GGUF_NATIVE_Q2=1`): the widest
37/// keep-packed projection expanded to BF16 (see [`q2_dequant_scratch_bytes`]).
38/// - `q2_act_q8` (Tier-2 MMQ, `ATLAS_GGUF_NATIVE_Q2_MMQ=1`): the q8_1 activation
39/// scratch. Widest INPUT dim K — FFN gate/up (h) or down (intermediate), attn
40/// qkv (h) or o (q_heads*head_dim), GDN qkvz (h). q8_1_mmq is 4 bytes/elem
41/// over kpad (K rounded to 256), + 1MB margin — matches `q8_1_scratch_bytes`.
42pub fn q2_scratch_sizes(config: &ModelConfig, m: usize, h: usize, hd: usize) -> (usize, usize) {
43 let dequant_enabled = std::env::var("ATLAS_GGUF_NATIVE_Q2").ok().as_deref() == Some("1");
44 let mmq_enabled = std::env::var("ATLAS_GGUF_NATIVE_Q2_MMQ").ok().as_deref() == Some("1");
45 q2_scratch_sizes_for(config, m, h, hd, dequant_enabled, mmq_enabled)
46}
47
48pub(super) fn q2_scratch_sizes_for(
49 config: &ModelConfig,
50 m: usize,
51 h: usize,
52 hd: usize,
53 dequant_enabled: bool,
54 mmq_enabled: bool,
55) -> (usize, usize) {
56 let q2_dequant_scratch = if dequant_enabled {
57 q2_dequant_scratch_bytes(config)
58 } else {
59 0
60 };
61
62 let q2_act_q8 = if mmq_enabled {
63 let kmax = h
64 .max(config.intermediate_size)
65 .max(config.num_attention_heads * hd);
66 let kpad = kmax.div_ceil(256) * 256;
67 m * kpad * 4 + (1 << 20)
68 } else {
69 0
70 };
71
72 (q2_dequant_scratch, q2_act_q8)
73}