pub(super) mod layout;
use cudarc::driver::{
CudaFunction, CudaSlice, CudaStream, CudaView, CudaViewMut, DevicePtrMut, LaunchConfig,
PushKernelArg,
};
use self::layout::{
GATE_COLUMNS, GROUPS, HIDDEN, KERNEL, STATE_TILE, Schedule, pack_bias, pack_directions,
};
use super::{
Batches, Coverage, CoverageEntry, DeviceAttributes, Direction, FiniteContract, GeometryError,
InfinityContract, LstmCandidate, LstmPhases, LstmPin, LstmSpec, Maths, NanContract, Op,
PlanError, ProjectionGemm, Scratch, SideStream, SignedZeroContract, SpecialValues,
};
use crate::inference::cuda::{CudaError, CudaMath, CudaRuntime, LoadedKernels, PtxTier};
const SPK_LSTM_CLEAR: &str = "spk_lstm_clear";
pub(crate) const REQUIRED_KERNELS: [&str; 2] = [KERNEL, SPK_LSTM_CLEAR];
const THREADS: u32 = 128;
const FEATURES: usize = 60;
const DIRECTIONS: [Direction; 2] = [Direction::Forward, Direction::Reverse];
#[derive(Debug)]
struct PackedLayer {
input: usize,
w: CudaSlice<f32>,
r: CudaSlice<f32>,
bias: CudaSlice<f32>,
}
#[derive(Debug)]
pub(crate) struct Oxide {
recurrence: CudaFunction,
clear: CudaFunction,
layers: [PackedLayer; 4],
batch: usize,
steps: usize,
schedule: Schedule,
gates: Scratch<f32>,
state: Scratch<u64>,
side: SideStream,
}
impl LstmCandidate for Oxide {
const COVERAGE: Coverage = Coverage(&[CoverageEntry {
layers: &["lstm.stack"],
batches: Batches::All,
maths: Maths::Only(&[CudaMath::Fp32]),
}]);
const SPECIAL_VALUES: SpecialValues = SpecialValues {
finite: FiniteContract::BoundedActivation { headroom: 2 },
nan: NanContract::Unspecified,
infinity: InfinityContract::Unspecified,
signed_zero: SignedZeroContract::Unspecified,
};
fn implemented_pin(_spec: &LstmSpec<'_>) -> Result<LstmPin, PlanError> {
Ok(LstmPin::LegacyCooperative)
}
fn device_pin(
_device: &DeviceAttributes,
_tier: PtxTier,
_spec: &LstmSpec<'_>,
) -> Result<LstmPin, PlanError> {
Ok(LstmPin::LegacyCooperative)
}
fn plan(
runtime: &CudaRuntime,
kernels: &LoadedKernels,
spec: LstmSpec<'_>,
pin: LstmPin,
) -> Result<Self, PlanError> {
let LstmPin::LegacyCooperative = pin else {
return Err(PlanError::Geometry(GeometryError::Invalid {
context: "oxide LSTM plan",
reason: format!("pin {pin:?} belongs to the lstmproj area"),
}));
};
let recurrence = kernels.function(KERNEL)?;
let clear = kernels.function(SPK_LSTM_CLEAR)?;
let capacity = runtime.cooperative_capacity(&recurrence, THREADS, 0)?;
let concurrent_capacity =
runtime.concurrent_cooperative_capacity(&recurrence, THREADS, 0)?;
let schedule = Schedule::new(spec.batch, capacity, concurrent_capacity)?;
let stream = runtime.stream();
let pack = |index: usize| -> Result<PackedLayer, CudaError> {
let layer = spec.layers[index];
let input = if index == 0 { FEATURES } else { 2 * HIDDEN };
check_len("oxide LSTM input width", input, layer.input)?;
check_len("oxide LSTM W", 2 * GATE_COLUMNS * input, layer.w.len())?;
check_len("oxide LSTM R", 2 * GATE_COLUMNS * HIDDEN, layer.r.len())?;
check_len("oxide LSTM B", 4 * GATE_COLUMNS, layer.b.len())?;
Ok(PackedLayer {
input,
w: stream.clone_htod(&pack_directions(layer.w, input))?,
r: stream.clone_htod(&pack_directions(layer.r, HIDDEN))?,
bias: stream.clone_htod(&pack_bias(layer.b))?,
})
};
Ok(Self {
recurrence,
clear,
layers: [pack(0)?, pack(1)?, pack(2)?, pack(3)?],
batch: spec.batch,
steps: spec.frames,
schedule,
gates: Scratch::zeros(runtime, 2 * spec.batch * spec.frames * GATE_COLUMNS)?,
state: Scratch::zeros(runtime, 2 * schedule.tiles * STATE_TILE)?,
side: SideStream::new(runtime)?,
})
}
fn enqueue(
&self,
input: &CudaView<'_, f32>,
output: &mut CudaViewMut<'_, f32>,
phases: &LstmPhases<'_>,
stream: &CudaStream,
) -> Result<(), CudaError> {
let rows = self.batch * self.steps;
check_len("oxide LSTM input", rows * FEATURES, input.len())?;
check_len("oxide LSTM output", rows * 2 * HIDDEN, output.len())?;
phases.op(Op::Prologue, || self.clear_state(stream))?;
for (index, layer) in self.layers.iter().enumerate() {
for direction in DIRECTIONS {
phases.input_proj(index, direction, |projection| {
let d = direction_index(direction);
let weights = GATE_COLUMNS * layer.input;
let w = layer.w.slice(d * weights..(d + 1) * weights);
let mut gates = self.gates.get();
let mut gates =
gates.slice_mut(d * rows * GATE_COLUMNS..(d + 1) * rows * GATE_COLUMNS);
let gemm = ProjectionGemm {
n: GATE_COLUMNS,
weight_transposed: true,
beta: 0.0,
};
if index == 0 {
projection.project(index, direction, gemm, input, &w, &mut gates)
} else {
projection.project(
index,
direction,
gemm,
&output.as_view(),
&w,
&mut gates,
)
}
})?;
}
self.recur(index, layer, output, phases, stream)?;
}
Ok(())
}
}
impl Oxide {
fn clear_state(&self, stream: &CudaStream) -> Result<(), CudaError> {
let mut state = self.state.get();
let len = state.len();
let len_arg = len as u64;
let mut builder = stream.launch_builder(&self.clear);
builder.arg(&mut *state).arg(&len_arg);
unsafe {
builder.launch(LaunchConfig::for_num_elems(to_u32(
"oxide LSTM clear",
len,
)?))
}?;
Ok(())
}
fn recur(
&self,
index: usize,
layer: &PackedLayer,
output: &mut CudaViewMut<'_, f32>,
phases: &LstmPhases<'_>,
stream: &CudaStream,
) -> Result<(), CudaError> {
let context = "oxide LSTM recurrence";
let rows = self.batch * self.steps;
let mut state = self.state.get();
let gates = self.gates.get();
let (output, _output_record) = output.device_ptr_mut(stream);
let (state, _state_record) = state.device_ptr_mut(stream);
let concurrent = self.schedule.concurrent;
if concurrent {
self.side.split(stream)?;
}
for direction in DIRECTIONS {
let d = direction_index(direction);
let launch = DirectionLaunch {
gates: gates.slice(d * rows * GATE_COLUMNS..(d + 1) * rows * GATE_COLUMNS),
direction: d,
output: output + (d * HIDDEN * size_of::<f32>()) as u64,
state: state + (d * self.schedule.tiles * STATE_TILE * size_of::<u64>()) as u64,
flag_base: to_u32(context, index * self.steps)?,
};
let on: &CudaStream = if concurrent && d == 1 {
self.side.stream()
} else {
stream
};
phases.recurrence(index, direction, || self.launch(on, layer, launch))?;
}
if concurrent {
self.side.merge(stream)?;
}
Ok(())
}
fn launch(
&self,
stream: &CudaStream,
layer: &PackedLayer,
launch: DirectionLaunch<'_>,
) -> Result<(), CudaError> {
let context = "oxide LSTM recurrence";
let DirectionLaunch {
gates,
direction,
output,
state,
flag_base,
} = launch;
let recurrent = GATE_COLUMNS * HIDDEN;
let r = layer
.r
.slice(direction * recurrent..(direction + 1) * recurrent);
let bias = layer
.bias
.slice(direction * GATE_COLUMNS..(direction + 1) * GATE_COLUMNS);
let lens = [gates.len(), r.len(), bias.len()].map(|len| len as u64);
let batch = to_u32(context, self.batch)?;
let steps = to_u32(context, self.steps)?;
let direction = to_u32(context, direction)?;
let tile_rows = to_u32(context, self.schedule.tile_rows)?;
for (first, tiles) in self.schedule.launches() {
let first = to_u32(context, first)?;
let mut builder = stream.launch_builder(&self.recurrence);
builder
.arg(&gates)
.arg(&lens[0])
.arg(&r)
.arg(&lens[1])
.arg(&bias)
.arg(&lens[2])
.arg(&output)
.arg(&state)
.arg(&batch)
.arg(&steps)
.arg(&direction)
.arg(&first)
.arg(&tile_rows)
.arg(&flag_base);
let config = LaunchConfig {
grid_dim: (GROUPS as u32, to_u32(context, tiles)?, 1),
block_dim: (THREADS, 1, 1),
shared_mem_bytes: 0,
};
unsafe { builder.launch_cooperative(config) }?;
}
Ok(())
}
}
struct DirectionLaunch<'a> {
gates: CudaView<'a, f32>,
direction: usize,
output: u64,
state: u64,
flag_base: u32,
}
fn direction_index(direction: Direction) -> usize {
match direction {
Direction::Forward => 0,
Direction::Reverse => 1,
}
}
fn check_len(context: &'static str, expected: usize, actual: usize) -> Result<(), CudaError> {
if expected == actual {
return Ok(());
}
Err(CudaError::BufferLength {
context,
expected,
actual,
})
}
fn to_u32(context: &'static str, value: usize) -> Result<u32, CudaError> {
u32::try_from(value).map_err(|_| CudaError::DimensionOverflow { context, value })
}