metrale_model_layers/layers/qwen3_attention/innerq_driver.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Host-side driver for InnerQ, the per-channel K equalization of
4//! the WHT-rotated (turbo) KV dtypes.
5//!
6//! `TURBO_INNERQ=N` (N > 0 calibration tokens) turns it on;
7//! `TURBO_INNERQ_STRENGTH` in (0, 1] sets the exponent, 0.5 otherwise. The
8//! state is `__device__` globals in namespace `tq_plus`, defined in
9//! `kernels/gb10/common/tq_plus_innerq_apply.cu`. The driver reaches them
10//! through the registry: `device_symbol` for the address, async copies to read
11//! and write.
12//!
13//! `start` zeroes the accumulators and sets `d_innerq_calibrating = 1`.
14//! `maybe_finalize` reads `d_innerq_count`; once it reaches `target_tokens` it
15//! clears `d_innerq_calibrating`, derives per-channel `scale`/`scale_inv` from
16//! `d_innerq_sq_accum`, and either stops (every channel ratio within 1.2x) or
17//! uploads them and sets `d_innerq_active = 1`.
18//!
19//! Owner: model-layers (attention).
20//! Invariants: the `d_innerq_active = 1` write is issued only after the scale
21//! uploads were synchronised on the driver's stream.
22
23use std::ffi::c_void;
24use std::sync::atomic::{AtomicBool, Ordering};
25
26use anyhow::{Context, Result, bail};
27use std::sync::Arc;
28
29use metrale_gpu_runtime::registry::MetraleRegistry;
30
31// 2026-09-25: Itanium-mangled names of the `tq_plus::d_innerq_*` globals,
32// defined in `kernels/gb10/common/tq_plus_innerq_apply.cu`. The module must be
33// that file's: each `.cu` is its own PTX module, built without `-rdc`, so each
34// module has its own copy of a `__device__` global, and only this module's
35// kernels (`tq_plus_innerq_apply_q`, `tq_plus_innerq_apply_k`) read them.
36const MODULE: &str = "tq_plus_innerq_apply";
37const SYM_SCALE: &str = "_ZN7tq_plus14d_innerq_scaleE";
38const SYM_SCALE_INV: &str = "_ZN7tq_plus18d_innerq_scale_invE";
39const SYM_SQ_ACCUM: &str = "_ZN7tq_plus17d_innerq_sq_accumE";
40const SYM_COUNT: &str = "_ZN7tq_plus14d_innerq_countE";
41const SYM_ACTIVE: &str = "_ZN7tq_plus15d_innerq_activeE";
42const SYM_CALIBRATING: &str = "_ZN7tq_plus20d_innerq_calibratingE";
43
44// 2026-09-25: Equal to `INNERQ_MAX_CHANNELS` in `tq_plus_innerq.cuh`; the
45// copies below also check each symbol's real size.
46const MAX_CHANNELS: usize = 128;
47
48pub struct InnerQDriver {
49 /// 2026-09-25: This model's kernel registry. Every device symbol is
50 /// resolved against it, so a driver writes only its own model's globals.
51 registry: Arc<MetraleRegistry>,
52 pub target_tokens: i32,
53 pub strength: f32,
54 pub calibrating: AtomicBool,
55 pub finalized: AtomicBool,
56}
57
58impl InnerQDriver {
59 /// 2026-09-25: Reads `TURBO_INNERQ` and `TURBO_INNERQ_STRENGTH`. Returns
60 /// `None` if `TURBO_INNERQ` is unset, not an `i32`, or `<= 0`. A strength
61 /// outside (0, 1] or unparsable is 0.5.
62 pub fn from_env(registry: Arc<MetraleRegistry>) -> Option<Self> {
63 let n = std::env::var("TURBO_INNERQ")
64 .ok()
65 .and_then(|v| v.parse::<i32>().ok())
66 .filter(|&n| n > 0)?;
67 let strength: f32 = std::env::var("TURBO_INNERQ_STRENGTH")
68 .ok()
69 .and_then(|v| v.parse().ok())
70 .filter(|&s: &f32| s > 0.0 && s <= 1.0)
71 .unwrap_or(0.5);
72 Some(Self {
73 registry,
74 target_tokens: n,
75 strength,
76 calibrating: AtomicBool::new(false),
77 finalized: AtomicBool::new(false),
78 })
79 }
80
81 /// 2026-09-25: Enters calibration: zeroes `d_innerq_sq_accum`,
82 /// `d_innerq_count` and `d_innerq_active`, sets `d_innerq_calibrating = 1`,
83 /// and resets the host flags. Calling it again gives the same state.
84 pub fn start(&self) -> Result<()> {
85 let reg = &self.registry;
86 let stream = reg.raw_stream();
87
88 let zeros_f32 = [0.0f32; MAX_CHANNELS];
89 let zero_i32: i32 = 0;
90 let one_i32: i32 = 1;
91
92 let (sq_ptr, sq_bytes) = reg
93 .device_symbol(MODULE, SYM_SQ_ACCUM)
94 .with_context(|| format!("resolve {MODULE}::{SYM_SQ_ACCUM}"))?;
95 let (count_ptr, _) = reg.device_symbol(MODULE, SYM_COUNT)?;
96 let (active_ptr, _) = reg.device_symbol(MODULE, SYM_ACTIVE)?;
97 let (calib_ptr, _) = reg.device_symbol(MODULE, SYM_CALIBRATING)?;
98
99 let copy_bytes = sq_bytes.min(std::mem::size_of_val(&zeros_f32));
100 // 2026-09-25: SAFETY (all four copies): `copy_h2d_async` requires (a) a
101 // valid device destination, (b) `bytes` readable from `src`, and (c)
102 // `src` alive until the next sync on `stream`.
103 // (a) every `*_ptr` comes from `device_symbol` on this model's
104 // registry.
105 // (b) `copy_bytes = min(sq_bytes, size_of_val(&zeros_f32))` is bounded
106 // by the symbol's reported length and the 512-byte host array. The
107 // three scalar copies move `size_of::<i32>()` bytes from `i32`
108 // locals into `d_innerq_count/_active/_calibrating`, declared
109 // `int` in tq_plus_innerq.cuh.
110 // (c) the sources are locals of this function, and the
111 // `stream_synchronize` after the block retires every copy before
112 // they go out of scope. If a later copy returns an error, the
113 // function returns before that sync.
114 unsafe {
115 reg.copy_h2d_async(
116 sq_ptr,
117 zeros_f32.as_ptr() as *const c_void,
118 copy_bytes,
119 stream,
120 )?;
121 reg.copy_h2d_async(
122 count_ptr,
123 &zero_i32 as *const i32 as *const c_void,
124 std::mem::size_of::<i32>(),
125 stream,
126 )?;
127 reg.copy_h2d_async(
128 active_ptr,
129 &zero_i32 as *const i32 as *const c_void,
130 std::mem::size_of::<i32>(),
131 stream,
132 )?;
133 reg.copy_h2d_async(
134 calib_ptr,
135 &one_i32 as *const i32 as *const c_void,
136 std::mem::size_of::<i32>(),
137 stream,
138 )?;
139 }
140 // 2026-09-25: The stack sources must live until the copies retire.
141 reg.stream_synchronize(stream)?;
142
143 self.calibrating.store(true, Ordering::Release);
144 self.finalized.store(false, Ordering::Release);
145 tracing::info!(
146 "InnerQ calibration started: target={} tokens, strength={:.2}",
147 self.target_tokens,
148 self.strength,
149 );
150 Ok(())
151 }
152
153 /// 2026-09-25: Polls `d_innerq_count`. Once it reaches `target_tokens`,
154 /// reads `d_innerq_sq_accum`, computes per-channel `scale`/`scale_inv`,
155 /// uploads them and sets `d_innerq_active = 1`. Returns `Ok(true)` on the
156 /// call that activates and `Ok(false)` otherwise, including when the
157 /// channels are already balanced and nothing is uploaded. Errors when
158 /// `group_size` is outside `1..=MAX_CHANNELS` or larger than a symbol.
159 pub fn maybe_finalize(&self, group_size: i32) -> Result<bool> {
160 if self.finalized.load(Ordering::Acquire) {
161 return Ok(false);
162 }
163 let gs = group_size as usize;
164 if gs == 0 || gs > MAX_CHANNELS {
165 bail!("group_size {group_size} out of range (1..={MAX_CHANNELS})");
166 }
167
168 let reg = &self.registry;
169 let stream = reg.raw_stream();
170
171 let (count_ptr, _) = reg.device_symbol(MODULE, SYM_COUNT)?;
172 let mut count: i32 = 0;
173 // 2026-09-25: SAFETY: `count_ptr` is `d_innerq_count`, an `int` in this
174 // model's module; the copy is `size_of::<i32>()` bytes into `count`, a
175 // local that the `stream_synchronize` on the next line keeps alive until
176 // the copy retires. No other reference to `count` exists.
177 unsafe {
178 reg.copy_d2h_async(
179 &mut count as *mut i32 as *mut c_void,
180 count_ptr,
181 std::mem::size_of::<i32>(),
182 stream,
183 )?;
184 }
185 reg.stream_synchronize(stream)?;
186
187 if count < self.target_tokens {
188 return Ok(false);
189 }
190
191 let (sq_ptr, sq_bytes) = reg.device_symbol(MODULE, SYM_SQ_ACCUM)?;
192 let mut sq_accum = [0.0f32; MAX_CHANNELS];
193 let accum_bytes = gs * std::mem::size_of::<f32>();
194 // 2026-09-25: The host side is bounded by the `gs <= MAX_CHANNELS` check
195 // above. The device side is bounded by the symbol's real length from
196 // `device_symbol`, so the check does not depend on `MAX_CHANNELS`
197 // agreeing with `INNERQ_MAX_CHANNELS`.
198 if accum_bytes > sq_bytes {
199 bail!(
200 "{MODULE}::{SYM_SQ_ACCUM} is {sq_bytes} bytes but group_size {group_size} \
201 needs {accum_bytes} — INNERQ_MAX_CHANNELS and MAX_CHANNELS disagree"
202 );
203 }
204 // 2026-09-25: SAFETY: `copy_d2h_async` needs a valid device source,
205 // `bytes` writable at `dst`, and `dst` alive until the sync. `sq_ptr` is
206 // `d_innerq_sq_accum` from this model's module; `accum_bytes = gs * 4` is
207 // `<= sq_bytes` (checked above) and `<= size_of_val(&sq_accum)` because
208 // `gs <= MAX_CHANNELS`. `sq_accum` is an initialised local that lives to
209 // the end of the function, and the `stream_synchronize` below retires the
210 // copy before it is read.
211 unsafe {
212 reg.copy_d2h_async(
213 sq_accum.as_mut_ptr() as *mut c_void,
214 sq_ptr,
215 accum_bytes,
216 stream,
217 )?;
218 }
219 reg.stream_synchronize(stream)?;
220
221 // 2026-09-25: `scale[i] = (mean_rms / rms[i]) ^ strength`, clamped to
222 // [0.5, 2.0] (ratio 1 for a channel with rms <= 1e-10). Nothing is
223 // uploaded when every ratio is inside (1/1.2, 1.2).
224 let count_f = count as f32;
225 let mut rms = [0.0f32; MAX_CHANNELS];
226 let mut mean_rms = 0.0f32;
227 for i in 0..gs {
228 rms[i] = (sq_accum[i] / count_f).sqrt();
229 mean_rms += rms[i];
230 }
231 mean_rms /= gs as f32;
232
233 let mut scale = [1.0f32; MAX_CHANNELS];
234 let mut scale_inv = [1.0f32; MAX_CHANNELS];
235 let mut max_ratio = 0.0f32;
236 let mut min_ratio = 1e30f32;
237 for i in 0..gs {
238 let ratio = if rms[i] > 1e-10 {
239 mean_rms / rms[i]
240 } else {
241 1.0
242 };
243 let s = ratio.powf(self.strength).clamp(0.5, 2.0);
244 scale[i] = s;
245 scale_inv[i] = 1.0 / s;
246 if ratio > max_ratio {
247 max_ratio = ratio;
248 }
249 if ratio < min_ratio {
250 min_ratio = ratio;
251 }
252 }
253
254 let (calib_ptr, _) = reg.device_symbol(MODULE, SYM_CALIBRATING)?;
255 let zero_i32: i32 = 0;
256 // 2026-09-25: SAFETY: `calib_ptr` is `d_innerq_calibrating`, an `int` in
257 // this model's module; the copy is `size_of::<i32>()` bytes from
258 // `zero_i32`, a local that stays in scope to the end of the function. The
259 // auto-disable return and the success path both synchronise the stream
260 // first. The error returns between here and the first sync below (a
261 // failed `device_symbol`, the size `bail!`, a failed upload) do not.
262 unsafe {
263 reg.copy_h2d_async(
264 calib_ptr,
265 &zero_i32 as *const i32 as *const c_void,
266 std::mem::size_of::<i32>(),
267 stream,
268 )?;
269 }
270
271 if max_ratio < 1.2 && min_ratio > (1.0 / 1.2) {
272 reg.stream_synchronize(stream)?;
273 self.calibrating.store(false, Ordering::Release);
274 self.finalized.store(true, Ordering::Release);
275 tracing::info!(
276 "InnerQ auto-disabled (channels already balanced: max_ratio={max_ratio:.3}, \
277 min_ratio={min_ratio:.3})"
278 );
279 return Ok(false);
280 }
281
282 let (scale_ptr, scale_bytes) = reg.device_symbol(MODULE, SYM_SCALE)?;
283 let (scale_inv_ptr, scale_inv_bytes) = reg.device_symbol(MODULE, SYM_SCALE_INV)?;
284 let (active_ptr, _) = reg.device_symbol(MODULE, SYM_ACTIVE)?;
285 let one_i32: i32 = 1;
286 let copy_bytes = gs * std::mem::size_of::<f32>();
287 // 2026-09-25: These two are writes into device globals, where an overrun
288 // would silently overwrite the next global. They are bounded by the
289 // symbols' real lengths, not by `MAX_CHANNELS`.
290 if copy_bytes > scale_bytes || copy_bytes > scale_inv_bytes {
291 bail!(
292 "{MODULE} scale symbols are {scale_bytes}/{scale_inv_bytes} bytes but \
293 group_size {group_size} needs {copy_bytes} — INNERQ_MAX_CHANNELS and \
294 MAX_CHANNELS disagree"
295 );
296 }
297 // 2026-09-25: SAFETY: `scale_ptr`/`scale_inv_ptr` are `d_innerq_scale` /
298 // `d_innerq_scale_inv` from this model's module. `copy_bytes = gs * 4` is
299 // `<= scale_bytes`/`scale_inv_bytes` (checked above) and
300 // `<= size_of_val(&scale)` because `gs <= MAX_CHANNELS`. `scale` and
301 // `scale_inv` are initialised locals that stay in scope past the
302 // `stream_synchronize` that retires these copies. If the second copy
303 // returns an error, the function returns before that sync.
304 unsafe {
305 reg.copy_h2d_async(
306 scale_ptr,
307 scale.as_ptr() as *const c_void,
308 copy_bytes,
309 stream,
310 )?;
311 reg.copy_h2d_async(
312 scale_inv_ptr,
313 scale_inv.as_ptr() as *const c_void,
314 copy_bytes,
315 stream,
316 )?;
317 }
318 // 2026-09-25: The scale uploads are synchronised before the
319 // `d_innerq_active` write is issued.
320 reg.stream_synchronize(stream)?;
321 // 2026-09-25: SAFETY: `active_ptr` is `d_innerq_active`, an `int` in this
322 // model's module; the copy is `size_of::<i32>()` bytes from `one_i32`, a
323 // local that outlives the `stream_synchronize` after the block.
324 unsafe {
325 reg.copy_h2d_async(
326 active_ptr,
327 &one_i32 as *const i32 as *const c_void,
328 std::mem::size_of::<i32>(),
329 stream,
330 )?;
331 }
332 reg.stream_synchronize(stream)?;
333
334 self.calibrating.store(false, Ordering::Release);
335 self.finalized.store(true, Ordering::Release);
336 tracing::info!(
337 "InnerQ scales activated (group_size={group_size}, max_ratio={max_ratio:.3}, \
338 strength={:.2})",
339 self.strength,
340 );
341 Ok(true)
342 }
343}