use crate::Engine;
use crate::hybrid::{HybridModel, Mixer};
use cudarc::driver::{CudaGraph, CudaSlice};
use memra_kv::Cache;
use std::sync::atomic::Ordering;
type Res<T> = Result<T, Box<dyn std::error::Error>>;
struct RunGraph {
lo: usize,
hi: usize,
graphs: [CudaGraph; 2],
phase: usize,
_keeper: Vec<Box<dyn std::any::Any + Send>>,
}
unsafe impl Send for RunGraph {}
unsafe impl Send for StageGraphs {}
struct StageGraphs {
dev: usize,
lo: usize,
hi: usize,
runs: Vec<RunGraph>,
x_io: CudaSlice<f32>,
x_out: CudaSlice<f32>,
ws: crate::hyper::HyperDecodeWs,
f16: Option<crate::f16_ffi::F16Scratch>,
next_pos: usize,
state_sig: Vec<LayerSig>,
launched_since_sync: bool,
}
struct StageBufs {
x_io: CudaSlice<f32>,
x_out: CudaSlice<f32>,
ws: crate::hyper::HyperDecodeWs,
f16: Option<crate::f16_ffi::F16Scratch>,
}
#[derive(Default)]
pub(crate) struct Glm5DecodeGraphs {
stages: Vec<StageGraphs>,
failed: Vec<(usize, usize, usize)>,
}
fn capturable_runs(
lo: usize,
hi: usize,
capturable: impl Fn(usize) -> bool,
) -> Vec<(usize, usize)> {
let mut runs = Vec::new();
let mut start: Option<usize> = None;
for il in lo..hi {
match (capturable(il), start) {
(true, None) => start = Some(il),
(false, Some(a)) => {
runs.push((a, il));
start = None;
}
_ => {}
}
}
if let Some(a) = start {
runs.push((a, hi));
}
runs
}
fn kda_runs(m: &HybridModel, lo: usize, hi: usize) -> Vec<(usize, usize)> {
capturable_runs(
lo,
hi,
|il| matches!(&m.layers[il].mixer, Mixer::Kda(la) if la.tp.is_none()),
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct LayerSig {
il: usize,
conv: u64,
p_lo: u64,
p_hi: u64,
}
fn recur_sig(e: &Engine, cache: &Cache, il: usize) -> Option<LayerSig> {
use cudarc::driver::DevicePtr;
let rl = cache.recur.get(il)?.as_ref()?;
let st = e.stream();
let (conv, _g0) = rl.conv_state.device_ptr(&st);
let (s, _g1) = rl.ssm_state.device_ptr(&st);
let (a, _g2) = rl.ssm_state_alt.device_ptr(&st);
Some(LayerSig {
il,
conv,
p_lo: s.min(a),
p_hi: s.max(a),
})
}
fn stage_sig(m: &HybridModel, e: &Engine, cache: &Cache, lo: usize, hi: usize) -> Vec<LayerSig> {
kda_runs(m, lo, hi)
.iter()
.flat_map(|(a, b)| *a..*b)
.filter_map(|il| recur_sig(e, cache, il))
.collect()
}
fn sig_diff(old: &[LayerSig], new: &[LayerSig]) -> Option<String> {
if old.len() != new.len() {
return Some(format!(
"layer-count {} -> {} (a captured layer lost or gained its recurrent slot)",
old.len(),
new.len()
));
}
for (o, n) in old.iter().zip(new.iter()) {
if o.il != n.il {
return Some(format!("layer order {} -> {}", o.il, n.il));
}
if o.conv != n.conv {
return Some(format!(
"layer {} conv_state 0x{:x} -> 0x{:x}",
o.il, o.conv, n.conv
));
}
if o.p_lo != n.p_lo || o.p_hi != n.p_hi {
return Some(format!(
"layer {} ssm pair {{0x{:x}, 0x{:x}}} -> {{0x{:x}, 0x{:x}}}",
o.il, o.p_lo, o.p_hi, n.p_lo, n.p_hi
));
}
}
None
}
fn note(msg: &str) {
static SEEN: std::sync::OnceLock<std::sync::Mutex<std::collections::BTreeSet<String>>> =
std::sync::OnceLock::new();
let seen = SEEN.get_or_init(Default::default);
if seen.lock().unwrap().insert(msg.to_string()) {
eprintln!("[glm5-decode-graph] {msg}");
}
}
#[derive(Clone, Copy)]
struct CapCtx {
dev: usize,
lo: usize,
hi: usize,
run: usize,
runs: usize,
a: usize,
b: usize,
phase: usize,
recapture: bool,
}
impl std::fmt::Display for CapCtx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"dev={} stage=[{}, {}) run={}/{} layers=[{}, {}) phase={} recapture={}",
self.dev,
self.lo,
self.hi,
self.run,
self.runs,
self.a,
self.b,
self.phase,
self.recapture
)
}
}
fn capture_status(e: &Engine) -> &'static str {
use cudarc::driver::sys;
let mut st = sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE;
let rc = unsafe { sys::cuStreamIsCapturing(e.stream().cu_stream(), &mut st) };
if rc != sys::CUresult::CUDA_SUCCESS {
return "query-failed";
}
match st {
sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE => "none",
sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_ACTIVE => "ACTIVE",
sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_INVALIDATED => "INVALIDATED",
}
}
fn free_mb(e: &Engine) -> String {
match e.ctx().mem_get_info() {
Ok((free, total)) => format!("{}/{}MB", free >> 20, total >> 20),
Err(_) => "?".to_string(),
}
}
fn capture_error(e: &Engine, ctx: &CapCtx, call: &str, err: &dyn std::fmt::Display) {
eprintln!(
"[glm5-decode-graph] capture-error: call={call} {ctx} stream_capture={} free={} \
ledger={} err={err}",
capture_status(e),
free_mb(e),
crate::glm5_sel_ledger::armed(),
);
}
fn step<T>(
e: &Engine,
ctx: &CapCtx,
call: &str,
r: Result<T, Box<dyn std::error::Error>>,
) -> Res<T> {
match r {
Ok(v) => Ok(v),
Err(err) => {
capture_error(e, ctx, call, &err);
Err(err)
}
}
}
fn capture_one<F>(e: &Engine, ctx: &CapCtx, mut body: F) -> Res<CudaGraph>
where
F: FnMut(&Engine) -> Res<()>,
{
use cudarc::driver::sys::{CUgraphInstantiate_flags, CUstreamCaptureMode};
let pre = capture_status(e);
if pre != "none" {
capture_error(
e,
ctx,
"pre-begin-status",
&format!("stream capture status is {pre}"),
);
return Err(format!("glm5 decode graph: stream capture status {pre} before begin").into());
}
step(
e,
ctx,
"synchronize(before begin_capture)",
e.stream().synchronize().map_err(Into::into),
)?;
let was_tracking = e.ctx().is_event_tracking();
if was_tracking {
unsafe { e.ctx().disable_event_tracking() };
}
let out = (|| -> Res<CudaGraph> {
step(
e,
ctx,
"cuStreamBeginCapture(RELAXED)",
e.stream()
.begin_capture(CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_RELAXED)
.map_err(Into::into),
)?;
let body_res = body(e);
let ended = e
.stream()
.end_capture(CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_AUTO_FREE_ON_LAUNCH);
if let Err(err) = body_res {
capture_error(e, ctx, "capture body", &err);
return Err(err);
}
let graph = step(
e,
ctx,
"cuStreamEndCapture+cuGraphInstantiate",
ended.map_err(Into::into),
)?
.ok_or_else(|| {
capture_error(e, ctx, "cuStreamEndCapture", &"capture produced no graph");
"glm5 decode graph: capture produced no graph (stream was not capturing)"
})?;
step(e, ctx, "cuGraphUpload", graph.upload().map_err(Into::into))?;
Ok(graph)
})();
if was_tracking {
unsafe { e.ctx().enable_event_tracking() };
}
out
}
fn x_sum(e: &Engine, x: &CudaSlice<f32>) -> String {
match e.dtoh(x) {
Ok(v) => {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
let mut nz = 0usize;
let mut absmax = 0f32;
for f in &v {
h ^= f.to_bits() as u64;
h = h.wrapping_mul(0x100_0000_01b3);
if *f != 0.0 {
nz += 1;
}
if f.abs() > absmax {
absmax = f.abs();
}
}
format!("sum=0x{h:016x} nz={nz}/{} absmax={absmax:.6e}", v.len())
}
Err(err) => format!("sum=? ({err})"),
}
}
#[allow(clippy::too_many_arguments)] fn trace_seg(
e: &Engine,
dev: usize,
lo: usize,
hi: usize,
a: usize,
b: usize,
arm: &str,
pos: usize,
x: &CudaSlice<f32>,
) {
if !crate::glm5_graph_trace_on() {
return;
}
eprintln!(
"[glm5-graph-trace] pos={pos} dev={dev} stage=[{lo}, {hi}) seg=[{a}, {b}) arm={arm} {}",
x_sum(e, x)
);
}
impl HybridModel {
fn glm5_decode_graph_refusal(
&self,
e: &Engine,
cache: &Cache,
lo: usize,
hi: usize,
) -> Option<String> {
if self.hyper.is_none() {
return Some("model carries no HyperConnections topology".into());
}
if self.cfg.sigmoid_router().is_none() {
return Some(
"this trunk has no sigmoid router (the device-table MoE arm needs one)".into(),
);
}
if crate::glm5_graph_no_capture() {
return Some(
"MEMRA_GLM5_GRAPH_NO_CAPTURE: capture disabled while the door's device-table MoE \
arm stays engaged (the half of the bisect MEMRA_GLM5_GRAPH_HOST_MOE could not \
supply, since that one turns off both enablers at once)"
.into(),
);
}
if crate::glm5_graph_host_moe() {
return Some(
"MEMRA_GLM5_GRAPH_HOST_MOE forces the host-oracle MoE: its per-layer readback and \
stream drain cannot live inside a capture region (bisect arm)"
.into(),
);
}
if !crate::htod_diet_on() {
return Some(
"MEMRA_HTOD_DIET is off: the shared expert still uploads a pageable constant \
per MoE layer, which a capture region refuses"
.into(),
);
}
if !crate::hybrid_forward::sigmoid_router_enabled() {
return Some(
"MEMRA_SIG_ROUTER=0 selects the host oracle (no device selection to capture)"
.into(),
);
}
if crate::moesd::capture_active() || memra_reference::hidden_trace::enabled() {
return Some("a host-visible route/hidden observer is armed".into());
}
for env in [
"MEMRA_MOE_STATS",
"MEMRA_MOE_TRACE",
"MEMRA_MOE_WEIGHT_TRACE",
"MEMRA_MOE_INPUT_TRACE_DIR",
"MEMRA_SIG_ROUTER_LOGIT_TRACE",
"MEMRA_MOE_SEL_DUMP",
] {
if std::env::var_os(env).is_some() {
return Some(format!(
"{env} is armed (its consumer reads the selection on the host)"
));
}
}
if crate::spill_pread::worker_enabled() && crate::spill_pread::copy_h2d_enabled() {
return Some("the NVMe worker H2D promotion reads the host selection".into());
}
if e.ctx().is_event_tracking() {
return Some(
"cudarc event tracking is on (MEMRA_EVT); capture refuses cross-stream waits"
.into(),
);
}
for il in lo..hi {
if let Mixer::Kda(la) = &self.layers[il].mixer
&& la.tp.is_some()
{
return Some(format!("layer {il} is glm5-TP sharded"));
}
if cache.recur.get(il).is_none() {
return Some(format!(
"layer {il} has no recurrent-state slot in this cache"
));
}
}
let runs = kda_runs(self, lo, hi);
if runs.is_empty() {
return Some(format!("stage [{lo}, {hi}) holds no KDA layer to capture"));
}
for (a, b) in runs {
for il in a..b {
if !HybridModel::glm5_t1_dev_moe_ready(e, &self.layers[il], &self.cfg, il) {
return Some(format!(
"layer {il}: the T=1 device-table MoE arm would not fire (it needs \
slab-local uniform q8 experts, a PRE clamp, and n_used <= 8)"
));
}
}
}
None
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn hyper_range_decode_eager_traced(
&self,
e: &Engine,
topology: &crate::hyper::HyperTopology,
mut x: CudaSlice<f32>,
lo: usize,
hi: usize,
pos_d: &CudaSlice<i32>,
pos: usize,
cache: &mut Cache,
) -> Res<CudaSlice<f32>> {
let dev = e.ctx().ordinal();
let runs = kda_runs(self, lo, hi);
let mut cursor = lo;
for (a, b) in runs {
if a > cursor {
x = self.hyper_range_decode_eager(e, topology, x, cursor, a, pos_d, pos, cache)?;
trace_seg(e, dev, lo, hi, cursor, a, "eager-gap", pos, &x);
}
x = self.hyper_range_decode_eager(e, topology, x, a, b, pos_d, pos, cache)?;
trace_seg(e, dev, lo, hi, a, b, "eager-run", pos, &x);
cursor = b;
}
if cursor < hi {
x = self.hyper_range_decode_eager(e, topology, x, cursor, hi, pos_d, pos, cache)?;
trace_seg(e, dev, lo, hi, cursor, hi, "eager-gap", pos, &x);
}
Ok(x)
}
pub(crate) fn glm5_decode_graph_ready(
&self,
e: &Engine,
cache: &Cache,
lo: usize,
hi: usize,
) -> bool {
if cache
.glm5_decode_graph
.as_ref()
.and_then(|b| b.downcast_ref::<Glm5DecodeGraphs>())
.is_some_and(|p| {
p.failed
.iter()
.any(|&(d, l, h)| d == e.ctx().ordinal() && l == lo && h == hi)
})
{
return false;
}
if let Some(why) = self.glm5_decode_graph_refusal(e, cache, lo, hi) {
note(&format!("eager: {why}"));
return false;
}
if !crate::spec::graph_launch_headroom_ok(e) {
static SUSPENDED: std::sync::Once = std::sync::Once::new();
SUSPENDED.call_once(|| crate::spec::graph_replay_suspended_note("glm5-decode-graph"));
return false;
}
true
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn hyper_range_decode_graphed(
&self,
e: &Engine,
topology: &crate::hyper::HyperTopology,
mut x: CudaSlice<f32>,
lo: usize,
hi: usize,
pos_d: &CudaSlice<i32>,
pos: usize,
cache: &mut Cache,
) -> Res<CudaSlice<f32>> {
let dev = e.ctx().ordinal();
let n_embd = self.cfg.n_embd as usize;
let width = topology.streams * n_embd;
let live_sig = stage_sig(self, e, cache, lo, hi);
let mut reuse: Option<StageBufs> = None;
let mut latch_eager = false;
let recapture_armed = crate::glm5_graph_recapture_on();
let mut need_capture = {
let pool = self.glm5_graph_pool(cache);
match pool
.stages
.iter()
.position(|s| s.dev == dev && s.lo == lo && s.hi == hi)
{
Some(i) => {
let st = &pool.stages[i];
let stale_pos = st.next_pos != pos;
let diff = sig_diff(&st.state_sig, &live_sig);
if stale_pos || diff.is_some() {
eprintln!(
"[glm5-decode-graph] {} dev={dev} stage=[{lo}, {hi}) pos={pos} \
expected_pos={} stale_pos={stale_pos} launched_since_sync={} \
stream_capture={} free={} sig_diff={}",
if recapture_armed {
"re-capture"
} else {
"eager-latch (MEMRA_GLM5_GRAPH_RECAPTURE is off; this stage runs \
eager for the rest of the session and the walk stays \
byte-identical)"
},
st.next_pos,
st.launched_since_sync,
capture_status(e),
free_mb(e),
diff.as_deref().unwrap_or("none"),
);
if recapture_armed {
true
} else {
pool.failed.push((dev, lo, hi));
latch_eager = true;
false
}
} else {
false
}
}
None => true,
}
};
if recapture_armed && !latch_eager && need_capture {
let present = {
let pool = self.glm5_graph_pool(cache);
pool.stages
.iter()
.position(|s| s.dev == dev && s.lo == lo && s.hi == hi)
};
if let Some(i) = present {
if let Err(err) = e.stream().synchronize() {
note(&format!(
"eager from here on stage=[{lo}, {hi}) dev={dev}: the pre-teardown drain \
failed ({err}), so the stale execs are left alone"
));
self.glm5_graph_pool(cache).failed.push((dev, lo, hi));
return self
.hyper_range_decode_eager(e, topology, x, lo, hi, pos_d, pos, cache);
}
let stale = self.glm5_graph_pool(cache).stages.remove(i);
crate::GLM5_DECODE_GRAPH_RECAPTURES.fetch_add(1, Ordering::Relaxed);
let StageGraphs {
runs,
x_io,
x_out,
ws,
f16,
..
} = stale;
reuse = Some(StageBufs {
x_io,
x_out,
ws,
f16,
});
drop(runs);
need_capture = true;
}
}
if latch_eager {
return self.hyper_range_decode_eager(e, topology, x, lo, hi, pos_d, pos, cache);
}
if need_capture
&& let Err(err) =
self.glm5_capture_stage(e, topology, lo, hi, cache, dev, width, pos, reuse)
{
note(&format!(
"eager from here on stage=[{lo}, {hi}) dev={dev}: capture failed ({err})"
));
let _ = e.stream().synchronize();
self.glm5_graph_pool(cache).failed.push((dev, lo, hi));
return self.hyper_range_decode_eager(e, topology, x, lo, hi, pos_d, pos, cache);
}
let runs: Vec<(usize, usize)> = kda_runs(self, lo, hi);
let mut cursor = lo;
for (a, b) in runs {
if a > cursor {
x = self.hyper_range_decode_eager(e, topology, x, cursor, a, pos_d, pos, cache)?;
trace_seg(e, dev, lo, hi, cursor, a, "graph-gap", pos, &x);
}
x = self.glm5_replay_run(e, dev, lo, hi, a, b, x, width, cache)?;
trace_seg(e, dev, lo, hi, a, b, "graph-run", pos, &x);
cursor = b;
}
if cursor < hi {
x = self.hyper_range_decode_eager(e, topology, x, cursor, hi, pos_d, pos, cache)?;
trace_seg(e, dev, lo, hi, cursor, hi, "graph-gap", pos, &x);
}
{
let pool = self.glm5_graph_pool(cache);
if let Some(st) = pool
.stages
.iter_mut()
.find(|s| s.dev == dev && s.lo == lo && s.hi == hi)
{
st.next_pos = pos + 1;
}
}
Ok(x)
}
fn glm5_graph_pool<'a>(&self, cache: &'a mut Cache) -> &'a mut Glm5DecodeGraphs {
if cache
.glm5_decode_graph
.as_ref()
.and_then(|b| b.downcast_ref::<Glm5DecodeGraphs>())
.is_none()
{
cache.glm5_decode_graph = Some(Box::new(Glm5DecodeGraphs::default()));
}
cache
.glm5_decode_graph
.as_mut()
.and_then(|b| b.downcast_mut::<Glm5DecodeGraphs>())
.expect("just installed")
}
#[allow(clippy::too_many_arguments)] fn glm5_capture_stage(
&self,
e: &Engine,
topology: &crate::hyper::HyperTopology,
lo: usize,
hi: usize,
cache: &mut Cache,
dev: usize,
width: usize,
pos: usize,
reuse: Option<StageBufs>,
) -> Res<()> {
let n_embd = self.cfg.n_embd as usize;
let runs = kda_runs(self, lo, hi);
let recapture = reuse.is_some();
let alloc_ctx = CapCtx {
dev,
lo,
hi,
run: 0,
runs: runs.len(),
a: lo,
b: hi,
phase: 0,
recapture,
};
let (x_io, x_out, ws, f16) = match reuse {
Some(b) => (b.x_io, b.x_out, b.ws, b.f16),
None => (
step(e, &alloc_ctx, "alloc(x_io)", e.zeros(width))?,
step(e, &alloc_ctx, "alloc(x_out)", e.zeros(width))?,
step(
e,
&alloc_ctx,
"alloc(HyperDecodeWs)",
crate::hyper::HyperDecodeWs::new(e, topology, n_embd),
)?,
None,
),
};
let f16 = match f16 {
Some(f) => f,
None => step(
e,
&alloc_ctx,
"alloc(F16Scratch)",
crate::f16_ffi::F16Scratch::with_capacity(e, (4 << 20).max(n_embd * 16)),
)?,
};
let mut stage = StageGraphs {
dev,
lo,
hi,
runs: Vec::with_capacity(runs.len()),
x_io,
x_out,
ws,
f16: None,
next_pos: pos,
state_sig: runs
.iter()
.flat_map(|(a, b)| *a..*b)
.filter_map(|il| recur_sig(e, cache, il))
.collect(),
launched_since_sync: false,
};
if crate::glm5_sel_ledger::armed()
&& let Some(moe) = self.cfg.moe.as_ref()
{
for (a, b) in &runs {
for il in *a..*b {
crate::glm5_sel_ledger::prearm(e, il as u16, moe.expert_used_count as usize)?;
}
}
}
let prev_f16 = e.f16_scratch_swap(Some(f16));
let captured = (|| -> Res<()> {
crate::GLM5_GRAPH_CAPTURE_OPEN.store(true, Ordering::Relaxed);
let mut ctx = CapCtx {
dev,
lo,
hi,
run: 0,
runs: runs.len(),
a: 0,
b: 0,
phase: 0,
recapture,
};
let pos_d = step(
e,
&ctx,
"htod_i32(pos_d)",
e.htod_i32(&[pos as i32]).map(Some),
)?
.expect("htod returned a buffer");
for (ri, (a, b)) in runs.iter().enumerate() {
let (a, b) = (*a, *b);
ctx.run = ri;
ctx.a = a;
ctx.b = b;
let mut phase_graphs: Vec<CudaGraph> = Vec::with_capacity(2);
for phase in 0..2 {
ctx.phase = phase;
let x_cell = std::cell::RefCell::new(&mut stage.x_io);
let out_cell = std::cell::RefCell::new(&mut stage.x_out);
let ws_cell = std::cell::RefCell::new(&mut stage.ws);
let cache_cell = std::cell::RefCell::new(&mut *cache);
let g = capture_one(e, &ctx, |e| {
self.hyper_range_decode_ws_body(
e,
topology,
&mut x_cell.borrow_mut(),
a,
b,
&pos_d,
pos,
&mut cache_cell.borrow_mut(),
&mut ws_cell.borrow_mut(),
)?;
let live = x_cell.borrow();
e.copy_into(&mut out_cell.borrow_mut(), 0, &live, width)
})?;
phase_graphs.push(g);
}
let mut it = phase_graphs.into_iter();
let g0 = it.next().expect("phase 0 captured");
let g1 = it.next().expect("phase 1 captured");
let keeper = std::mem::take(&mut *e.glm5_graph_keep().lock().unwrap());
stage.runs.push(RunGraph {
lo: a,
hi: b,
graphs: [g0, g1],
phase: 0,
_keeper: keeper,
});
crate::GLM5_DECODE_GRAPH_LAYERS.fetch_add((b - a) as u64, Ordering::Relaxed);
}
Ok(())
})();
crate::GLM5_GRAPH_CAPTURE_OPEN.store(false, Ordering::Relaxed);
stage.f16 = e.f16_scratch_swap(prev_f16);
captured?;
crate::GLM5_DECODE_GRAPH_CAPTURES.fetch_add(1, Ordering::Relaxed);
let line = format!(
"engaged dev={dev} stage=[{lo}, {hi}) runs={} captured_layers={} recapture={recapture} \
free={} (2 ping-pong phases each; MLA/DSA layers stay eager)",
stage.runs.len(),
stage.runs.iter().map(|r| r.hi - r.lo).sum::<usize>(),
free_mb(e),
);
if recapture {
eprintln!("[glm5-decode-graph] {line}");
} else {
note(&line);
}
self.glm5_graph_pool(cache).stages.push(stage);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn glm5_replay_run(
&self,
e: &Engine,
dev: usize,
lo: usize,
hi: usize,
a: usize,
b: usize,
x: CudaSlice<f32>,
width: usize,
cache: &mut Cache,
) -> Res<CudaSlice<f32>> {
let phase;
{
let pool = self.glm5_graph_pool(cache);
let st = pool
.stages
.iter_mut()
.find(|s| s.dev == dev && s.lo == lo && s.hi == hi)
.ok_or("glm5 decode graph: stage pool vanished between capture and replay")?;
let ri = st
.runs
.iter()
.position(|r| r.lo == a && r.hi == b)
.ok_or_else(|| format!("glm5 decode graph: no captured run [{a}, {b})"))?;
phase = st.runs[ri].phase;
let ctx = CapCtx {
dev,
lo,
hi,
run: ri,
runs: st.runs.len(),
a,
b,
phase,
recapture: false,
};
step(
e,
&ctx,
"memcpy_dtod(x -> x_io)",
e.copy_into(&mut st.x_io, 0, &x, width),
)?;
let prev = e.f16_scratch_swap(st.f16.take());
let launched = st.runs[ri].graphs[phase].launch();
st.f16 = e.f16_scratch_swap(prev);
st.launched_since_sync = true;
step(e, &ctx, "cuGraphLaunch", launched.map_err(Into::into))?;
st.runs[ri].phase ^= 1;
}
for il in a..b {
if let Some(rl) = cache.recur[il].as_mut() {
std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
}
}
let out_ctx = CapCtx {
dev,
lo,
hi,
run: 0,
runs: 0,
a,
b,
phase: 0,
recapture: false,
};
let mut out = step(e, &out_ctx, "alloc(out)", e.uninit(width))?;
{
let pool = self.glm5_graph_pool(cache);
let st = pool
.stages
.iter()
.find(|s| s.dev == dev && s.lo == lo && s.hi == hi)
.expect("checked above");
step(
e,
&out_ctx,
"memcpy_dtod(x_out -> out)",
e.copy_into(&mut out, 0, &st.x_out, width),
)?;
}
crate::GLM5_DECODE_GRAPH_REPLAYS.fetch_add(1, Ordering::Relaxed);
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::capturable_runs;
#[test]
fn runs_split_on_the_uncapturable_layers() {
let cap = |il: usize| il % 4 != 3;
assert_eq!(
capturable_runs(0, 12, cap),
vec![(0, 3), (4, 7), (8, 11)],
"whole-trunk split"
);
assert_eq!(capturable_runs(5, 12, cap), vec![(5, 7), (8, 11)]);
assert_eq!(capturable_runs(0, 3, cap), vec![(0, 3)]);
assert!(capturable_runs(0, 4, |il| il == 4).is_empty());
assert!(capturable_runs(7, 7, |_| true).is_empty());
}
#[test]
fn every_run_length_makes_an_even_number_of_swaps() {
for (a, b) in capturable_runs(0, 12, |il| il % 4 != 3) {
assert_eq!(2 * (b - a) % 2, 0, "run [{a}, {b}) swaps must be even");
}
}
}