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