use super::blas::{TypedPtr, gpu_gemm_forward_dispatch, gpu_gemm_typed_forward_raw};
use super::buffers::{DtypedBuf, GpuBuffer};
use super::context::{GemmMode, GemmRole, GpuCtx};
use super::device::GpuDevice;
use super::dtype::WeightDtype;
use super::forward::GpuMambaDims;
use super::gemm_bi_inference::prepare_inference_arch_rung;
use super::graph_capture::{
capture_into_graph_with_gemm_plan, require_deterministic_gemm_graph_plan,
with_validated_gemm_graph_launch,
};
use super::kernel_identity::{CapturedGemmGraphPlan, PreparedGemmCaptureManifest};
use super::launch::{grid_1d, grid_norm};
use super::weights::{
GpuMambaMixedWeights, GpuMambaWeights, MambaLayerWeightsView, MambaWeightsView,
};
use crate::config::MambaConfig;
use crate::weights::MambaWeights;
use cudarc::driver::PushKernelArg;
use std::cell::Cell;
use std::sync::Arc;
#[cfg(test)]
mod model_gemm_manifest_tests {
use super::super::blas::vendor_gemm_test::Guard;
use super::super::context::BiGemmFamily;
use super::super::graph_capture::model_gemm_guard_tests::{
assert_inventory, assert_plan_mutations, configure,
};
use super::*;
use crate::config::ScanMode;
fn config() -> MambaConfig {
MambaConfig {
d_model: 32,
n_layers: 2,
d_state: 8,
d_conv: 4,
expand: 2,
scan_mode: ScanMode::Sequential,
rms_norm_eps: 1e-5,
}
}
fn weights(cfg: &MambaConfig, input_dim: usize, identity: bool) -> MambaWeights {
let mut weights = MambaWeights::init(cfg, input_dim, 0x9051);
if identity {
weights.input_proj_w.clear();
weights.input_proj_b.clear();
}
weights
}
fn projections(
cfg: &MambaConfig,
batch: usize,
input: Option<usize>,
) -> Vec<(usize, usize, usize)> {
let mut expected = Vec::new();
if let Some(input) = input {
expected.push((batch, input, cfg.d_model));
}
let layer = [
(batch, cfg.d_model, 2 * cfg.d_inner()),
(batch, cfg.d_inner(), cfg.xdbl_dim()),
(batch, cfg.dt_rank(), cfg.d_inner()),
(batch, cfg.d_inner(), cfg.d_model),
];
for _ in 0..cfg.n_layers {
expected.extend(layer);
}
expected
}
fn bits(output: &[f32]) -> Vec<u32> {
assert!(output.iter().all(|x| x.is_finite()));
output.iter().map(|x| x.to_bits()).collect()
}
#[test]
#[ignore = "needs a CUDA device"]
fn m1_failed_steps_clear_permits_and_mixed_graphs_reject_wrong_path() {
let device = GpuDevice::new(0).unwrap();
let cfg = config();
let weights = weights(&cfg, cfg.d_model, true);
let input = vec![0.01; cfg.d_model];
let mut output = vec![0.0; cfg.d_model];
let mut f32 = GpuMambaInference::new(&device, &weights, cfg, cfg.d_model, 1).unwrap();
configure(&f32.ctx, BiGemmFamily::Inference, true);
let mut state = f32.alloc_state().unwrap();
let mut scratch = f32.alloc_scratch().unwrap();
assert!(
unsafe { f32.capture_graph(&mut state, &mut scratch) }
.unwrap_err()
.contains("eager")
);
for gpu_only in [false, true] {
f32.step(&input, &mut output, &mut state, &mut scratch)
.unwrap();
assert!(f32.eager_gemm_manifest.get().is_some());
let failed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if gpu_only {
f32.step_gpu_only(&[], &mut state, &mut scratch)
} else {
f32.step(&[], &mut output, &mut state, &mut scratch)
}
}));
assert!(failed.is_err());
assert!(f32.eager_gemm_manifest.get().is_none());
assert!(
unsafe { f32.capture_graph(&mut state, &mut scratch) }
.unwrap_err()
.contains("eager")
);
}
f32.step(&input, &mut output, &mut state, &mut scratch)
.unwrap();
let mut cold_scratch = f32.alloc_scratch().unwrap();
assert!(unsafe { f32.capture_graph(&mut state, &mut cold_scratch) }.is_err());
assert!(!f32.has_graph());
assert!(f32.eager_gemm_manifest.get().is_none());
drop(f32);
for path in [MixedGraphPath::Legacy, MixedGraphPath::Native] {
for gpu_only in [false, true] {
for invalid_upload in [false, true] {
eprintln!(
"installed-opposite graph: attempted={path:?} gpu_only={gpu_only} invalid_upload={invalid_upload}"
);
let mut engine = GpuMambaInferenceMixed::new(
&device,
&weights,
cfg,
cfg.d_model,
1,
WeightDtype::Bf16,
)
.unwrap();
configure(&engine.engine.ctx, BiGemmFamily::Inference, true);
let mut state = engine.alloc_state().unwrap();
let mut legacy = engine.alloc_scratch().unwrap();
let mut native = engine.alloc_mixed_scratch().unwrap();
engine
.step(&input, &mut output, &mut state, &mut legacy)
.unwrap();
engine
.step_mixed_native(&input, &mut output, &mut state, &mut native)
.unwrap();
let installed_path = if path == MixedGraphPath::Legacy {
unsafe { engine.capture_graph_mixed_native(&mut state, &mut native) }
.unwrap();
assert!(engine.eager_legacy_gemm_manifest.get().is_some());
MixedGraphPath::Native
} else {
unsafe { engine.capture_graph(&mut state, &mut legacy) }.unwrap();
assert!(engine.eager_mixed_native_gemm_manifest.get().is_some());
MixedGraphPath::Legacy
};
let installed_routes = engine
.captured_gemm_plan
.as_ref()
.unwrap()
.routes()
.to_vec();
let installed_scratch = (
engine.captured_state_ptr,
engine.captured_scratch_ptr,
engine.captured_half_staging_ptr,
engine.captured_bi_upcast_ptrs,
);
let attempted_input = if invalid_upload {
&[][..]
} else {
input.as_slice()
};
let failed =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
match (path, gpu_only) {
(MixedGraphPath::Legacy, false) => engine.step(
attempted_input,
&mut output,
&mut state,
&mut legacy,
),
(MixedGraphPath::Legacy, true) => {
engine.step_gpu_only(attempted_input, &mut state, &mut legacy)
}
(MixedGraphPath::Native, false) => engine.step_mixed_native(
attempted_input,
&mut output,
&mut state,
&mut native,
),
(MixedGraphPath::Native, true) => engine
.step_gpu_only_mixed_native(
attempted_input,
&mut state,
&mut native,
),
}
}));
if invalid_upload {
assert!(failed.is_err(), "invalid upload must fail before replay");
} else {
assert!(failed.unwrap().unwrap_err().contains("captured path"));
}
let recapture = if path == MixedGraphPath::Legacy {
assert!(
engine.eager_legacy_gemm_manifest.get().is_none(),
"failed legacy attempt retained its eager permit with native graph installed"
);
unsafe { engine.capture_graph(&mut state, &mut legacy) }
} else {
assert!(
engine.eager_mixed_native_gemm_manifest.get().is_none(),
"failed native attempt retained its eager permit with legacy graph installed"
);
unsafe { engine.capture_graph_mixed_native(&mut state, &mut native) }
};
assert!(recapture.unwrap_err().contains("eager"));
assert!(engine.has_graph());
assert_eq!(engine.captured_path, Some(installed_path));
assert_eq!(
engine.captured_gemm_plan.as_ref().unwrap().routes(),
installed_routes
);
assert_eq!(
(
engine.captured_state_ptr,
engine.captured_scratch_ptr,
engine.captured_half_staging_ptr,
engine.captured_bi_upcast_ptrs
),
installed_scratch
);
if installed_path == MixedGraphPath::Native {
engine
.step_gpu_only_mixed_native(&input, &mut state, &mut native)
.unwrap();
} else {
engine
.step_gpu_only(&input, &mut state, &mut legacy)
.unwrap();
}
engine.engine.ctx.stream.synchronize().unwrap();
}
}
}
let mut engine =
GpuMambaInferenceMixed::new(&device, &weights, cfg, cfg.d_model, 1, WeightDtype::Bf16)
.unwrap();
configure(&engine.engine.ctx, BiGemmFamily::Inference, true);
let mut state = engine.alloc_state().unwrap();
let mut legacy = engine.alloc_scratch().unwrap();
let mut native = engine.alloc_mixed_scratch().unwrap();
for path in [MixedGraphPath::Legacy, MixedGraphPath::Native] {
for gpu_only in [false, true] {
engine
.step(&input, &mut output, &mut state, &mut legacy)
.unwrap();
engine
.step_mixed_native(&input, &mut output, &mut state, &mut native)
.unwrap();
let failed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
match (path, gpu_only) {
(MixedGraphPath::Legacy, false) => {
engine.step(&[], &mut output, &mut state, &mut legacy)
}
(MixedGraphPath::Legacy, true) => {
engine.step_gpu_only(&[], &mut state, &mut legacy)
}
(MixedGraphPath::Native, false) => {
engine.step_mixed_native(&[], &mut output, &mut state, &mut native)
}
(MixedGraphPath::Native, true) => {
engine.step_gpu_only_mixed_native(&[], &mut state, &mut native)
}
}
}));
assert!(failed.is_err());
if path == MixedGraphPath::Legacy {
assert!(engine.eager_legacy_gemm_manifest.get().is_none());
assert!(engine.eager_mixed_native_gemm_manifest.get().is_some());
assert!(
unsafe { engine.capture_graph(&mut state, &mut legacy) }
.unwrap_err()
.contains("eager")
);
} else {
assert!(engine.eager_mixed_native_gemm_manifest.get().is_none());
assert!(engine.eager_legacy_gemm_manifest.get().is_some());
assert!(
unsafe { engine.capture_graph_mixed_native(&mut state, &mut native) }
.unwrap_err()
.contains("eager")
);
}
}
}
engine
.step(&input, &mut output, &mut state, &mut legacy)
.unwrap();
engine
.step_mixed_native(&input, &mut output, &mut state, &mut native)
.unwrap();
unsafe { engine.capture_graph_mixed_native(&mut state, &mut native) }.unwrap();
unsafe { engine.capture_graph(&mut state, &mut legacy) }.unwrap();
assert_eq!(engine.captured_path, Some(MixedGraphPath::Legacy));
assert!(
engine
.step_mixed_native(&input, &mut output, &mut state, &mut native)
.unwrap_err()
.contains("captured path")
);
assert!(
engine
.step_gpu_only_mixed_native(&input, &mut state, &mut native)
.unwrap_err()
.contains("captured path")
);
let ctx = &engine.engine.ctx;
assert!(
ctx.ensure_half_staging(usize::MAX)
.unwrap_err()
.contains("cannot grow")
);
let original = engine.captured_half_staging_ptr;
engine.captured_half_staging_ptr ^= 16;
assert!(
engine
.step_gpu_only(&input, &mut state, &mut legacy)
.unwrap_err()
.contains("staging scratch changed")
);
engine.captured_half_staging_ptr = original;
engine
.step_gpu_only(&input, &mut state, &mut legacy)
.unwrap();
let calls = Cell::new(0);
engine.engine.ctx.poison_gemm_for_test();
assert!(
with_validated_gemm_graph_launch(
&engine.engine.ctx,
true,
engine.captured_gemm_plan.as_ref(),
"poison",
|| {
calls.set(calls.get() + 1);
Ok(())
}
)
.unwrap_err()
.contains("unusable")
);
assert_eq!(calls.get(), 0);
drop(engine);
}
#[test]
#[ignore = "needs a CUDA device"]
fn m1_model_manifests_replay_all_paths_without_vendor_gemm() {
let device = GpuDevice::new(0).unwrap();
let cfg = config();
let deny = Guard::new(true).unwrap();
for (family, tc) in [
(BiGemmFamily::Inference, true),
(BiGemmFamily::Triad, false),
(BiGemmFamily::Triad, true),
] {
for batch in [1, 3] {
eprintln!("M1 F32 {family:?} tc={tc} B{batch} nonidentity");
let input = vec![0.01; batch * 24];
let mut output = vec![0.0; batch * cfg.d_model];
let mut engine =
GpuMambaInference::new(&device, &weights(&cfg, 24, false), cfg, 24, batch)
.unwrap();
configure(&engine.ctx, family, tc);
let mut state = engine.alloc_state().unwrap();
let mut scratch = engine.alloc_scratch().unwrap();
engine
.step(&input, &mut output, &mut state, &mut scratch)
.unwrap();
let trace = engine
.ctx
.record_eager_gemm_trace(|| engine.step_kernels(&mut state, &mut scratch))
.unwrap();
state.reset(&engine.ctx.stream).unwrap();
engine
.step_gpu_only(&input, &mut state, &mut scratch)
.unwrap();
scratch
.temporal
.download(&engine.ctx.stream, &mut output)
.unwrap();
let expected_bits = bits(&output);
let manifest = engine.eager_gemm_manifest.get().unwrap();
unsafe { engine.capture_graph(&mut state, &mut scratch) }.unwrap();
assert!(engine.eager_gemm_manifest.get().is_none());
assert_inventory(
&engine.ctx,
&trace,
manifest,
engine.captured_gemm_plan.as_ref().unwrap(),
&projections(&cfg, batch, Some(24)),
);
if family == BiGemmFamily::Inference && batch == 1 {
assert_plan_mutations(&engine.ctx, engine.captured_gemm_plan.as_ref().unwrap());
}
for gpu_only in [false, true] {
state.reset(&engine.ctx.stream).unwrap();
if gpu_only {
engine
.step_gpu_only(&input, &mut state, &mut scratch)
.unwrap();
scratch
.temporal
.download(&engine.ctx.stream, &mut output)
.unwrap();
} else {
engine
.step(&input, &mut output, &mut state, &mut scratch)
.unwrap();
}
assert_eq!(bits(&output), expected_bits);
}
drop(engine);
for dtype in [WeightDtype::Bf16, WeightDtype::F16] {
for path in [MixedGraphPath::Legacy, MixedGraphPath::Native] {
eprintln!("M1 {dtype:?} {path:?} {family:?} tc={tc} B{batch}");
let native = path == MixedGraphPath::Native;
let input_dim = if native { cfg.d_model } else { 24 };
let input = vec![0.01; batch * input_dim];
let mut engine = GpuMambaInferenceMixed::new(
&device,
&weights(&cfg, input_dim, native),
cfg,
input_dim,
batch,
dtype,
)
.unwrap();
configure(&engine.engine.ctx, family, tc);
let mut state = engine.alloc_state().unwrap();
let mut legacy_scratch = engine.alloc_scratch().unwrap();
let mut native_scratch = engine.alloc_mixed_scratch().unwrap();
let ctx = &engine.engine.ctx;
let trace;
let manifest = if native {
engine
.step_mixed_native(
&input,
&mut output,
&mut state,
&mut native_scratch,
)
.unwrap();
trace = ctx
.record_eager_gemm_trace(|| {
engine
.step_kernels_mixed_native(&mut state, &mut native_scratch)
})
.unwrap();
state.reset(&ctx.stream).unwrap();
engine
.step_gpu_only_mixed_native(&input, &mut state, &mut native_scratch)
.unwrap();
native_scratch
.temporal
.download_f32(&ctx.stream, &mut output)
.unwrap();
engine.eager_mixed_native_gemm_manifest.get().unwrap()
} else {
engine
.step(&input, &mut output, &mut state, &mut legacy_scratch)
.unwrap();
trace = ctx
.record_eager_gemm_trace(|| {
engine.step_kernels_mixed(&mut state, &mut legacy_scratch)
})
.unwrap();
state.reset(&ctx.stream).unwrap();
engine
.step_gpu_only(&input, &mut state, &mut legacy_scratch)
.unwrap();
legacy_scratch
.temporal
.download(&ctx.stream, &mut output)
.unwrap();
engine.eager_legacy_gemm_manifest.get().unwrap()
};
let expected_bits = bits(&output);
if native {
unsafe {
engine.capture_graph_mixed_native(&mut state, &mut native_scratch)
}
.unwrap();
} else {
unsafe { engine.capture_graph(&mut state, &mut legacy_scratch) }
.unwrap();
}
let ctx = &engine.engine.ctx;
assert_eq!(engine.captured_path, Some(path));
assert_inventory(
ctx,
&trace,
manifest,
engine.captured_gemm_plan.as_ref().unwrap(),
&projections(&cfg, batch, if native { None } else { Some(input_dim) }),
);
if family == BiGemmFamily::Triad && !tc {
assert!(trace.routes().iter().all(|r| r.symbol.contains("matvec")));
}
for gpu_only in [false, true] {
state.reset(&ctx.stream).unwrap();
match (native, gpu_only) {
(true, true) => {
engine
.step_gpu_only_mixed_native(
&input,
&mut state,
&mut native_scratch,
)
.unwrap();
native_scratch
.temporal
.download_f32(&ctx.stream, &mut output)
.unwrap();
}
(true, false) => engine
.step_mixed_native(
&input,
&mut output,
&mut state,
&mut native_scratch,
)
.unwrap(),
(false, true) => {
engine
.step_gpu_only(&input, &mut state, &mut legacy_scratch)
.unwrap();
legacy_scratch
.temporal
.download(&ctx.stream, &mut output)
.unwrap();
}
(false, false) => engine
.step(&input, &mut output, &mut state, &mut legacy_scratch)
.unwrap(),
}
assert_eq!(bits(&output), expected_bits);
}
drop(engine);
}
}
}
}
assert_eq!(deny.calls(), 0);
}
}
pub struct GpuInferenceState {
pub conv: GpuBuffer,
pub ssm: GpuBuffer,
batch: usize,
d_inner: usize,
d_conv: usize,
d_state: usize,
}
impl GpuInferenceState {
pub fn zeros(
stream: &Arc<cudarc::driver::CudaStream>,
batch: usize,
cfg: &MambaConfig,
) -> Result<Self, String> {
let di = cfg.d_inner();
let conv_len = cfg.n_layers * batch * di * cfg.d_conv;
let ssm_len = cfg.n_layers * batch * di * cfg.d_state;
Ok(Self {
conv: GpuBuffer::zeros(stream, conv_len)?,
ssm: GpuBuffer::zeros(stream, ssm_len)?,
batch,
d_inner: di,
d_conv: cfg.d_conv,
d_state: cfg.d_state,
})
}
pub fn reset(&mut self, stream: &Arc<cudarc::driver::CudaStream>) -> Result<(), String> {
self.conv.zero(stream)?;
self.ssm.zero(stream)
}
pub fn conv_offset(&self, layer: usize) -> usize {
layer * self.batch * self.d_inner * self.d_conv
}
pub fn ssm_offset(&self, layer: usize) -> usize {
layer * self.batch * self.d_inner * self.d_state
}
pub fn batch(&self) -> usize {
self.batch
}
}
pub struct GpuInferenceScratch {
pub gpu_input: GpuBuffer,
pub temporal: GpuBuffer,
pub residual: GpuBuffer,
pub proj: GpuBuffer,
pub u: GpuBuffer,
pub xdbl: GpuBuffer,
pub dt_gather: GpuBuffer,
pub delta: GpuBuffer,
pub y: GpuBuffer,
pub rms_buf: GpuBuffer,
}
impl GpuInferenceScratch {
pub fn new(
stream: &Arc<cudarc::driver::CudaStream>,
batch: usize,
cfg: &MambaConfig,
input_dim: usize,
) -> Result<Self, String> {
let dm = cfg.d_model;
let di = cfg.d_inner();
let dt_rank = cfg.dt_rank();
let xdbl_dim = cfg.xdbl_dim();
Ok(Self {
gpu_input: GpuBuffer::zeros(stream, batch * input_dim)?,
temporal: GpuBuffer::zeros(stream, batch * dm)?,
residual: GpuBuffer::zeros(stream, batch * dm)?,
proj: GpuBuffer::zeros(stream, batch * 2 * di)?,
u: GpuBuffer::zeros(stream, batch * di)?,
xdbl: GpuBuffer::zeros(stream, batch * xdbl_dim)?,
dt_gather: GpuBuffer::zeros(stream, batch * dt_rank)?,
delta: GpuBuffer::zeros(stream, batch * di)?,
y: GpuBuffer::zeros(stream, batch * di)?,
rms_buf: GpuBuffer::zeros(stream, batch)?,
})
}
}
pub struct GpuInferenceMixedScratch {
pub gpu_input: GpuBuffer,
pub temporal: DtypedBuf,
pub residual: GpuBuffer,
pub proj: DtypedBuf,
pub u: DtypedBuf,
pub xdbl: DtypedBuf,
pub dt_gather: DtypedBuf,
pub delta: DtypedBuf,
pub y: DtypedBuf,
pub rms_buf: GpuBuffer,
pub dtype: WeightDtype,
}
impl GpuInferenceMixedScratch {
pub fn new(
stream: &Arc<cudarc::driver::CudaStream>,
batch: usize,
cfg: &MambaConfig,
input_dim: usize,
dtype: WeightDtype,
) -> Result<Self, String> {
if matches!(dtype, WeightDtype::F32) {
return Err("GpuInferenceMixedScratch requires bf16 or f16 dtype (use \
GpuInferenceScratch for f32)"
.to_string());
}
let dm = cfg.d_model;
let di = cfg.d_inner();
let dt_rank = cfg.dt_rank();
let xdbl_dim = cfg.xdbl_dim();
Ok(Self {
gpu_input: GpuBuffer::zeros(stream, batch * input_dim)?,
temporal: DtypedBuf::zeros(stream, batch * dm, dtype)?,
residual: GpuBuffer::zeros(stream, batch * dm)?,
proj: DtypedBuf::zeros(stream, batch * 2 * di, dtype)?,
u: DtypedBuf::zeros(stream, batch * di, dtype)?,
xdbl: DtypedBuf::zeros(stream, batch * xdbl_dim, dtype)?,
dt_gather: DtypedBuf::zeros(stream, batch * dt_rank, dtype)?,
delta: DtypedBuf::zeros(stream, batch * di, dtype)?,
y: DtypedBuf::zeros(stream, batch * di, dtype)?,
rms_buf: GpuBuffer::zeros(stream, batch)?,
dtype,
})
}
}
pub struct GpuMambaInference {
pub(super) ctx: GpuCtx,
pub(super) weights: GpuMambaWeights,
pub(super) a_neg_all: GpuBuffer,
pub(super) cfg: MambaConfig,
pub(super) input_dim: usize,
pub(super) batch: usize,
pub(super) identity_proj: bool,
graph: Option<cudarc::driver::CudaGraph>,
captured_gemm_route: Option<crate::mamba_ssm::gpu::context::GemmRoute>,
captured_gemm_plan: Option<CapturedGemmGraphPlan>,
eager_gemm_manifest: Cell<Option<PreparedGemmCaptureManifest>>,
captured_state_ptr: u64,
captured_scratch_ptr: u64,
}
impl Drop for GpuMambaInference {
fn drop(&mut self) {
let _ = self.ctx.stream.synchronize();
drop(self.graph.take());
}
}
impl GpuMambaInference {
fn dims(&self, seq_len: usize) -> GpuMambaDims {
GpuMambaDims {
batch: self.batch,
seq_len,
n_layers: self.cfg.n_layers,
d_model: self.cfg.d_model,
d_inner: self.cfg.d_inner(),
d_state: self.cfg.d_state,
d_conv: self.cfg.d_conv,
dt_rank: self.cfg.dt_rank(),
xdbl_dim: self.cfg.xdbl_dim(),
mamba_input_dim: self.input_dim,
scan_mode: self.cfg.scan_mode,
rms_norm_eps: self.cfg.rms_norm_eps,
}
}
pub fn new(
device: &GpuDevice,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
) -> Result<Self, String> {
Self::new_inner(
device,
cpu_weights,
cfg,
input_dim,
batch,
None,
WeightDtype::F32,
)
}
pub fn new_with_mode(
device: &GpuDevice,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
mode: GemmMode,
) -> Result<Self, String> {
Self::new_inner(
device,
cpu_weights,
cfg,
input_dim,
batch,
Some(mode),
WeightDtype::F32,
)
}
pub(crate) fn new_inner(
device: &GpuDevice,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
mode: Option<GemmMode>,
dtype: WeightDtype,
) -> Result<Self, String> {
cfg.validate()?;
let state_cap = crate::mamba_ssm::gpu::kernels::state_capacity(cfg.d_state)?;
let role = GemmRole::inference(dtype);
let ctx = match mode {
Some(mode) => GpuCtx::new_with_state_cap_mode_and_role(device, state_cap, mode, role)?,
None => GpuCtx::new_from_env_with_state_cap_and_role(device, state_cap, role)?,
};
let weights = GpuMambaWeights::from_cpu(&ctx.stream, cpu_weights, &cfg)?;
let di = cfg.d_inner();
let ds = cfg.d_state;
let total_aneg = cfg.n_layers * di * ds;
let a_neg_all = GpuBuffer::zeros(&ctx.stream, total_aneg)?;
for (layer_idx, lw) in weights.layers.iter().enumerate() {
let offset = layer_idx * di * ds;
let dst_ptr = a_neg_all.raw_ptr_at(&ctx.stream, offset);
let src_ptr = lw.a_log.ptr();
let n_i = (di * ds) as i32;
let mut builder = ctx.stream.launch_builder(&ctx.kernels.exp_negate);
builder.arg(&dst_ptr);
builder.arg(&src_ptr);
builder.arg(&n_i);
unsafe { builder.launch(grid_1d(di * ds)) }
.map_err(|e| format!("exp_negate layer {layer_idx}: {e:?}"))?;
}
let identity_proj = cpu_weights.input_proj_w.is_empty();
Ok(Self {
ctx,
weights,
a_neg_all,
cfg,
input_dim,
batch,
identity_proj,
graph: None,
captured_gemm_route: None,
captured_gemm_plan: None,
eager_gemm_manifest: Cell::new(None),
captured_state_ptr: 0,
captured_scratch_ptr: 0,
})
}
pub unsafe fn capture_graph(
&mut self,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
) -> Result<(), String> {
let manifest = self.eager_gemm_manifest.take().ok_or_else(|| {
"M1 f32 inference graph capture requires a successful eager step".to_string()
})?;
self.ctx.presize_bi_scratch()?;
let snap_state = state.conv.cached_ptr();
let snap_scratch = scratch.gpu_input.cached_ptr();
let snap_gemm_route = self.ctx.gemm_route();
let (graph, captured_gemm_plan) = unsafe {
capture_into_graph_with_gemm_plan(&self.ctx, manifest.route_capacity, &manifest, || {
self.step_kernels(state, scratch)
})
}?;
require_deterministic_gemm_graph_plan(
&self.ctx,
self.has_gemm_work(),
captured_gemm_plan.as_ref(),
"M1 f32 inference graph capture",
)?;
self.graph = Some(graph);
self.captured_gemm_route = Some(snap_gemm_route);
self.captured_gemm_plan = captured_gemm_plan;
self.captured_state_ptr = snap_state;
self.captured_scratch_ptr = snap_scratch;
self.ctx.note_graph_capture();
Ok(())
}
fn launch_captured_graph(&self) -> Result<(), String> {
let graph = self
.graph
.as_ref()
.ok_or_else(|| "M1 f32 inference graph is not captured".to_string())?;
with_validated_gemm_graph_launch(
&self.ctx,
self.has_gemm_work(),
self.captured_gemm_plan.as_ref(),
"M1 f32 inference graph replay",
|| {
graph
.launch()
.map_err(|error| format!("graph launch: {error:?}"))
},
)
}
fn has_gemm_work(&self) -> bool {
self.batch != 0 && (!self.identity_proj || self.cfg.n_layers != 0)
}
pub fn has_graph(&self) -> bool {
self.graph.is_some()
}
pub fn alloc_state(&self) -> Result<GpuInferenceState, String> {
GpuInferenceState::zeros(&self.ctx.stream, self.batch, &self.cfg)
}
pub fn alloc_scratch(&self) -> Result<GpuInferenceScratch, String> {
GpuInferenceScratch::new(&self.ctx.stream, self.batch, &self.cfg, self.input_dim)
}
pub fn step(
&self,
input: &[f32],
output: &mut [f32],
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
) -> Result<(), String> {
self.eager_gemm_manifest.set(None);
scratch.gpu_input.upload(&self.ctx.stream, input)?;
if self.graph.is_some() {
if self.captured_gemm_route != Some(self.ctx.gemm_route()) {
return Err("inference graph replay: GEMM route changed since capture".into());
}
assert_eq!(
state.conv.cached_ptr(),
self.captured_state_ptr,
"CUDA Graph replay requires the same state buffers used during capture"
);
assert_eq!(
scratch.gpu_input.cached_ptr(),
self.captured_scratch_ptr,
"CUDA Graph replay requires the same scratch buffers used during capture"
);
self.launch_captured_graph()?;
} else {
if self.has_gemm_work() {
prepare_inference_arch_rung(&self.ctx)?;
}
let manifest = self
.ctx
.record_eager_gemm_manifest(|| self.step_kernels(state, scratch))?;
self.eager_gemm_manifest.set(Some(manifest));
}
self.ctx
.stream
.synchronize()
.map_err(|e| format!("sync: {e:?}"))?;
scratch.temporal.download(&self.ctx.stream, output)?;
Ok(())
}
pub fn step_gpu_only(
&self,
input: &[f32],
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
) -> Result<(), String> {
self.eager_gemm_manifest.set(None);
scratch.gpu_input.upload(&self.ctx.stream, input)?;
if self.graph.is_some() {
if self.captured_gemm_route != Some(self.ctx.gemm_route()) {
return Err("inference graph replay: GEMM route changed since capture".into());
}
assert_eq!(state.conv.cached_ptr(), self.captured_state_ptr);
assert_eq!(scratch.gpu_input.cached_ptr(), self.captured_scratch_ptr);
self.launch_captured_graph()?;
} else {
if self.has_gemm_work() {
prepare_inference_arch_rung(&self.ctx)?;
}
let manifest = self
.ctx
.record_eager_gemm_manifest(|| self.step_kernels(state, scratch))?;
self.eager_gemm_manifest.set(Some(manifest));
}
Ok(())
}
pub fn temporal_buffer<'a>(&self, scratch: &'a GpuInferenceScratch) -> &'a GpuBuffer {
&scratch.temporal
}
fn step_kernels(
&self,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
) -> Result<(), String> {
self.step_kernels_generic(&self.weights, state, scratch)
}
#[doc(hidden)]
pub fn step_kernels_f32_debug(
&self,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
stop_after_layer: usize,
) -> Result<(), String> {
self.step_kernels_generic_impl(&self.weights, state, scratch, Some(stop_after_layer))
}
pub(super) fn step_kernels_generic<W: MambaWeightsView>(
&self,
weights: &W,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
) -> Result<(), String> {
self.step_kernels_generic_impl(weights, state, scratch, None)
}
fn step_kernels_generic_impl<W: MambaWeightsView>(
&self,
weights: &W,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
stop_after_layer: Option<usize>,
) -> Result<(), String> {
let b = self.batch;
let cfg = &self.cfg;
let dm = cfg.d_model;
let di = cfg.d_inner();
let ds = cfg.d_state;
let dt_rank = cfg.dt_rank();
let xdbl_dim = cfg.xdbl_dim();
let d_conv = cfg.d_conv;
let k = &self.ctx.kernels;
if self.identity_proj {
debug_assert_eq!(
self.input_dim, dm,
"identity_proj requires input_dim == d_model"
);
scratch
.temporal
.copy_from_raw(&scratch.gpu_input, &self.ctx.stream)?;
} else {
let (ipw_ptr, ipw_dtype) = weights.input_proj_w();
gpu_gemm_forward_dispatch(
&self.ctx,
&mut scratch.temporal,
&scratch.gpu_input,
ipw_ptr,
ipw_dtype,
Some(weights.input_proj_b()),
(b, self.input_dim, dm),
)?;
}
let f32_sz = std::mem::size_of::<f32>() as u64;
let layer_limit = stop_after_layer
.map(|n| n.min(weights.n_layers()))
.unwrap_or(weights.n_layers());
for layer_idx in 0..layer_limit {
let lw = weights.layer(layer_idx);
let conv_ptr = state.conv.cached_ptr() + (state.conv_offset(layer_idx) as u64) * f32_sz;
let ssm_ptr = state.ssm.cached_ptr() + (state.ssm_offset(layer_idx) as u64) * f32_sz;
let aneg_ptr = self.a_neg_all.cached_ptr() + (layer_idx * di * ds) as u64 * f32_sz;
{
let b_i = b as i32;
let dm_i = dm as i32;
let eps: f32 = cfg.rms_norm_eps;
let t_ptr = scratch.temporal.cached_ptr();
let rms_ptr = scratch.rms_buf.cached_ptr();
let res_ptr = scratch.residual.cached_ptr();
let nw = lw.norm_weight();
let mut bld = if layer_idx == 0 {
scratch
.residual
.copy_from_raw(&scratch.temporal, &self.ctx.stream)?;
let mut bld = self.ctx.stream.launch_builder(&k.rmsnorm_fwd);
bld.arg(&t_ptr); bld.arg(&rms_ptr);
bld.arg(&res_ptr); bld
} else {
let mut bld = self
.ctx
.stream
.launch_builder(&k.rmsnorm_fwd_resadd_typed.f32);
bld.arg(&t_ptr); bld.arg(&rms_ptr);
bld.arg(&res_ptr); bld.arg(&t_ptr); bld
};
bld.arg(&nw);
bld.arg(&b_i);
bld.arg(&dm_i);
bld.arg(&eps);
unsafe { bld.launch(grid_norm(b, dm)) }
.map_err(|e| format!("rmsnorm_fwd L{layer_idx}: {e:?}"))?;
}
let (ipw, ipw_dt) = lw.in_proj_w();
gpu_gemm_forward_dispatch(
&self.ctx,
&mut scratch.proj,
&scratch.temporal,
ipw,
ipw_dt,
None,
(b, dm, 2 * di),
)?;
{
let b_i = b as i32;
let di_i = di as i32;
let dc_i = d_conv as i32;
let x_stride_i = (2 * di) as i32;
let mut bld = self
.ctx
.stream
.launch_builder(&k.conv1d_step_fwd_silu_typed.f32);
let u_ptr = scratch.u.cached_ptr();
let proj_ptr = scratch.proj.cached_ptr();
bld.arg(&u_ptr);
bld.arg(&conv_ptr); bld.arg(&proj_ptr);
bld.arg(&x_stride_i);
let cw = lw.conv1d_weight();
let cb = lw.conv1d_bias();
bld.arg(&cw);
bld.arg(&cb);
bld.arg(&b_i);
bld.arg(&di_i);
bld.arg(&dc_i);
unsafe { bld.launch(grid_1d(b * di)) }
.map_err(|e| format!("conv1d_step+silu L{layer_idx}: {e:?}"))?;
}
let (xpw, xpw_dt) = lw.x_proj_w();
gpu_gemm_forward_dispatch(
&self.ctx,
&mut scratch.xdbl,
&scratch.u,
xpw,
xpw_dt,
None,
(b, di, xdbl_dim),
)?;
{
let b_i = b as i32;
let xdbl_i = xdbl_dim as i32;
let dt_i = dt_rank as i32;
let offset: i32 = 0;
let mut bld = self.ctx.stream.launch_builder(&k.gather_cols);
let dtg_ptr = scratch.dt_gather.cached_ptr();
let xdbl_ptr = scratch.xdbl.cached_ptr();
bld.arg(&dtg_ptr);
bld.arg(&xdbl_ptr);
bld.arg(&b_i);
bld.arg(&xdbl_i);
bld.arg(&dt_i);
bld.arg(&offset);
unsafe { bld.launch(grid_1d(b * dt_rank)) }
.map_err(|e| format!("gather_cols dt L{layer_idx}: {e:?}"))?;
}
let (dpw, dpw_dt) = lw.dt_proj_w();
gpu_gemm_forward_dispatch(
&self.ctx,
&mut scratch.delta,
&scratch.dt_gather,
dpw,
dpw_dt,
Some(lw.dt_proj_b()),
(b, dt_rank, di),
)?;
{
let b_i = b as i32;
let di_i = di as i32;
let ds_i = ds as i32;
let xdbl_stride_i = xdbl_dim as i32;
let gate_stride_i = (2 * di) as i32;
let b_off = dt_rank as i32;
let c_off = (dt_rank + ds) as i32;
assert!(
ds <= k.state_cap,
"ssm_step_fwd_fused: d_state {ds} exceeds the compiled state capacity {}",
k.state_cap
);
let dp = lw.d_param();
let mut bld = self
.ctx
.stream
.launch_builder(&k.ssm_step_fwd_fused_typed.f32);
let y_ssm_ptr = scratch.y.cached_ptr();
let delta_ssm_ptr = scratch.delta.cached_ptr();
let u_ssm_ptr = scratch.u.cached_ptr();
let xdbl_ssm_ptr = scratch.xdbl.cached_ptr();
let proj_ptr = scratch.proj.cached_ptr();
bld.arg(&ssm_ptr);
bld.arg(&y_ssm_ptr);
bld.arg(&delta_ssm_ptr);
bld.arg(&u_ssm_ptr);
bld.arg(&xdbl_ssm_ptr);
bld.arg(&proj_ptr);
bld.arg(&gate_stride_i);
bld.arg(&aneg_ptr);
bld.arg(&dp);
bld.arg(&b_i);
bld.arg(&di_i);
bld.arg(&ds_i);
bld.arg(&xdbl_stride_i);
bld.arg(&b_off);
bld.arg(&c_off);
unsafe { bld.launch(grid_1d(b * di)) }
.map_err(|e| format!("ssm_step fused L{layer_idx}: {e:?}"))?;
}
let (opw, opw_dt) = lw.out_proj_w();
gpu_gemm_forward_dispatch(
&self.ctx,
&mut scratch.temporal,
&scratch.y,
opw,
opw_dt,
None,
(b, di, dm),
)?;
}
if layer_limit > 0 && stop_after_layer.is_some() {
let n = (b * dm) as i32;
let mut bld = self.ctx.stream.launch_builder(&k.residual_add);
let t_ptr = scratch.temporal.cached_ptr();
let r_ptr = scratch.residual.cached_ptr();
bld.arg(&t_ptr);
bld.arg(&r_ptr);
bld.arg(&t_ptr); bld.arg(&n);
unsafe { bld.launch(grid_1d(b * dm)) }
.map_err(|e| format!("residual debug tail: {e:?}"))?;
}
if stop_after_layer.is_none() {
let b_i = b as i32;
let dm_i = dm as i32;
let eps: f32 = cfg.rms_norm_eps;
let mut bld = self
.ctx
.stream
.launch_builder(&k.rmsnorm_fwd_resadd_typed.f32);
let t_ptr = scratch.temporal.cached_ptr();
let rms_ptr = scratch.rms_buf.cached_ptr();
let res_ptr = scratch.residual.cached_ptr();
bld.arg(&t_ptr);
bld.arg(&rms_ptr);
bld.arg(&res_ptr);
bld.arg(&t_ptr); let nfw = weights.norm_f_weight();
bld.arg(&nfw);
bld.arg(&b_i);
bld.arg(&dm_i);
bld.arg(&eps);
unsafe { bld.launch(grid_norm(b, dm)) }.map_err(|e| format!("norm_f: {e:?}"))?;
}
Ok(())
}
pub fn config(&self) -> &MambaConfig {
&self.cfg
}
pub fn batch(&self) -> usize {
self.batch
}
pub fn ctx(&self) -> &GpuCtx {
&self.ctx
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum MixedGraphPath {
Legacy,
Native,
}
pub struct GpuMambaInferenceMixed {
engine: GpuMambaInference, mixed_weights: GpuMambaMixedWeights,
a_neg_all: GpuBuffer,
graph: Option<cudarc::driver::CudaGraph>,
captured_gemm_route: Option<crate::mamba_ssm::gpu::context::GemmRoute>,
captured_gemm_plan: Option<CapturedGemmGraphPlan>,
captured_path: Option<MixedGraphPath>,
eager_legacy_gemm_manifest: Cell<Option<PreparedGemmCaptureManifest>>,
eager_mixed_native_gemm_manifest: Cell<Option<PreparedGemmCaptureManifest>>,
captured_state_ptr: u64,
captured_scratch_ptr: u64,
captured_half_staging_ptr: u64,
captured_bi_upcast_ptrs: [u64; 3],
}
impl Drop for GpuMambaInferenceMixed {
fn drop(&mut self) {
let _ = self.engine.ctx.stream.synchronize();
drop(self.graph.take());
}
}
impl GpuMambaInferenceMixed {
fn ensure_graph_scratch(&self) -> Result<(), String> {
self.engine.ctx.ensure_graph_scratch_ptrs(
self.captured_half_staging_ptr,
self.captured_bi_upcast_ptrs,
"mixed inference graph replay",
)
}
fn has_gemm_work(&self, path: MixedGraphPath) -> bool {
self.engine.batch != 0
&& (self.engine.cfg.n_layers != 0
|| (path == MixedGraphPath::Legacy && !self.engine.identity_proj))
}
fn ensure_graph_path(&self, path: MixedGraphPath) -> Result<(), String> {
if self.captured_path != Some(path) {
return Err(
"M1 mixed inference graph replay: captured path does not match entry".into(),
);
}
Ok(())
}
fn launch_captured_graph(&self, path: MixedGraphPath) -> Result<(), String> {
self.ensure_graph_path(path)?;
let graph = self
.graph
.as_ref()
.ok_or_else(|| "M1 mixed-native inference graph is not captured".to_string())?;
with_validated_gemm_graph_launch(
&self.engine.ctx,
self.has_gemm_work(path),
self.captured_gemm_plan.as_ref(),
"M1 mixed inference graph replay",
|| {
graph
.launch()
.map_err(|error| format!("graph launch mixed_native: {error:?}"))
},
)
}
pub fn new(
device: &GpuDevice,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
bulk_dtype: WeightDtype,
) -> Result<Self, String> {
Self::new_inner(device, cpu_weights, cfg, input_dim, batch, bulk_dtype, None)
}
pub fn new_with_mode(
device: &GpuDevice,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
bulk_dtype: WeightDtype,
mode: GemmMode,
) -> Result<Self, String> {
Self::new_inner(
device,
cpu_weights,
cfg,
input_dim,
batch,
bulk_dtype,
Some(mode),
)
}
fn new_inner(
device: &GpuDevice,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
bulk_dtype: WeightDtype,
mode: Option<GemmMode>,
) -> Result<Self, String> {
cfg.validate()?;
let engine = GpuMambaInference::new_inner(
device,
cpu_weights,
cfg,
input_dim,
batch,
mode,
WeightDtype::F32,
)?;
let mixed_weights =
GpuMambaMixedWeights::from_cpu(&engine.ctx.stream, cpu_weights, &cfg, bulk_dtype)?;
engine
.ctx
.presize_half_staging_for_step(&cfg, batch, bulk_dtype)?;
let di = cfg.d_inner();
let ds = cfg.d_state;
let total_aneg = cfg.n_layers * di * ds;
let a_neg_all = GpuBuffer::zeros(&engine.ctx.stream, total_aneg)?;
for (layer_idx, lw) in mixed_weights.layers.iter().enumerate() {
let offset = layer_idx * di * ds;
let dst_ptr = a_neg_all.raw_ptr_at(&engine.ctx.stream, offset);
let src_ptr = lw.a_log.ptr();
let n_i = (di * ds) as i32;
let mut builder = engine
.ctx
.stream
.launch_builder(&engine.ctx.kernels.exp_negate);
builder.arg(&dst_ptr);
builder.arg(&src_ptr);
builder.arg(&n_i);
unsafe { builder.launch(grid_1d(di * ds)) }
.map_err(|e| format!("exp_negate mixed L{layer_idx}: {e:?}"))?;
}
Ok(Self {
engine,
mixed_weights,
a_neg_all,
graph: None,
captured_gemm_route: None,
captured_gemm_plan: None,
captured_path: None,
eager_legacy_gemm_manifest: Cell::new(None),
eager_mixed_native_gemm_manifest: Cell::new(None),
captured_state_ptr: 0,
captured_scratch_ptr: 0,
captured_half_staging_ptr: 0,
captured_bi_upcast_ptrs: [0; 3],
})
}
pub fn step(
&self,
input: &[f32],
output: &mut [f32],
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
) -> Result<(), String> {
self.eager_legacy_gemm_manifest.set(None);
scratch.gpu_input.upload(&self.engine.ctx.stream, input)?;
if self.graph.is_some() {
self.ensure_graph_path(MixedGraphPath::Legacy)?;
if self.captured_gemm_route != Some(self.engine.ctx.gemm_route()) {
return Err(
"mixed inference graph replay: GEMM route changed since capture".into(),
);
}
self.ensure_graph_scratch()?;
assert_eq!(state.conv.cached_ptr(), self.captured_state_ptr);
assert_eq!(scratch.gpu_input.cached_ptr(), self.captured_scratch_ptr);
self.launch_captured_graph(MixedGraphPath::Legacy)?;
} else {
if self.has_gemm_work(MixedGraphPath::Legacy) {
prepare_inference_arch_rung(&self.engine.ctx)?;
}
let manifest = self
.engine
.ctx
.record_eager_gemm_manifest(|| self.step_kernels_mixed(state, scratch))?;
self.eager_legacy_gemm_manifest.set(Some(manifest));
}
self.engine
.ctx
.stream
.synchronize()
.map_err(|e| format!("sync: {e:?}"))?;
scratch.temporal.download(&self.engine.ctx.stream, output)?;
Ok(())
}
pub fn step_gpu_only(
&self,
input: &[f32],
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
) -> Result<(), String> {
self.eager_legacy_gemm_manifest.set(None);
scratch.gpu_input.upload(&self.engine.ctx.stream, input)?;
if self.graph.is_some() {
self.ensure_graph_path(MixedGraphPath::Legacy)?;
if self.captured_gemm_route != Some(self.engine.ctx.gemm_route()) {
return Err(
"mixed inference graph replay: GEMM route changed since capture".into(),
);
}
self.ensure_graph_scratch()?;
assert_eq!(state.conv.cached_ptr(), self.captured_state_ptr);
assert_eq!(scratch.gpu_input.cached_ptr(), self.captured_scratch_ptr);
self.launch_captured_graph(MixedGraphPath::Legacy)?;
} else {
if self.has_gemm_work(MixedGraphPath::Legacy) {
prepare_inference_arch_rung(&self.engine.ctx)?;
}
let manifest = self
.engine
.ctx
.record_eager_gemm_manifest(|| self.step_kernels_mixed(state, scratch))?;
self.eager_legacy_gemm_manifest.set(Some(manifest));
}
Ok(())
}
fn step_kernels_mixed(
&self,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
) -> Result<(), String> {
self.engine
.step_kernels_generic(&self.mixed_weights, state, scratch)
}
#[doc(hidden)]
pub fn step_kernels_mixed_native_debug(
&self,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceMixedScratch,
stop_after_layer: usize,
) -> Result<(), String> {
self.step_kernels_mixed_native_impl(state, scratch, Some(stop_after_layer))
}
pub(super) fn step_kernels_mixed_native(
&self,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceMixedScratch,
) -> Result<(), String> {
self.step_kernels_mixed_native_impl(state, scratch, None)
}
fn step_kernels_mixed_native_impl(
&self,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceMixedScratch,
stop_after_layer: Option<usize>,
) -> Result<(), String> {
let engine = &self.engine;
assert!(
engine.identity_proj,
"step_kernels_mixed_native requires identity_proj=true (LLM path)"
);
assert_eq!(
scratch.dtype, self.mixed_weights.bulk_dtype,
"mixed scratch dtype must match mixed weights bulk_dtype"
);
let dt = scratch.dtype;
let b = engine.batch;
let cfg = &engine.cfg;
let dm = cfg.d_model;
let di = cfg.d_inner();
let ds = cfg.d_state;
let dt_rank = cfg.dt_rank();
let xdbl_dim = cfg.xdbl_dim();
let d_conv = cfg.d_conv;
let k = &engine.ctx.kernels;
let w = &self.mixed_weights;
scratch
.residual
.copy_from_raw(&scratch.gpu_input, &engine.ctx.stream)?;
let f32_sz = std::mem::size_of::<f32>() as u64;
let layer_limit = stop_after_layer
.map(|n| n.min(w.n_layers()))
.unwrap_or(w.n_layers());
for layer_idx in 0..layer_limit {
let lw = w.layer(layer_idx);
let conv_ptr = state.conv.cached_ptr() + (state.conv_offset(layer_idx) as u64) * f32_sz;
let ssm_ptr = state.ssm.cached_ptr() + (state.ssm_offset(layer_idx) as u64) * f32_sz;
let aneg_ptr = self.a_neg_all.cached_ptr() + (layer_idx * di * ds) as u64 * f32_sz;
{
let b_i = b as i32;
let dm_i = dm as i32;
let eps: f32 = cfg.rms_norm_eps;
let t_ptr = scratch.temporal.cached_ptr();
let rms_ptr = scratch.rms_buf.cached_ptr();
let res_ptr = scratch.residual.cached_ptr();
let nw = lw.norm_weight();
let mut bld = if layer_idx == 0 {
let mut bld = engine
.ctx
.stream
.launch_builder(k.rmsnorm_fwd_f32in_typed.get(dt));
bld.arg(&t_ptr);
bld.arg(&rms_ptr);
bld.arg(&res_ptr);
bld
} else {
let mut bld = engine
.ctx
.stream
.launch_builder(k.rmsnorm_fwd_resadd_typed.get(dt));
bld.arg(&t_ptr);
bld.arg(&rms_ptr);
bld.arg(&res_ptr);
bld.arg(&t_ptr); bld
};
bld.arg(&nw);
bld.arg(&b_i);
bld.arg(&dm_i);
bld.arg(&eps);
unsafe { bld.launch(grid_norm(b, dm)) }
.map_err(|e| format!("rmsnorm_f32in L{layer_idx}: {e:?}"))?;
}
let (ipw, ipw_dt) = lw.in_proj_w();
gpu_gemm_typed_forward_raw(
&engine.ctx,
TypedPtr {
ptr: scratch.proj.cached_ptr(),
dtype: dt,
},
TypedPtr {
ptr: scratch.temporal.cached_ptr(),
dtype: dt,
},
TypedPtr {
ptr: ipw,
dtype: ipw_dt,
},
None,
(b, dm, 2 * di),
)?;
{
let b_i = b as i32;
let di_i = di as i32;
let dc_i = d_conv as i32;
let x_stride_i = (2 * di) as i32;
let mut bld = engine
.ctx
.stream
.launch_builder(k.conv1d_step_fwd_silu_typed.get(dt));
let u_ptr = scratch.u.cached_ptr();
let proj_ptr = scratch.proj.cached_ptr();
bld.arg(&u_ptr);
bld.arg(&conv_ptr);
bld.arg(&proj_ptr);
bld.arg(&x_stride_i);
let cw = lw.conv1d_weight();
let cb = lw.conv1d_bias();
bld.arg(&cw);
bld.arg(&cb);
bld.arg(&b_i);
bld.arg(&di_i);
bld.arg(&dc_i);
unsafe { bld.launch(grid_1d(b * di)) }
.map_err(|e| format!("conv1d_step+silu L{layer_idx}: {e:?}"))?;
}
let (xpw, xpw_dt) = lw.x_proj_w();
gpu_gemm_typed_forward_raw(
&engine.ctx,
TypedPtr {
ptr: scratch.xdbl.cached_ptr(),
dtype: dt,
},
TypedPtr {
ptr: scratch.u.cached_ptr(),
dtype: dt,
},
TypedPtr {
ptr: xpw,
dtype: xpw_dt,
},
None,
(b, di, xdbl_dim),
)?;
{
let b_i = b as i32;
let xdbl_i = xdbl_dim as i32;
let dt_i = dt_rank as i32;
let offset: i32 = 0;
let mut bld = engine
.ctx
.stream
.launch_builder(k.gather_cols_typed.get(dt));
let dtg_ptr = scratch.dt_gather.cached_ptr();
let xdbl_ptr = scratch.xdbl.cached_ptr();
bld.arg(&dtg_ptr);
bld.arg(&xdbl_ptr);
bld.arg(&b_i);
bld.arg(&xdbl_i);
bld.arg(&dt_i);
bld.arg(&offset);
unsafe { bld.launch(grid_1d(b * dt_rank)) }
.map_err(|e| format!("gather_cols dt L{layer_idx}: {e:?}"))?;
}
let (dpw, dpw_dt) = lw.dt_proj_w();
gpu_gemm_typed_forward_raw(
&engine.ctx,
TypedPtr {
ptr: scratch.delta.cached_ptr(),
dtype: dt,
},
TypedPtr {
ptr: scratch.dt_gather.cached_ptr(),
dtype: dt,
},
TypedPtr {
ptr: dpw,
dtype: dpw_dt,
},
Some(lw.dt_proj_b()),
(b, dt_rank, di),
)?;
assert!(
ds <= k.state_cap,
"ssm_step_fwd_fused_typed: d_state {ds} exceeds the compiled \
state capacity {} (the fused kernel keeps the state in registers \
sized at compile time)",
k.state_cap
);
{
let b_i = b as i32;
let di_i = di as i32;
let ds_i = ds as i32;
let xdbl_stride_i = xdbl_dim as i32;
let gate_stride_i = (2 * di) as i32;
let b_off = dt_rank as i32;
let c_off = (dt_rank + ds) as i32;
let dp = lw.d_param();
let mut bld = engine
.ctx
.stream
.launch_builder(k.ssm_step_fwd_fused_typed.get(dt));
let y_ssm_ptr = scratch.y.cached_ptr();
let delta_ssm_ptr = scratch.delta.cached_ptr();
let u_ssm_ptr = scratch.u.cached_ptr();
let xdbl_ssm_ptr = scratch.xdbl.cached_ptr();
let proj_ptr = scratch.proj.cached_ptr();
bld.arg(&ssm_ptr);
bld.arg(&y_ssm_ptr);
bld.arg(&delta_ssm_ptr);
bld.arg(&u_ssm_ptr);
bld.arg(&xdbl_ssm_ptr);
bld.arg(&proj_ptr);
bld.arg(&gate_stride_i);
bld.arg(&aneg_ptr);
bld.arg(&dp);
bld.arg(&b_i);
bld.arg(&di_i);
bld.arg(&ds_i);
bld.arg(&xdbl_stride_i);
bld.arg(&b_off);
bld.arg(&c_off);
unsafe { bld.launch(grid_1d(b * di)) }
.map_err(|e| format!("ssm_step fused L{layer_idx}: {e:?}"))?;
}
let (opw, opw_dt) = lw.out_proj_w();
gpu_gemm_typed_forward_raw(
&engine.ctx,
TypedPtr {
ptr: scratch.temporal.cached_ptr(),
dtype: dt,
},
TypedPtr {
ptr: scratch.y.cached_ptr(),
dtype: dt,
},
TypedPtr {
ptr: opw,
dtype: opw_dt,
},
None,
(b, di, dm),
)?;
}
if layer_limit > 0 && stop_after_layer.is_some() {
let n = (b * dm) as i32;
let mut bld = engine
.ctx
.stream
.launch_builder(k.residual_add_f32_typed.get(dt));
let r_ptr = scratch.residual.cached_ptr();
let t_ptr = scratch.temporal.cached_ptr();
bld.arg(&r_ptr);
bld.arg(&r_ptr);
bld.arg(&t_ptr);
bld.arg(&n);
unsafe { bld.launch(grid_1d(b * dm)) }
.map_err(|e| format!("residual_add_f32 debug tail: {e:?}"))?;
}
if stop_after_layer.is_none() {
let b_i = b as i32;
let dm_i = dm as i32;
let eps: f32 = cfg.rms_norm_eps;
let mut bld = engine
.ctx
.stream
.launch_builder(k.rmsnorm_fwd_resadd_typed.get(dt));
let t_ptr = scratch.temporal.cached_ptr();
let rms_ptr = scratch.rms_buf.cached_ptr();
let res_ptr = scratch.residual.cached_ptr();
bld.arg(&t_ptr);
bld.arg(&rms_ptr);
bld.arg(&res_ptr);
bld.arg(&t_ptr); let nfw = w.norm_f_weight();
bld.arg(&nfw);
bld.arg(&b_i);
bld.arg(&dm_i);
bld.arg(&eps);
unsafe { bld.launch(grid_norm(b, dm)) }.map_err(|e| format!("norm_f_mixed: {e:?}"))?;
}
Ok(())
}
pub fn step_mixed_native(
&self,
input: &[f32],
output: &mut [f32],
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceMixedScratch,
) -> Result<(), String> {
self.eager_mixed_native_gemm_manifest.set(None);
scratch.gpu_input.upload(&self.engine.ctx.stream, input)?;
if self.graph.is_some() {
self.ensure_graph_path(MixedGraphPath::Native)?;
if self.captured_gemm_route != Some(self.engine.ctx.gemm_route()) {
return Err(
"mixed inference graph replay: GEMM route changed since capture".into(),
);
}
self.ensure_graph_scratch()?;
assert_eq!(state.conv.cached_ptr(), self.captured_state_ptr);
assert_eq!(scratch.gpu_input.cached_ptr(), self.captured_scratch_ptr);
self.launch_captured_graph(MixedGraphPath::Native)?;
} else {
if self.has_gemm_work(MixedGraphPath::Native) {
prepare_inference_arch_rung(&self.engine.ctx)?;
}
let manifest = self
.engine
.ctx
.record_eager_gemm_manifest(|| self.step_kernels_mixed_native(state, scratch))?;
self.eager_mixed_native_gemm_manifest.set(Some(manifest));
}
self.engine
.ctx
.stream
.synchronize()
.map_err(|e| format!("sync: {e:?}"))?;
scratch
.temporal
.download_f32(&self.engine.ctx.stream, output)?;
Ok(())
}
pub fn step_gpu_only_mixed_native(
&self,
input: &[f32],
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceMixedScratch,
) -> Result<(), String> {
self.eager_mixed_native_gemm_manifest.set(None);
scratch.gpu_input.upload(&self.engine.ctx.stream, input)?;
if self.graph.is_some() {
self.ensure_graph_path(MixedGraphPath::Native)?;
if self.captured_gemm_route != Some(self.engine.ctx.gemm_route()) {
return Err(
"mixed inference graph replay: GEMM route changed since capture".into(),
);
}
self.ensure_graph_scratch()?;
assert_eq!(state.conv.cached_ptr(), self.captured_state_ptr);
assert_eq!(scratch.gpu_input.cached_ptr(), self.captured_scratch_ptr);
self.launch_captured_graph(MixedGraphPath::Native)?;
Ok(())
} else {
if self.has_gemm_work(MixedGraphPath::Native) {
prepare_inference_arch_rung(&self.engine.ctx)?;
}
let manifest = self
.engine
.ctx
.record_eager_gemm_manifest(|| self.step_kernels_mixed_native(state, scratch))?;
self.eager_mixed_native_gemm_manifest.set(Some(manifest));
Ok(())
}
}
pub fn alloc_mixed_scratch(&self) -> Result<GpuInferenceMixedScratch, String> {
GpuInferenceMixedScratch::new(
&self.engine.ctx.stream,
self.engine.batch,
&self.engine.cfg,
self.engine.input_dim,
self.mixed_weights.bulk_dtype,
)
}
pub unsafe fn capture_graph_mixed_native(
&mut self,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceMixedScratch,
) -> Result<(), String> {
let manifest = self
.eager_mixed_native_gemm_manifest
.take()
.ok_or_else(|| {
"M1 mixed-native inference graph capture requires a successful eager step"
.to_string()
})?;
self.engine.ctx.presize_bi_scratch()?;
self.engine
.ctx
.presize_mixed_graph_scratch_m1(&self.engine.dims(1), self.mixed_weights.bulk_dtype)?;
let snap_state = state.conv.cached_ptr();
let snap_scratch = scratch.gpu_input.cached_ptr();
let snap_half_staging = self.engine.ctx.half_staging_ptr();
let snap_bi_upcast = self.engine.ctx.bi_upcast_scratch_ptrs();
let snap_gemm_route = self.engine.ctx.gemm_route();
self.engine.ctx.freeze_graph_scratch();
let (graph, captured_gemm_plan) = unsafe {
capture_into_graph_with_gemm_plan(
&self.engine.ctx,
manifest.route_capacity,
&manifest,
|| self.step_kernels_mixed_native(state, scratch),
)
}?;
require_deterministic_gemm_graph_plan(
&self.engine.ctx,
self.has_gemm_work(MixedGraphPath::Native),
captured_gemm_plan.as_ref(),
"M1 mixed-native inference graph capture",
)?;
self.graph = Some(graph);
self.captured_gemm_route = Some(snap_gemm_route);
self.captured_gemm_plan = captured_gemm_plan;
self.captured_path = Some(MixedGraphPath::Native);
self.captured_state_ptr = snap_state;
self.captured_scratch_ptr = snap_scratch;
self.captured_half_staging_ptr = snap_half_staging;
self.captured_bi_upcast_ptrs = snap_bi_upcast;
self.engine.ctx.note_graph_capture();
Ok(())
}
pub unsafe fn capture_graph(
&mut self,
state: &mut GpuInferenceState,
scratch: &mut GpuInferenceScratch,
) -> Result<(), String> {
let manifest = self.eager_legacy_gemm_manifest.take().ok_or_else(|| {
"M1 legacy mixed inference graph capture requires a successful eager step".to_string()
})?;
self.engine.ctx.presize_bi_scratch()?;
self.engine
.ctx
.presize_mixed_graph_scratch_m1(&self.engine.dims(1), self.mixed_weights.bulk_dtype)?;
let snap_state = state.conv.cached_ptr();
let snap_scratch = scratch.gpu_input.cached_ptr();
let snap_half_staging = self.engine.ctx.half_staging_ptr();
let snap_bi_upcast = self.engine.ctx.bi_upcast_scratch_ptrs();
let snap_gemm_route = self.engine.ctx.gemm_route();
self.engine.ctx.freeze_graph_scratch();
let (graph, captured_gemm_plan) = unsafe {
capture_into_graph_with_gemm_plan(
&self.engine.ctx,
manifest.route_capacity,
&manifest,
|| self.step_kernels_mixed(state, scratch),
)
}?;
require_deterministic_gemm_graph_plan(
&self.engine.ctx,
self.has_gemm_work(MixedGraphPath::Legacy),
captured_gemm_plan.as_ref(),
"M1 legacy mixed inference graph capture",
)?;
self.graph = Some(graph);
self.captured_gemm_route = Some(snap_gemm_route);
self.captured_gemm_plan = captured_gemm_plan;
self.captured_path = Some(MixedGraphPath::Legacy);
self.captured_state_ptr = snap_state;
self.captured_scratch_ptr = snap_scratch;
self.captured_half_staging_ptr = snap_half_staging;
self.captured_bi_upcast_ptrs = snap_bi_upcast;
self.engine.ctx.note_graph_capture();
Ok(())
}
pub fn alloc_state(&self) -> Result<GpuInferenceState, String> {
self.engine.alloc_state()
}
pub fn alloc_scratch(&self) -> Result<GpuInferenceScratch, String> {
self.engine.alloc_scratch()
}
pub fn config(&self) -> &MambaConfig {
&self.engine.cfg
}
pub fn batch(&self) -> usize {
self.engine.batch
}
pub fn ctx(&self) -> &GpuCtx {
&self.engine.ctx
}
pub fn stream(&self) -> &Arc<cudarc::driver::CudaStream> {
&self.engine.ctx.stream
}
pub fn bulk_dtype(&self) -> WeightDtype {
self.mixed_weights.bulk_dtype
}
pub fn has_graph(&self) -> bool {
self.graph.is_some()
}
pub fn engine_ref(&self) -> &GpuMambaInference {
&self.engine
}
pub fn weights_mixed_ref(&self) -> &GpuMambaMixedWeights {
&self.mixed_weights
}
pub fn a_neg_all_ref(&self) -> &GpuBuffer {
&self.a_neg_all
}
}
enum BackboneEngine {
F32(Box<GpuMambaInference>),
Mixed(Box<GpuMambaInferenceMixed>),
}
enum BackboneScratch {
F32(GpuInferenceScratch),
Mixed(GpuInferenceMixedScratch),
}
impl BackboneScratch {
fn temporal_ptr(&self) -> cudarc::driver::sys::CUdeviceptr {
match self {
BackboneScratch::F32(s) => s.temporal.cached_ptr(),
BackboneScratch::Mixed(s) => s.temporal.cached_ptr(),
}
}
fn temporal_dtype(&self) -> WeightDtype {
match self {
BackboneScratch::F32(_) => WeightDtype::F32,
BackboneScratch::Mixed(s) => s.dtype,
}
}
}
pub struct GpuMambaBackbone {
engine: BackboneEngine,
state: GpuInferenceState,
scratch: BackboneScratch,
}
impl GpuMambaBackbone {
pub fn new(
gpu_ordinal: usize,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
) -> Result<Self, String> {
Self::new_with_dtype(
gpu_ordinal,
cpu_weights,
cfg,
input_dim,
batch,
WeightDtype::F32,
)
}
pub fn new_with_mode(
gpu_ordinal: usize,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
mode: GemmMode,
) -> Result<Self, String> {
Self::new_with_dtype_and_mode(
gpu_ordinal,
cpu_weights,
cfg,
input_dim,
batch,
WeightDtype::F32,
mode,
)
}
pub fn new_with_dtype(
gpu_ordinal: usize,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
dtype: WeightDtype,
) -> Result<Self, String> {
Self::new_with_dtype_inner(gpu_ordinal, cpu_weights, cfg, input_dim, batch, dtype, None)
}
pub fn new_with_dtype_and_mode(
gpu_ordinal: usize,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
dtype: WeightDtype,
mode: GemmMode,
) -> Result<Self, String> {
Self::new_with_dtype_inner(
gpu_ordinal,
cpu_weights,
cfg,
input_dim,
batch,
dtype,
Some(mode),
)
}
fn new_with_dtype_inner(
gpu_ordinal: usize,
cpu_weights: &MambaWeights,
cfg: MambaConfig,
input_dim: usize,
batch: usize,
dtype: WeightDtype,
mode: Option<GemmMode>,
) -> Result<Self, String> {
let device = GpuDevice::new(gpu_ordinal)?;
let (engine, state, scratch) = match dtype {
WeightDtype::F32 | WeightDtype::Tf32 => {
let e = GpuMambaInference::new_inner(
&device,
cpu_weights,
cfg,
input_dim,
batch,
mode,
dtype,
)?;
let s = e.alloc_state()?;
let sc = BackboneScratch::F32(e.alloc_scratch()?);
(BackboneEngine::F32(Box::new(e)), s, sc)
}
WeightDtype::Bf16 | WeightDtype::F16 => {
let e = match mode {
Some(mode) => GpuMambaInferenceMixed::new_with_mode(
&device,
cpu_weights,
cfg,
input_dim,
batch,
dtype,
mode,
)?,
None => GpuMambaInferenceMixed::new(
&device,
cpu_weights,
cfg,
input_dim,
batch,
dtype,
)?,
};
let s = e.alloc_state()?;
let sc = BackboneScratch::Mixed(e.alloc_mixed_scratch()?);
(BackboneEngine::Mixed(Box::new(e)), s, sc)
}
};
Ok(Self {
engine,
state,
scratch,
})
}
pub fn dtype(&self) -> WeightDtype {
match &self.engine {
BackboneEngine::F32(e) => e.ctx.f32_storage_dtype(),
BackboneEngine::Mixed(e) => e.bulk_dtype(),
}
}
pub fn step(&mut self, input: &[f32], output: &mut [f32]) -> Result<(), String> {
match (&self.engine, &mut self.scratch) {
(BackboneEngine::F32(e), BackboneScratch::F32(sc)) => {
e.step(input, output, &mut self.state, sc)
}
(BackboneEngine::Mixed(e), BackboneScratch::Mixed(sc)) => {
e.step_mixed_native(input, output, &mut self.state, sc)
}
_ => Err("engine/scratch dtype mismatch (internal invariant)".to_string()),
}
}
pub fn reset(&mut self) -> Result<(), String> {
let stream = match &self.engine {
BackboneEngine::F32(e) => e.ctx.stream.clone(),
BackboneEngine::Mixed(e) => e.stream().clone(),
};
self.state.reset(&stream)
}
pub fn capture_graph(&mut self) -> Result<(), String> {
let (input_dim, batch, d_model) = match &self.engine {
BackboneEngine::F32(e) => (e.input_dim, e.batch, e.cfg.d_model),
BackboneEngine::Mixed(e) => (
e.engine_ref().input_dim,
e.engine_ref().batch,
e.engine_ref().cfg.d_model,
),
};
let input = vec![0.0f32; batch * input_dim];
let mut output = vec![0.0f32; batch * d_model];
self.step(&input, &mut output)?;
self.reset()?;
match (&mut self.engine, &mut self.scratch) {
(BackboneEngine::F32(e), BackboneScratch::F32(sc)) => {
unsafe { e.capture_graph(&mut self.state, sc) }
}
(BackboneEngine::Mixed(e), BackboneScratch::Mixed(sc)) => {
unsafe { e.capture_graph_mixed_native(&mut self.state, sc) }
}
_ => Err("engine/scratch dtype mismatch".to_string()),
}
}
pub fn config(&self) -> &MambaConfig {
match &self.engine {
BackboneEngine::F32(e) => e.config(),
BackboneEngine::Mixed(e) => e.config(),
}
}
pub fn batch(&self) -> usize {
match &self.engine {
BackboneEngine::F32(e) => e.batch(),
BackboneEngine::Mixed(e) => e.batch(),
}
}
pub fn has_graph(&self) -> bool {
match &self.engine {
BackboneEngine::F32(e) => e.has_graph(),
BackboneEngine::Mixed(e) => e.has_graph(),
}
}
pub fn ctx(&self) -> &GpuCtx {
match &self.engine {
BackboneEngine::F32(e) => &e.ctx,
BackboneEngine::Mixed(e) => e.ctx(),
}
}
pub fn stream(&self) -> &std::sync::Arc<cudarc::driver::CudaStream> {
match &self.engine {
BackboneEngine::F32(e) => &e.ctx.stream,
BackboneEngine::Mixed(e) => e.stream(),
}
}
pub fn step_gpu_only(&mut self, input: &[f32]) -> Result<(), String> {
match (&self.engine, &mut self.scratch) {
(BackboneEngine::F32(e), BackboneScratch::F32(sc)) => {
e.step_gpu_only(input, &mut self.state, sc)
}
(BackboneEngine::Mixed(e), BackboneScratch::Mixed(sc)) => {
e.step_gpu_only_mixed_native(input, &mut self.state, sc)
}
_ => Err("engine/scratch dtype mismatch".to_string()),
}
}
#[doc(hidden)]
pub fn debug_step_partial(
&mut self,
input: &[f32],
layer_limit: usize,
out: &mut [f32],
) -> Result<(), String> {
match (&self.engine, &mut self.scratch) {
(BackboneEngine::F32(e), BackboneScratch::F32(sc)) => {
sc.gpu_input.upload(&e.ctx.stream, input)?;
e.step_kernels_f32_debug(&mut self.state, sc, layer_limit)?;
e.ctx
.stream
.synchronize()
.map_err(|err| format!("sync: {err:?}"))?;
sc.temporal.download(&e.ctx.stream, out)
}
(BackboneEngine::Mixed(e), BackboneScratch::Mixed(sc)) => {
sc.gpu_input.upload(e.stream(), input)?;
e.step_kernels_mixed_native_debug(&mut self.state, sc, layer_limit)?;
e.stream()
.synchronize()
.map_err(|err| format!("sync: {err:?}"))?;
sc.residual.download(e.stream(), out)
}
_ => Err("engine/scratch dtype mismatch".to_string()),
}
}
pub fn temporal_ptr(&self) -> cudarc::driver::sys::CUdeviceptr {
self.scratch.temporal_ptr()
}
pub fn temporal_dtype(&self) -> WeightDtype {
self.scratch.temporal_dtype()
}
pub fn download_temporal(&self, output: &mut [f32]) -> Result<(), String> {
self.stream()
.synchronize()
.map_err(|e| format!("sync: {e:?}"))?;
match &self.scratch {
BackboneScratch::F32(s) => s.temporal.download(self.stream(), output),
BackboneScratch::Mixed(s) => s.temporal.download_f32(self.stream(), output),
}
}
pub fn prefill_sequence(
&mut self,
ip_out_flat: &GpuBuffer,
prefill_scratch: &mut super::backward::GpuMambaTargetScratch,
) -> Result<(), String> {
use super::prefill::{PrefillInputs, gpu_forward_inference_prefill};
match (&self.engine, &mut self.scratch) {
(BackboneEngine::F32(e), BackboneScratch::F32(sc)) => gpu_forward_inference_prefill(
&e.ctx,
&mut sc.temporal,
PrefillInputs {
ip_out_flat,
weights: &e.weights,
a_neg_all: &e.a_neg_all,
},
&mut self.state,
prefill_scratch,
),
(BackboneEngine::Mixed(_), _) => {
Err("mixed backbone: use prefill_sequence_mixed with a \
GpuMambaTargetMixedScratch (native bf16/f16 prefill)"
.to_string())
}
_ => Err("engine/scratch dtype mismatch".to_string()),
}
}
pub fn prefill_sequence_mixed(
&mut self,
ip_out_flat: &GpuBuffer,
prefill_scratch: &mut super::backward::GpuMambaTargetMixedScratch,
) -> Result<(), String> {
use super::prefill::{PrefillInputs, gpu_forward_inference_prefill_mixed};
match (&self.engine, &mut self.scratch) {
(BackboneEngine::Mixed(e), BackboneScratch::Mixed(sc)) => {
gpu_forward_inference_prefill_mixed(
e.ctx(),
&sc.temporal,
PrefillInputs {
ip_out_flat,
weights: e.weights_mixed_ref(),
a_neg_all: e.a_neg_all_ref(),
},
&mut self.state,
prefill_scratch,
)
}
_ => Err("prefill_sequence_mixed requires Mixed backbone + Mixed scratch".to_string()),
}
}
pub fn alloc_prefill_mixed_scratch(
&self,
seq_len: usize,
) -> Result<super::backward::GpuMambaTargetMixedScratch, String> {
let dtype = match &self.engine {
BackboneEngine::Mixed(e) => e.bulk_dtype(),
BackboneEngine::F32(_) => {
return Err("alloc_prefill_mixed_scratch: backbone is F32".to_string());
}
};
let cfg = self.config();
let dims = super::forward::GpuMambaDims {
batch: self.batch(),
seq_len,
n_layers: cfg.n_layers,
d_model: cfg.d_model,
d_inner: cfg.d_inner(),
d_state: cfg.d_state,
d_conv: cfg.d_conv,
dt_rank: cfg.dt_rank(),
xdbl_dim: cfg.xdbl_dim(),
mamba_input_dim: cfg.d_model,
scan_mode: cfg.scan_mode,
rms_norm_eps: cfg.rms_norm_eps,
};
super::backward::GpuMambaTargetMixedScratch::new(self.stream(), &dims, dtype)
}
pub fn alloc_prefill_scratch(
&self,
seq_len: usize,
) -> Result<super::backward::GpuMambaTargetScratch, String> {
let cfg = self.config();
let dims = super::forward::GpuMambaDims {
batch: self.batch(),
seq_len,
n_layers: cfg.n_layers,
d_model: cfg.d_model,
d_inner: cfg.d_inner(),
d_state: cfg.d_state,
d_conv: cfg.d_conv,
dt_rank: cfg.dt_rank(),
xdbl_dim: cfg.xdbl_dim(),
mamba_input_dim: cfg.d_model, scan_mode: cfg.scan_mode,
rms_norm_eps: cfg.rms_norm_eps,
};
super::backward::GpuMambaTargetScratch::new(self.stream(), &dims)
}
}