GPU Mamba forward + rtx-tensor device-pointer API + matrix exponential
Performance Benchmarks / Run Benchmarks (pull_request) Has been cancelled
CI / Format Check (pull_request) Has been cancelled
Documentation / Build User Guide (pull_request) Has been cancelled
CI / Clippy Check (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Test (macos-latest) (pull_request) Has been cancelled
CI / Test (ubuntu-latest) (pull_request) Has been cancelled
CI / Build CPU-Only (Explicit) (pull_request) Has been cancelled
CI / CI Success (pull_request) Has been cancelled
Documentation / Build API Documentation (pull_request) Has been cancelled
GPU Tests / Check GPU Availability (pull_request) Has been cancelled
GPU Tests / CUDA Tests (11.8) (pull_request) Has been cancelled
GPU Tests / CUDA Tests (12.1) (pull_request) Has been cancelled
GPU Tests / Metal Tests (pull_request) Has been cancelled

Persist accumulated WIP across rtx-tensor / rtx-transformers / rtx-runtime.
Two related feature groups:

GPU enablement (unblocks the Phase-3 spec §8 CUDA Mamba path):
- rtx-tensor: `Tensor::cuda_device_ptr()` + storage GPU-buffer accessors
  (`raw_ptr.rs`) expose the raw CUdeviceptr that kernel launches need —
  the "rtx-tensor GPU memory access API" the Mamba CUDA kernels were
  blocked on.
- rtx-transformers: `MambaBlock::forward_cuda` runs the four linear
  projections through cuBLAS on-device (in/out/x/dt_proj), keeping the
  selective scan + conv1d + activations on CPU; dispatched automatically
  from `forward` when on a CUDA device under the `cuda` feature. Updated
  `mamba_cuda_kernels.rs` accordingly.
- supporting plumbing in rtx-runtime stream/bridge and rtx-tensor
  storage/conversion/concatenation/creation + rtx-flash-attention.

Linear algebra (rtx-tensor):
- `linalg/matrix_exp.rs`: real matrix exponential via scaling-and-squaring
  with a degree-13 Padé approximant (Higham 2005), f64 internally.
- `complex/linalg.rs`: complex matmul/adjoint, Hermitian eigendecomposition
  (`ComplexEigenResult`), and the complex matrix exponential, nalgebra-backed.
- tests for both.

Builds verified on the CPU path (`cargo check -p rtx-tensor -p rtx-transformers
-p rtx-runtime -p rtx-flash-attention` clean). The `cuda` feature and the
rtx-backend-cuda NVCC build remain unbuildable on this host (CUDA/glibc header
mismatch) — pre-existing and unrelated to these changes.
This commit is contained in:
2026-06-15 18:51:30 -07:00
parent 627db58d09
commit 5c2ee63d53
18 changed files with 939 additions and 92 deletions
@@ -504,6 +504,12 @@ impl MambaBlock {
/// Forward pass through `MambaBlock`
pub fn forward(&self, x: &Tensor) -> Result<MambaOutput> {
// GPU-accelerated path: cuBLAS for projections, CPU loops for SSM scan.
#[cfg(feature = "cuda")]
if matches!(self.device, Device::Cuda(_)) {
return self.forward_cuda(x);
}
let dims = x.shape().dims().to_vec();
let (b, l, d_model) = (dims[0], dims[1], dims[2]);
let d = self.config.get_d_inner();
@@ -624,6 +630,136 @@ impl MambaBlock {
})
}
/// GPU-accelerated forward pass using cuBLAS for the four linear projections.
///
/// The two large projections (`in_proj` [b*l,d_model]→[b*l,2d] and `out_proj`
/// [b*l,d]→[b*l,d_model]) and the two smaller projections (`x_proj` and
/// `dt_proj`) are dispatched to cuBLAS and stay on GPU. The SSM selective
/// scan, conv1d, and element-wise activations (SiLU, softplus) remain on CPU
/// and require one D2H + one H2D transfer of the intermediate activations.
///
/// Called automatically by [`Self::forward`] when `self.device` is
/// `Device::Cuda(_)` and the `cuda` feature is enabled.
#[cfg(feature = "cuda")]
fn forward_cuda(&self, x: &Tensor) -> Result<MambaOutput> {
let dims = x.shape().dims().to_vec();
let (b, l, d_model) = (dims[0], dims[1], dims[2]);
let d = self.config.get_d_inner();
let n = self.config.d_state;
let dt_rank = self.config.get_dt_rank();
let kc = self.config.d_conv;
let dbc = dt_rank + 2 * n;
// ── Step 1: in_proj on GPU (cuBLAS) ─────────────────────────────────
// in_proj: [d_model, 2*d]. x_flat: [b*l, d_model].
// xz = x_flat @ in_proj → [b*l, 2*d].
let x_flat = x.view([b * l, d_model])?;
let xz_gpu = x_flat.matmul(&self.in_proj)?;
// D2H once only [b*l * 2*d] floats.
let xz = xz_gpu.to_cpu()?;
// Split xz into the SSM branch (x_in) and the gate branch (z).
let mut x_in = vec![0.0f32; b * l * d];
let mut z = vec![0.0f32; b * l * d];
for i in 0..b * l {
x_in[i * d..i * d + d].copy_from_slice(&xz[i * 2 * d..i * 2 * d + d]);
z[i * d..i * d + d].copy_from_slice(&xz[i * 2 * d + d..i * 2 * d + 2 * d]);
}
// ── Step 2-3: causal conv1d + SiLU on CPU (O(b*l*d*kc), cheap) ──────
let conv_w = self.conv1d_weight.to_cpu()?; // [d, 1, kc] stored as [d, kc]
let conv_b = match &self.conv1d_bias {
Some(t) => Some(t.to_cpu()?),
None => None,
};
let mut u = vec![0.0f32; b * l * d];
for bi in 0..b {
for li in 0..l {
for j in 0..d {
let mut acc = conv_b.as_ref().map_or(0.0f32, |cb| cb[j]);
for kk in 0..kc {
let src = li as isize - (kc as isize - 1) + kk as isize;
if src >= 0 {
acc += x_in[(bi * l + src as usize) * d + j]
* conv_w[j * kc + kk];
}
}
u[(bi * l + li) * d + j] = silu_f32(acc);
}
}
}
// ── Step 4: x_proj on GPU (cuBLAS) ──────────────────────────────────
// u_gpu: [b*l, d]. x_proj: [d, dbc].
// xdbl = u_gpu @ x_proj → [b*l, dbc].
let u_gpu = Tensor::from_vec(u.clone(), &[b * l, d], &self.device)?;
let xdbl_gpu = u_gpu.matmul(&self.x_proj)?;
// D2H xdbl [b*l * dbc] floats (small: dbc ≈ 48).
let xdbl = xdbl_gpu.to_cpu()?;
// ── Step 5: extract dt, B, C; dt_proj + softplus on CPU ─────────────
let dt_proj_w = self.dt_proj.to_cpu()?; // [dt_rank, d]
let dt_bias = self.dt_bias.to_cpu()?; // [d]
let a_log = self.A_log.to_cpu()?; // [d, n]
let d_skip = self.d_skip.to_cpu()?; // [d]
let mut bmat = vec![0.0f32; b * l * n];
let mut cmat = vec![0.0f32; b * l * n];
let mut delta = vec![0.0f32; b * l * d];
for i in 0..b * l {
for nn in 0..n {
bmat[i * n + nn] = xdbl[i * dbc + dt_rank + nn];
cmat[i * n + nn] = xdbl[i * dbc + dt_rank + n + nn];
}
for j in 0..d {
let mut s = dt_bias[j];
for r in 0..dt_rank {
s += xdbl[i * dbc + r] * dt_proj_w[r * d + j];
}
delta[i * d + j] = softplus_f32(s);
}
}
// ── Step 6-7: selective scan + SiLU gate on CPU ──────────────────────
let mut out_flat = vec![0.0f32; b * l * d];
for bi in 0..b {
let mut h = vec![0.0f32; d * n];
for li in 0..l {
let base = (bi * l + li) * d;
for j in 0..d {
let dj = delta[base + j];
let uj = u[base + j];
let mut yj = d_skip[j] * uj;
for nn in 0..n {
let a = -a_log[j * n + nn].exp();
let da = (dj * a).exp();
let dbu = dj * bmat[(bi * l + li) * n + nn] * uj;
let hv = da * h[j * n + nn] + dbu;
h[j * n + nn] = hv;
yj += cmat[(bi * l + li) * n + nn] * hv;
}
// SiLU gate fused into the scan output.
out_flat[base + j] = yj * silu_f32(z[base + j]);
}
}
}
// ── Step 8: out_proj on GPU (cuBLAS) ─────────────────────────────────
// H2D once [b*l * d] floats.
let y_gated_gpu = Tensor::from_vec(out_flat, &[b * l, d], &self.device)?;
// out_proj: [d, d_model].
let out_flat_gpu = y_gated_gpu.matmul(&self.out_proj)?;
// Reshape to [b, l, d_model] — zero-copy view.
let output = out_flat_gpu.view([b, l, d_model])?;
Ok(MambaOutput {
output,
aux_info: None,
})
}
/// Analytic backward pass: given `d_out = ∂L/∂out` (same shape as
/// the forward output, `[b,l,d_model]`), return `∂L/∂θ` for every
/// parameter, keyed by its persistence name (`in_proj`,