use model_io::ArchConfig;
struct Snapshot {
position: usize,
resid: Vec<f32>,
}
pub(crate) struct ResidCapture {
path: std::path::PathBuf,
pub(crate) capture: gpu::MetalBuffer,
layers: usize,
hidden: usize,
armed: bool,
snapshots: Vec<Snapshot>,
}
impl ResidCapture {
pub(crate) fn from_env(context: &gpu::MetalContext, arch: &ArchConfig) -> Option<Self> {
let path = std::env::var_os("TURBOSPARK_RESID_CAPTURE")?;
if !crate::steering::family_dispatches_steering(arch.family) {
eprintln!(
"[resid-capture] TURBOSPARK_RESID_CAPTURE is not wired for family {:?}: its \
flow does not feed the capture, ignoring",
arch.family
);
return None;
}
let layers = arch.num_layers as usize;
let hidden = arch.hidden_size as usize;
let capture = context.new_output_buffer((layers * hidden * 2) as u64);
Some(Self {
path: path.into(),
capture,
layers,
hidden,
armed: true,
snapshots: Vec::new(),
})
}
pub(crate) fn layer_offset(&self, layer: usize) -> u64 {
(layer * self.hidden) as u64 * 2
}
pub(crate) fn hidden(&self) -> usize {
self.hidden
}
pub(crate) fn note_generation_start(&mut self) {
self.armed = true;
}
pub(crate) fn record_pass(&mut self, position: usize, skip_head: bool) {
if skip_head || !self.armed {
return;
}
let n = self.layers * self.hidden;
let halfs = gpu::read_buffer_f16(&self.capture, 0, n);
self.snapshots.push(Snapshot {
position,
resid: halfs.iter().map(|v| v.to_f32()).collect(),
});
self.armed = false;
}
fn data_path(&self) -> std::path::PathBuf {
self.path.with_extension("f32")
}
fn header_json(&self) -> String {
let data = self.data_path();
let data_name = data
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let mut out = String::new();
out.push_str("{\n");
out.push_str(&format!(" \"layers\": {},\n", self.layers));
out.push_str(&format!(" \"hidden\": {},\n", self.hidden));
out.push_str(&format!(" \"snapshots\": {},\n", self.snapshots.len()));
out.push_str(" \"dtype\": \"float32\",\n");
out.push_str(" \"layout\": \"[snapshot][layer][hidden]\",\n");
out.push_str(
" \"hook\": \"residual stream at the OUTPUT of each layer (post-FFN join)\",\n",
);
out.push_str(&format!(" \"data\": \"{data_name}\",\n"));
out.push_str(" \"positions\": [");
for (i, s) in self.snapshots.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&s.position.to_string());
}
out.push_str("]\n}\n");
out
}
}
impl Drop for ResidCapture {
fn drop(&mut self) {
if self.snapshots.is_empty() {
eprintln!(
"[resid-capture] no non-prefill pass ran; wrote nothing to {}",
self.path.display()
);
return;
}
let data = self.data_path();
let write_res = (|| -> std::io::Result<()> {
use std::io::Write;
let file = std::fs::File::create(&data)?;
let mut writer = std::io::BufWriter::new(file);
for s in &self.snapshots {
for v in &s.resid {
writer.write_all(&v.to_le_bytes())?;
}
}
writer.flush()
})();
if let Err(e) = write_res {
eprintln!("[resid-capture] FAILED writing {}: {e}", data.display());
return;
}
match std::fs::write(&self.path, self.header_json()) {
Ok(()) => eprintln!(
"[resid-capture] wrote {} ({} snapshot(s)) and {}",
self.path.display(),
self.snapshots.len(),
data.display()
),
Err(e) => eprintln!(
"[resid-capture] FAILED writing {}: {e}",
self.path.display()
),
}
}
}
pub(crate) fn encode_resid_capture(
context: &mut gpu::MetalContext,
pass: &gpu::PassEncoder,
scratch: &crate::real_forward_types::DecodeScratch,
capture: Option<&ResidCapture>,
layer: usize,
hidden: usize,
x_off: u64,
) -> Result<(), crate::real_forward_types::RealForwardError> {
let Some(c) = capture else {
return Ok(());
};
gpu::encode_dflash_copy_rows(
context,
pass,
(&scratch.x, x_off),
(&c.capture, c.layer_offset(layer)),
1,
hidden as u32,
c.hidden() as u32,
)
.map_err(crate::real_forward_types::RealForwardError::Gpu)
}
pub(crate) fn capture_blocker(mtp_open: bool, headless_drafter: bool) -> Option<String> {
if mtp_open && !headless_drafter {
Some(
"residual capture is incompatible with MTP speculative drafting: MTP requires \
headful prefill and its draft steps corrupt residual snapshots"
.to_string(),
)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capture_blocker_refuses_mtp_without_headless_drafter() {
assert!(capture_blocker(true, false).is_some());
assert!(capture_blocker(true, true).is_none());
assert!(capture_blocker(false, false).is_none());
assert!(capture_blocker(false, true).is_none());
}
}