spark_model/layers/qwen3_attention/innerq_driver.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Host-side driver for TurboQuant+ InnerQ per-channel K equalization.
4//!
5//! Triggers via `TURBO_INNERQ=N` env var (N = calibration token count). The
6//! kernel-side state lives in `kernels/gb10/common/tq_plus_innerq_apply.cu`
7//! as `__device__` globals inside `namespace tq_plus` — deliberately in the
8//! SAME translation unit (= same PTX module; Atlas has no `-rdc` device
9//! linking) as the apply/accumulate kernels that read it. This driver
10//! manipulates that state directly via the CUDA Driver API:
11//!
12//! `cuModuleGetGlobal_v2` → device pointer for each symbol
13//! `cuMemcpyHtoDAsync_v2` / `cuMemcpyDtoHAsync_v2` → push/pull state
14//!
15//! Two-phase operation:
16//! 1. `start()` — zero counters, set `d_innerq_calibrating = 1`.
17//! 2. `maybe_finalize()` — read `d_innerq_count`; once it crosses
18//! `target_tokens`, read `d_innerq_sq_accum`, compute per-channel
19//! scale + scale_inv, upload, set `d_innerq_active = 1`.
20//!
21//! Math identity: `<Q/s, s·K> = <Q, K>` — the kernel-side apply pass
22//! multiplies Q by `scale_inv` pre-WHT and K by `scale` post-WHT, leaving
23//! attention dot products unchanged while smoothing K variance across
24//! channels.
25
26use std::ffi::c_void;
27use std::sync::atomic::{AtomicBool, Ordering};
28
29use anyhow::{Context, Result, bail};
30use std::sync::Arc;
31
32use atlas_core::registry::AtlasRegistry;
33
34// Itanium-mangled names for `tq_plus::*` device globals. The kernel TU is
35// `kernels/gb10/common/tq_plus_innerq_apply.cu` — the module that also holds
36// `tq_plus_innerq_apply_q/_k`, the only kernels reading this state (module =
37// file stem; no [modules] override in common/KERNEL.toml). It MUST be that
38// module: each PTX module gets its own copy of `__device__` globals (no
39// -rdc), so uploading anywhere else feeds a copy the kernels never see.
40const MODULE: &str = "tq_plus_innerq_apply";
41const SYM_SCALE: &str = "_ZN7tq_plus14d_innerq_scaleE";
42const SYM_SCALE_INV: &str = "_ZN7tq_plus18d_innerq_scale_invE";
43const SYM_SQ_ACCUM: &str = "_ZN7tq_plus17d_innerq_sq_accumE";
44const SYM_COUNT: &str = "_ZN7tq_plus14d_innerq_countE";
45const SYM_ACTIVE: &str = "_ZN7tq_plus15d_innerq_activeE";
46const SYM_CALIBRATING: &str = "_ZN7tq_plus20d_innerq_calibratingE";
47
48// Matches INNERQ_MAX_CHANNELS in tq_plus_innerq.cuh. Head dim = 128 today.
49const MAX_CHANNELS: usize = 128;
50
51pub struct InnerQDriver {
52 /// This model's kernel modules. Held rather than fetched from a global:
53 /// the device symbols below live in THESE modules, and a swapped-in model's
54 /// driver must never resolve them against the previous model's.
55 registry: Arc<AtlasRegistry>,
56 pub target_tokens: i32,
57 pub strength: f32,
58 pub calibrating: AtomicBool,
59 pub finalized: AtomicBool,
60}
61
62impl InnerQDriver {
63 /// Reads `TURBO_INNERQ` and `TURBO_INNERQ_STRENGTH` env vars. Returns
64 /// `None` if `TURBO_INNERQ` is unset, unparsable, or `<= 0`.
65 pub fn from_env(registry: Arc<AtlasRegistry>) -> Option<Self> {
66 let n = std::env::var("TURBO_INNERQ")
67 .ok()
68 .and_then(|v| v.parse::<i32>().ok())
69 .filter(|&n| n > 0)?;
70 let strength: f32 = std::env::var("TURBO_INNERQ_STRENGTH")
71 .ok()
72 .and_then(|v| v.parse().ok())
73 .filter(|&s: &f32| s > 0.0 && s <= 1.0)
74 .unwrap_or(0.5);
75 Some(Self {
76 registry,
77 target_tokens: n,
78 strength,
79 calibrating: AtomicBool::new(false),
80 finalized: AtomicBool::new(false),
81 })
82 }
83
84 /// Enter calibration phase: zero `d_innerq_sq_accum` / `d_innerq_count`
85 /// / `d_innerq_active`, set `d_innerq_calibrating = 1`. Idempotent.
86 pub fn start(&self) -> Result<()> {
87 let reg = &self.registry;
88 let stream = reg.raw_stream();
89
90 let zeros_f32 = [0.0f32; MAX_CHANNELS];
91 let zero_i32: i32 = 0;
92 let one_i32: i32 = 1;
93
94 let (sq_ptr, sq_bytes) = reg
95 .device_symbol(MODULE, SYM_SQ_ACCUM)
96 .with_context(|| format!("resolve {MODULE}::{SYM_SQ_ACCUM}"))?;
97 let (count_ptr, _) = reg.device_symbol(MODULE, SYM_COUNT)?;
98 let (active_ptr, _) = reg.device_symbol(MODULE, SYM_ACTIVE)?;
99 let (calib_ptr, _) = reg.device_symbol(MODULE, SYM_CALIBRATING)?;
100
101 let copy_bytes = sq_bytes.min(std::mem::size_of_val(&zeros_f32));
102 // SAFETY (all four copies): `copy_h2d_async` requires (a) a valid device
103 // destination, (b) `bytes` readable from `src`, and (c) `src` alive until
104 // the next sync on `stream`.
105 // (a) every `*_ptr` came from `device_symbol()` above, i.e. straight from
106 // `cuModuleGetGlobal_v2` on THIS model's loaded module (the
107 // `self.registry` field exists precisely so a hot-swapped model
108 // cannot resolve against the previous model's copy).
109 // (b) `copy_bytes = min(sq_bytes, size_of_val(&zeros_f32))` is bounded by
110 // BOTH the device symbol's reported length and the 512-byte host
111 // array, so neither side can be over-run. The three scalar copies
112 // move `size_of::<i32>()` out of `&i32` locals — exact by
113 // construction — into `d_innerq_count/_active/_calibrating`, which
114 // are `__device__ int` in tq_plus_innerq.cuh.
115 // (c) `zeros_f32`, `zero_i32` and `one_i32` are stack locals of this fn
116 // and the `stream_synchronize` immediately after the block retires
117 // every copy before they go out of scope.
118 unsafe {
119 reg.copy_h2d_async(
120 sq_ptr,
121 zeros_f32.as_ptr() as *const c_void,
122 copy_bytes,
123 stream,
124 )?;
125 reg.copy_h2d_async(
126 count_ptr,
127 &zero_i32 as *const i32 as *const c_void,
128 std::mem::size_of::<i32>(),
129 stream,
130 )?;
131 reg.copy_h2d_async(
132 active_ptr,
133 &zero_i32 as *const i32 as *const c_void,
134 std::mem::size_of::<i32>(),
135 stream,
136 )?;
137 reg.copy_h2d_async(
138 calib_ptr,
139 &one_i32 as *const i32 as *const c_void,
140 std::mem::size_of::<i32>(),
141 stream,
142 )?;
143 }
144 // Stack locals must live until the copies retire.
145 reg.stream_synchronize(stream)?;
146
147 self.calibrating.store(true, Ordering::Release);
148 self.finalized.store(false, Ordering::Release);
149 tracing::info!(
150 "InnerQ calibration started: target={} tokens, strength={:.2}",
151 self.target_tokens,
152 self.strength,
153 );
154 Ok(())
155 }
156
157 /// Poll `d_innerq_count`. When it crosses `target_tokens`, pull
158 /// `d_innerq_sq_accum`, compute per-channel scale/scale_inv, upload,
159 /// and flip `d_innerq_active = 1`. Returns `Ok(true)` on the call
160 /// that activates, `Ok(false)` on every other call (including
161 /// auto-disable when channels are already balanced).
162 pub fn maybe_finalize(&self, group_size: i32) -> Result<bool> {
163 if self.finalized.load(Ordering::Acquire) {
164 return Ok(false);
165 }
166 let gs = group_size as usize;
167 if gs == 0 || gs > MAX_CHANNELS {
168 bail!("group_size {group_size} out of range (1..={MAX_CHANNELS})");
169 }
170
171 let reg = &self.registry;
172 let stream = reg.raw_stream();
173
174 let (count_ptr, _) = reg.device_symbol(MODULE, SYM_COUNT)?;
175 let mut count: i32 = 0;
176 // SAFETY: `count_ptr` is `d_innerq_count`, a `__device__ int`, resolved from
177 // this model's own module; the transfer is exactly `size_of::<i32>()` bytes
178 // into `&mut count`, a live stack local that outlives the copy — the
179 // `stream_synchronize` on the next line retires the DMA before `count` is
180 // read. `&mut count` is the only reference to it here.
181 unsafe {
182 reg.copy_d2h_async(
183 &mut count as *mut i32 as *mut c_void,
184 count_ptr,
185 std::mem::size_of::<i32>(),
186 stream,
187 )?;
188 }
189 reg.stream_synchronize(stream)?;
190
191 if count < self.target_tokens {
192 return Ok(false);
193 }
194
195 let (sq_ptr, sq_bytes) = reg.device_symbol(MODULE, SYM_SQ_ACCUM)?;
196 let mut sq_accum = [0.0f32; MAX_CHANNELS];
197 let accum_bytes = gs * std::mem::size_of::<f32>();
198 // The host side is bounded by the `gs <= MAX_CHANNELS` check above, but the
199 // DEVICE side is only bounded by `MAX_CHANNELS == INNERQ_MAX_CHANNELS` in
200 // tq_plus_innerq.cuh — a constant in a different language in a different
201 // tree. `device_symbol` hands back the symbol's real length; use it rather
202 // than assume the two constants are still in step.
203 if accum_bytes > sq_bytes {
204 bail!(
205 "{MODULE}::{SYM_SQ_ACCUM} is {sq_bytes} bytes but group_size {group_size} \
206 needs {accum_bytes} — INNERQ_MAX_CHANNELS and MAX_CHANNELS disagree"
207 );
208 }
209 // SAFETY: `copy_d2h_async` needs a valid device source, `bytes` writable at
210 // `dst`, and `dst` alive until the sync. `sq_ptr` is `d_innerq_sq_accum`
211 // from this model's module; `accum_bytes = gs * 4` is `<= sq_bytes` (checked
212 // immediately above) on the device side and `<= MAX_CHANNELS * 4 =
213 // size_of_val(&sq_accum)` on the host side, since `gs <= MAX_CHANNELS` was
214 // enforced at the top of this fn. `sq_accum` is a fully-initialised stack
215 // array that lives until the end of the fn, and the `stream_synchronize`
216 // below retires the copy before it is read.
217 unsafe {
218 reg.copy_d2h_async(
219 sq_accum.as_mut_ptr() as *mut c_void,
220 sq_ptr,
221 accum_bytes,
222 stream,
223 )?;
224 }
225 reg.stream_synchronize(stream)?;
226
227 // Identity-preserving equalization: scale[i] = (mean_rms / rms[i])
228 // ^strength, clamped to [0.5, 2.0]; auto-disable if max/min ratio
229 // < 1.2 either way.
230 let count_f = count as f32;
231 let mut rms = [0.0f32; MAX_CHANNELS];
232 let mut mean_rms = 0.0f32;
233 for i in 0..gs {
234 rms[i] = (sq_accum[i] / count_f).sqrt();
235 mean_rms += rms[i];
236 }
237 mean_rms /= gs as f32;
238
239 let mut scale = [1.0f32; MAX_CHANNELS];
240 let mut scale_inv = [1.0f32; MAX_CHANNELS];
241 let mut max_ratio = 0.0f32;
242 let mut min_ratio = 1e30f32;
243 for i in 0..gs {
244 let ratio = if rms[i] > 1e-10 {
245 mean_rms / rms[i]
246 } else {
247 1.0
248 };
249 let s = ratio.powf(self.strength).clamp(0.5, 2.0);
250 scale[i] = s;
251 scale_inv[i] = 1.0 / s;
252 if ratio > max_ratio {
253 max_ratio = ratio;
254 }
255 if ratio < min_ratio {
256 min_ratio = ratio;
257 }
258 }
259
260 let (calib_ptr, _) = reg.device_symbol(MODULE, SYM_CALIBRATING)?;
261 let zero_i32: i32 = 0;
262 // SAFETY: `calib_ptr` is `d_innerq_calibrating`, a `__device__ int` in this
263 // model's module; the copy is exactly `size_of::<i32>()` bytes out of
264 // `&zero_i32`. `zero_i32` is declared on the line above and stays in scope
265 // to the end of the fn, so it outlives the copy on BOTH exits: the
266 // auto-disable branch syncs before returning, and the normal path syncs
267 // after the scale uploads below.
268 unsafe {
269 reg.copy_h2d_async(
270 calib_ptr,
271 &zero_i32 as *const i32 as *const c_void,
272 std::mem::size_of::<i32>(),
273 stream,
274 )?;
275 }
276
277 if max_ratio < 1.2 && min_ratio > (1.0 / 1.2) {
278 reg.stream_synchronize(stream)?;
279 self.calibrating.store(false, Ordering::Release);
280 self.finalized.store(true, Ordering::Release);
281 tracing::info!(
282 "InnerQ auto-disabled (channels already balanced: max_ratio={max_ratio:.3}, \
283 min_ratio={min_ratio:.3})"
284 );
285 return Ok(false);
286 }
287
288 let (scale_ptr, scale_bytes) = reg.device_symbol(MODULE, SYM_SCALE)?;
289 let (scale_inv_ptr, scale_inv_bytes) = reg.device_symbol(MODULE, SYM_SCALE_INV)?;
290 let (active_ptr, _) = reg.device_symbol(MODULE, SYM_ACTIVE)?;
291 let one_i32: i32 = 1;
292 let copy_bytes = gs * std::mem::size_of::<f32>();
293 // These two are WRITES into device globals — an over-run corrupts whatever
294 // the linker placed after them in the module's global segment, silently.
295 // Bound against the symbols' real lengths rather than against
296 // MAX_CHANNELS's agreement with INNERQ_MAX_CHANNELS.
297 if copy_bytes > scale_bytes || copy_bytes > scale_inv_bytes {
298 bail!(
299 "{MODULE} scale symbols are {scale_bytes}/{scale_inv_bytes} bytes but \
300 group_size {group_size} needs {copy_bytes} — INNERQ_MAX_CHANNELS and \
301 MAX_CHANNELS disagree"
302 );
303 }
304 // SAFETY: `scale_ptr`/`scale_inv_ptr` are `d_innerq_scale` /
305 // `d_innerq_scale_inv` from this model's module. `copy_bytes = gs * 4` is
306 // `<= scale_bytes`/`scale_inv_bytes` on the device side (checked directly
307 // above) and `<= MAX_CHANNELS * 4 = size_of_val(&scale)` on the host side
308 // (`gs <= MAX_CHANNELS`, enforced at the top of this fn). `scale` and
309 // `scale_inv` are `[f32; MAX_CHANNELS]` stack arrays initialised to 1.0 at
310 // declaration and fully written for indices `0..gs` by the loop above; they
311 // stay in scope past the `stream_synchronize` that retires these copies.
312 unsafe {
313 reg.copy_h2d_async(
314 scale_ptr,
315 scale.as_ptr() as *const c_void,
316 copy_bytes,
317 stream,
318 )?;
319 reg.copy_h2d_async(
320 scale_inv_ptr,
321 scale_inv.as_ptr() as *const c_void,
322 copy_bytes,
323 stream,
324 )?;
325 }
326 // Order: scale uploads must retire before active flips so any kernel
327 // observing active=1 sees the finalized scales (cuMemcpyAsync within
328 // the same stream is already strict-order, but the active flag is
329 // visible to kernels on OTHER streams once a host-side sync passes).
330 reg.stream_synchronize(stream)?;
331 // SAFETY: `active_ptr` is `d_innerq_active`, a `__device__ int` in this
332 // model's module; the copy is exactly `size_of::<i32>()` bytes out of
333 // `&one_i32`, a stack local declared above that outlives the
334 // `stream_synchronize` on the line after.
335 unsafe {
336 reg.copy_h2d_async(
337 active_ptr,
338 &one_i32 as *const i32 as *const c_void,
339 std::mem::size_of::<i32>(),
340 stream,
341 )?;
342 }
343 reg.stream_synchronize(stream)?;
344
345 self.calibrating.store(false, Ordering::Release);
346 self.finalized.store(true, Ordering::Release);
347 tracing::info!(
348 "InnerQ scales activated (group_size={group_size}, max_ratio={max_ratio:.3}, \
349 strength={:.2})",
350 self.strength,
351 );
352 Ok(true)
353 }
354}