spark_model/layers/qwen3_attention/mod.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Qwen3 full attention layer.
4//!
5//! Q/K/V projection -> Q/K norms -> RoPE -> KV cache write ->
6//! paged decode attention -> O projection, then MoE FFN.
7//!
8//! Split into submodules:
9//! - `types`: `MlaWeights` + `Qwen3AttentionLayer` struct definitions
10//! - `init`: `new`, `new_ungated`, `new_with_gating` (kernel loading)
11//! - `helpers`: setters + `apply_layer_scalar` + `effective_attn_scale`
12//! - `prefill_weights`: prefill weight setup + W4A16 M128 dispatcher
13//! - `decode`: single-token attention forward + KV cache helpers
14//! - `prefill`: batched prefill with paged attention
15//! - `trait_impl`: `TransformerLayer` trait implementation
16
17// The bit-exact N-column-blocked decode tier for the FP8 attention
18// projections (#927). Lives beside `init` rather than inside `trait_impl`
19// because `init` caches its lever on the layer and BOTH multi-seq call sites
20// (QKV strided, o_proj contiguous) read the one rule.
21mod attn_ncol_gemv;
22// The `ATLAS_ATTN_M16_TC` route lines (#927, H100 round 9 cell W) — `pub(crate)`
23// because both multi-seq call sites that need them
24// (`trait_impl::multi_seq::qkv_fp8_batch`, `trait_impl::multi_seq::attn::o_proj`)
25// reach it via the full crate path, the same way `dense_ffn_m16_tc`'s route
26// log is reached from outside its own file.
27pub(crate) mod attn_m16_tc_route;
28mod decode;
29// V4: `pub(crate)` so the DeepSeek-V4 weight loader (`weight_loader::deepseek_v4`)
30// and the V4 attention submodules can call `helpers::yarn_rope_mscale`. Non-V4
31// code paths are unaffected by the wider visibility.
32pub(crate) mod helpers;
33mod init;
34mod init_arch_gates;
35mod init_kernel_dispatch;
36mod kernel_requirements;
37mod op_dump;
38// `innerq_driver` calls the CUDA Driver API directly via `atlas_core::registry`,
39// which is itself gated on the `cuda` feature. Mirror that gate here so the
40// metal-only build of spark-model (`--no-default-features --features metal`)
41// compiles on Apple Silicon without dragging in `atlas_core::registry`.
42#[cfg(feature = "cuda")]
43pub mod innerq_driver;
44mod prefill;
45// The cuBLASLt W8A8 prefill arm that replaced `ATLAS_CUBLAS_GEMM=attn`'s
46// off-ledger BF16 weight dequant (#917 round 3 / #927).
47mod prefill_qkv_w8a8;
48mod prefill_w8a8;
49mod prefill_weights;
50mod trait_impl;
51mod types;
52mod types_weights;
53
54#[cfg(feature = "cuda")]
55pub use innerq_driver::InnerQDriver;
56// V4: re-export the new hyper-connection / compressor weight types alongside the
57// existing ones. These are only constructed under DeepSeek-V4 detection.
58pub(crate) use types::HeadGateActivation;
59pub use types::Qwen3AttentionLayer;
60pub use types_weights::{
61 CompressorWeights, Fp8TwinSet, HcHeadWeights, HcLowRank, HcSiteWeights, HcWeights, MlaWeights,
62 W8A8_PREFILL_KERNELS, w8a8_prefill_kernels_loaded,
63};
64
65/// Startup fail-fast for `--kv-cache-dtype`: resolve every kernel handle the
66/// dtype's dispatch arms require (chunked-prefill kernel, WHT bookends) and
67/// error with the full missing list — BEFORE the multi-minute weight load,
68/// instead of at first dispatch. See `kernel_requirements.rs`.
69pub fn validate_required_kv_kernels(
70 gpu: &dyn spark_runtime::gpu::GpuBackend,
71 kv_dtype: spark_runtime::kv_cache::KvCacheDtype,
72 head_dim: usize,
73) -> anyhow::Result<()> {
74 kernel_requirements::validate_required_kernels(gpu, kv_dtype, head_dim)
75}
76
77// The InnerQ driver is owned by `TransformerModel` and reached through
78// `Model::poll_innerq`. It used to live in a process-wide static here, which
79// let it outlive the model whose `__device__` globals it writes.
80
81/// Reference sequence count for the split-K split-count computation.
82///
83/// `num_splits = NUM_SMS / (num_q_heads * num_seqs)` made a sequence's
84/// attention reduction tree depend on how many other sequences happened to be
85/// co-batched in that step. The online-softmax split-merge is non-associative,
86/// so the same sequence produced a few-ULP-different attention output (and a
87/// different temp-0 argmax) when decoded alone vs co-batched — nondeterministic
88/// output under concurrent load. Pinning the split count to the configured max
89/// batch (`ModelLevers::max_decode_seqs`) makes it invariant to co-batch count.
90/// See `tasks/determinism_investigation.md`.
91///
92/// Clamped to at least `num_seqs` so `num_splits` can never exceed what the
93/// fixed-size split-K workspace (`NUM_SMS` slots) supports for the actual batch.
94pub(crate) fn split_ref_seqs(num_seqs: u32, max_decode_seqs: u32) -> u32 {
95 // NOTE (2026-06-03): tried unpinning this for num_seqs==1 to raise split-K
96 // occupancy (16→48 CTAs) for single-stream long-ctx decode — clean A/B
97 // (eqfix vs splitk, same 21.8k code task) was BYTE-IDENTICAL (12.7 tok/s
98 // both), confirming attention occupancy is NOT the long-ctx bottleneck
99 // (attention is ~5% of decode bytes at depth). Reverted. The real ~3.6x
100 // decode gap vs vLLM is core kernel efficiency (MoE GEMV + per-step
101 // overhead), a separate multi-week effort. Determinism pin kept intact.
102 max_decode_seqs.max(num_seqs)
103}
104
105/// Host-time accumulator for the FFN/MoE half of prefill layers
106/// (`ATLAS_PREFILL_HOST_TIMING=1`). Summed across layers and read+reset once
107/// per prefill by the layer loop, so the attention half can be derived as
108/// loop_wall - ffn.
109pub static FFN_HOST_US: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
110
111pub fn add_ffn_host_us(us: u64) {
112 FFN_HOST_US.fetch_add(us, std::sync::atomic::Ordering::Relaxed);
113}
114
115pub fn take_ffn_host_us() -> u64 {
116 FFN_HOST_US.swap(0, std::sync::atomic::Ordering::Relaxed)
117}
118
119/// Per-phase host-time accumulators for the prefill ATTENTION path
120/// (`ATLAS_PREFILL_HOST_TIMING=1`). Index: 0=qkv projections, 1=everything
121/// between qkv and the attention call (deinterleave + per-head norms + RoPE +
122/// KV write), 2=the attention kernel call itself, 3=o_proj + head gate.
123/// Summed across layers; read and reset once per prefill.
124pub static ATTN_PHASE_US: [std::sync::atomic::AtomicU64; 4] = [
125 std::sync::atomic::AtomicU64::new(0),
126 std::sync::atomic::AtomicU64::new(0),
127 std::sync::atomic::AtomicU64::new(0),
128 std::sync::atomic::AtomicU64::new(0),
129];
130
131pub fn add_attn_phase_us(i: usize, us: u64) {
132 ATTN_PHASE_US[i].fetch_add(us, std::sync::atomic::Ordering::Relaxed);
133}
134
135pub fn take_attn_phase_us() -> [u64; 4] {
136 let mut o = [0u64; 4];
137 for (i, a) in ATTN_PHASE_US.iter().enumerate() {
138 o[i] = a.swap(0, std::sync::atomic::Ordering::Relaxed);
139 }
140 o
141}
142
143#[cfg(test)]
144mod split_ref_seqs_tests {
145 use super::split_ref_seqs;
146
147 #[test]
148 fn the_split_count_does_not_move_with_co_batch_size() {
149 // The whole point of the pin: one sequence decoded alone and the same
150 // sequence co-batched with fifteen others must see the same reduction
151 // tree, or the non-associative split-merge flips its temp-0 argmax.
152 let pin = 16;
153 assert_eq!(split_ref_seqs(1, pin), split_ref_seqs(8, pin));
154 assert_eq!(split_ref_seqs(1, pin), pin);
155 }
156
157 #[test]
158 fn a_batch_larger_than_the_pin_clamps_up() {
159 // `num_splits` must never exceed what the fixed-size split-K workspace
160 // supports for the actual batch.
161 assert_eq!(split_ref_seqs(32, 16), 32);
162 }
163
164 #[test]
165 fn two_models_can_pin_to_different_batches() {
166 // Was a `OnceLock`, so the second model to load silently kept the
167 // first's max batch — and with it the first model's split count.
168 assert_ne!(split_ref_seqs(1, 4), split_ref_seqs(1, 16));
169 }
170}