pub(super) mod layout;
use cudarc::driver::sys::CUfunction_attribute;
use cudarc::driver::{
CudaFunction, CudaSlice, CudaStream, CudaView, CudaViewMut, DevicePtr, DevicePtrMut,
DeviceRepr, LaunchConfig, PushKernelArg, ValidAsZeroBits,
};
use self::layout::{
AlignedSpan, ExchangeLayout, GATE_COLUMNS, GROUPS, HIDDEN, KERNEL, ProjectionLayout,
RESIDENT_KERNEL, RESIDENT_SHARED_BYTES, RESIDENT_STATIC_SHARED_BYTES, RESIDENT_THREADS,
STATE_TILE, Schedule, TENSOR_SHARED_BYTES, THREADS, TILED_GROUPS, TILED_KERNEL, TILED_ROWS,
TILED_THREADS, pack_bias, pack_directions, pack_input_directions, padded_input, tiled_exchange,
};
use super::{
Batches, Coverage, CoverageEntry, DeviceAttributes, Direction, FiniteContract, GeometryError,
InfinityContract, LstmCandidate, LstmPhases, LstmPin, LstmProjection, LstmSpec, Maths,
NanContract, Op, PlanError, Scratch, SideStream, SignedZeroContract, SpecialValues,
};
use crate::inference::cuda::{
ComputeCapability, CudaError, CudaMath, CudaRuntime, KernelModule, LoadedKernels, PtxTier,
};
const SPK_LSTM_CLEAR: &str = "spk_lstm_clear";
const SPK_LSTM_PROJECTION: &str = "spk_lstm_projection";
const SPK_LSTM_PROJECTION_SMALL: &str = "spk_lstm_projection_small";
const SPK_LSTM_PROJECTION_TF32: &str = "spk_lstm_projection_tf32";
pub(crate) const REQUIRED_KERNELS: [&str; 7] = [
KERNEL,
TILED_KERNEL,
RESIDENT_KERNEL,
SPK_LSTM_CLEAR,
SPK_LSTM_PROJECTION,
SPK_LSTM_PROJECTION_SMALL,
SPK_LSTM_PROJECTION_TF32,
];
const FEATURES: usize = 60;
const DIRECTIONS: [Direction; 2] = [Direction::Forward, Direction::Reverse];
const SMALL_PROJECTION_ROWS: usize = 2048;
const TILED_MIN_BATCH: usize = 9;
const RESIDENT_CAPABILITY: ComputeCapability = ComputeCapability::new(8, 0);
const RESIDENT_MIN_BATCH: usize = 2;
#[derive(Debug)]
struct PackedLayer {
input: usize,
w: CudaSlice<f32>,
r: AlignedScratch<f32>,
bias: CudaSlice<f32>,
}
#[derive(Debug)]
pub(crate) struct Oxide {
projection: InputProjection,
recurrence: CudaFunction,
clear: CudaFunction,
layers: [PackedLayer; 4],
batch: usize,
steps: usize,
plan: RecurrencePlan,
gates: AlignedScratch<f32>,
state: Option<Exchange>,
side: SideStream,
}
impl LstmCandidate for Oxide {
const COVERAGE: Coverage = Coverage(&[CoverageEntry {
layers: &["lstm.stack"],
batches: Batches::All,
maths: Maths::All,
}]);
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::Projected(fp32_projection(
spec.batch * spec.frames,
)))
}
fn device_pin(
device: &DeviceAttributes,
tier: PtxTier,
spec: &LstmSpec<'_>,
) -> Result<LstmPin, PlanError> {
Ok(LstmPin::Projected(projection_rule(
ProjectionTarget {
capability: device.capability(),
shared_optin_bytes: device.shared_optin_bytes(),
tier,
},
spec.batch,
spec.batch * spec.frames,
spec.math,
)))
}
fn plan(
runtime: &CudaRuntime,
kernels: &LoadedKernels,
spec: LstmSpec<'_>,
pin: LstmPin,
) -> Result<Self, PlanError> {
let recurrence = RecurrencePlan::select(runtime, kernels, spec.batch)?;
Self::with_recurrence(runtime, kernels, spec, pin, recurrence)
}
fn enqueue(
&self,
input: &CudaView<'_, f32>,
output: &mut CudaViewMut<'_, f32>,
phases: &LstmPhases<'_>,
stream: &CudaStream,
) -> Result<(), CudaError> {
let rows = self.batch * self.steps;
check_len("lstmproj input", rows * FEATURES, input.len())?;
check_len("lstmproj 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, |_| {
let d = direction_index(direction);
let weights = GATE_COLUMNS * padded_input(layer.input);
let w = layer.w.slice(d * weights..(d + 1) * weights);
let mut storage = self.gates.storage.get();
let mut gates = storage.slice_mut(self.gates.span.range());
let mut gates =
gates.slice_mut(d * rows * GATE_COLUMNS..(d + 1) * rows * GATE_COLUMNS);
let previous = output.as_view();
let x = if index == 0 { input } else { &previous };
self.projection
.enqueue(stream, x, &w, &mut gates, rows, layer.input)
})?;
}
self.recur(index, layer, output, phases, stream)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub(super) struct ProjectionTarget {
pub(super) capability: ComputeCapability,
pub(super) shared_optin_bytes: u32,
pub(super) tier: PtxTier,
}
pub(super) fn projection_rule(
target: ProjectionTarget,
batch: usize,
rows: usize,
math: CudaMath,
) -> LstmProjection {
let tensor = math == CudaMath::Tf32
&& rows >= SMALL_PROJECTION_ROWS
&& target.tier >= PtxTier::Sm80
&& target.shared_optin_bytes >= TENSOR_SHARED_BYTES
&& tf32_projection_accurate(target.capability, batch);
if tensor {
return LstmProjection::Tensor;
}
fp32_projection(rows)
}
fn fp32_projection(rows: usize) -> LstmProjection {
if rows < SMALL_PROJECTION_ROWS {
LstmProjection::Small
} else {
LstmProjection::Large
}
}
fn tf32_projection_accurate(capability: ComputeCapability, batch: usize) -> bool {
if capability == ComputeCapability::new(12, 0) {
return false;
}
!(capability == ComputeCapability::new(8, 9) && batch == 32)
}
impl Oxide {
pub(crate) fn with_recurrence(
runtime: &CudaRuntime,
kernels: &LoadedKernels,
spec: LstmSpec<'_>,
pin: LstmPin,
plan: RecurrencePlan,
) -> Result<Self, PlanError> {
let LstmPin::Projected(projection) = pin else {
return Err(PlanError::Geometry(GeometryError::Invalid {
context: "lstmproj plan",
reason: format!("pin {pin:?} belongs to the lstm area"),
}));
};
let projection = InputProjection::new(runtime.device(), kernels, projection)?;
let clear = kernels.function(SPK_LSTM_CLEAR)?;
let recurrence = plan.function(kernels)?;
if plan == RecurrencePlan::Resident {
recurrence.set_attribute(
CUfunction_attribute::CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES,
RESIDENT_SHARED_BYTES as i32,
)?;
}
let stream = runtime.stream();
let layout = projection.layout();
let pack = |index: usize| -> Result<PackedLayer, CudaError> {
let layer = spec.layers[index];
let input = if index == 0 { FEATURES } else { 2 * HIDDEN };
check_len("lstmproj input width", input, layer.input)?;
check_len("lstmproj W", 2 * GATE_COLUMNS * input, layer.w.len())?;
check_len("lstmproj R", 2 * GATE_COLUMNS * HIDDEN, layer.r.len())?;
check_len("lstmproj B", 4 * GATE_COLUMNS, layer.b.len())?;
Ok(PackedLayer {
input,
w: stream.clone_htod(&pack_input_directions(layer.w, input, layout))?,
r: AlignedScratch::from_host(runtime, &pack_directions(layer.r, HIDDEN))?,
bias: stream.clone_htod(&pack_bias(layer.b))?,
})
};
Ok(Self {
projection,
recurrence,
clear,
layers: [pack(0)?, pack(1)?, pack(2)?, pack(3)?],
batch: spec.batch,
steps: spec.frames,
plan,
gates: AlignedScratch::zeros(runtime, 2 * spec.batch * spec.frames * GATE_COLUMNS)?,
state: plan
.exchange_layout()
.map(|layout| Exchange::new(runtime, layout))
.transpose()?,
side: SideStream::new(runtime)?,
})
}
fn clear_state(&self, stream: &CudaStream) -> Result<(), CudaError> {
let Some(exchange) = &self.state else {
return Ok(());
};
let mut storage = exchange.buffer.storage.get();
let mut state = storage.slice_mut(exchange.buffer.span.range());
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("lstmproj clear", len)?)) }?;
Ok(())
}
fn recur(
&self,
index: usize,
layer: &PackedLayer,
output: &mut CudaViewMut<'_, f32>,
phases: &LstmPhases<'_>,
stream: &CudaStream,
) -> Result<(), CudaError> {
let context = "lstmproj recurrence";
let rows = self.batch * self.steps;
let Some(exchange) = &self.state else {
return phases.recurrence(index, Direction::Forward, || {
self.launch_resident(stream, layer, output)
});
};
let mut storage = exchange.buffer.storage.get();
let mut state = storage.slice_mut(exchange.buffer.span.range());
let gate_storage = self.gates.storage.get();
let gates = gate_storage.slice(self.gates.span.range());
let (output, _output_record) = output.device_ptr_mut(stream);
let (state, _state_record) = state.device_ptr_mut(stream);
let concurrent = self.plan.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 + (exchange.layout.direction(d) * 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 = "lstmproj recurrence";
let DirectionLaunch {
gates,
direction,
output,
state,
flag_base,
} = launch;
let recurrent = GATE_COLUMNS * HIDDEN;
let r_storage = layer.r.storage.get();
let r = r_storage.slice(layer.r.span.range());
let r = 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 schedule = match self.plan {
RecurrencePlan::Wide(schedule) => schedule,
RecurrencePlan::Resident => {
return Err(CudaError::Unsupported {
context,
reason: "the single-block recurrence has no per-direction launch".to_owned(),
});
}
RecurrencePlan::Tiled { tiles } => {
let first = 0u32;
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(&flag_base);
let config = LaunchConfig {
grid_dim: (TILED_GROUPS as u32, to_u32(context, tiles)?, 1),
block_dim: (TILED_THREADS, 1, 1),
shared_mem_bytes: 0,
};
unsafe { builder.launch_cooperative(config) }?;
return Ok(());
}
};
let tile_rows = to_u32(context, schedule.tile_rows)?;
for (first, tiles) in 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(())
}
}
impl Oxide {
fn launch_resident(
&self,
stream: &CudaStream,
layer: &PackedLayer,
output: &mut CudaViewMut<'_, f32>,
) -> Result<(), CudaError> {
let context = "lstmproj resident recurrence";
let gate_storage = self.gates.storage.get();
let gates = gate_storage.slice(self.gates.span.range());
let r_storage = layer.r.storage.get();
let r = r_storage.slice(layer.r.span.range());
let bias = layer.bias.as_view();
let lens = [gates.len(), r.len(), bias.len()].map(|len| len as u64);
let (output, _output_record) = output.device_ptr_mut(stream);
let reverse = output + (HIDDEN * size_of::<f32>()) as u64;
let batch = to_u32(context, self.batch)?;
let steps = to_u32(context, self.steps)?;
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(&reverse)
.arg(&batch)
.arg(&steps);
let config = LaunchConfig {
grid_dim: (batch, 2, 1),
block_dim: (RESIDENT_THREADS, 1, 1),
shared_mem_bytes: RESIDENT_SHARED_BYTES,
};
unsafe { builder.launch(config) }?;
Ok(())
}
}
#[derive(Debug)]
struct AlignedScratch<T> {
storage: Scratch<T>,
span: AlignedSpan<T>,
}
impl<T: DeviceRepr + ValidAsZeroBits> AlignedScratch<T> {
fn from_host(runtime: &CudaRuntime, values: &[T]) -> Result<Self, CudaError> {
let buffer = Self::zeros(runtime, values.len())?;
{
let mut storage = buffer.storage.get();
let mut view = storage.slice_mut(buffer.span.range());
runtime.stream().memcpy_htod(values, &mut view)?;
}
Ok(buffer)
}
fn zeros(runtime: &CudaRuntime, len: usize) -> Result<Self, CudaError> {
let storage = Scratch::zeros(runtime, AlignedSpan::<T>::allocation_len(len))?;
let span = {
let allocation = storage.get();
let (address, _record) = allocation.device_ptr(runtime.stream());
AlignedSpan::new(address, len)
};
Ok(Self { storage, span })
}
}
#[derive(Debug)]
struct Exchange {
buffer: AlignedScratch<u64>,
layout: ExchangeLayout,
}
impl Exchange {
fn new(runtime: &CudaRuntime, layout: ExchangeLayout) -> Result<Self, CudaError> {
let buffer = AlignedScratch::zeros(runtime, layout.words())?;
Ok(Self { buffer, layout })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RecurrencePlan {
Resident,
Wide(Schedule),
Tiled { tiles: usize },
}
impl RecurrencePlan {
pub(crate) fn select(
runtime: &CudaRuntime,
kernels: &LoadedKernels,
batch: usize,
) -> Result<Self, PlanError> {
if batch >= RESIDENT_MIN_BATCH && Self::resident_fits(runtime, kernels)? {
return Ok(Self::Resident);
}
if let Some(plan) = Self::tiled(batch, Self::tiled_budget(runtime, kernels)?) {
return Ok(plan);
}
Self::wide(runtime, kernels, batch)
}
pub(crate) fn resident_fits(
runtime: &CudaRuntime,
kernels: &LoadedKernels,
) -> Result<bool, PlanError> {
let device = runtime.device();
if !Self::resident_device(device.capability(), device.shared_optin_bytes()) {
return Ok(false);
}
let resident = kernels.function(RESIDENT_KERNEL)?;
resident.set_attribute(
CUfunction_attribute::CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES,
RESIDENT_SHARED_BYTES as i32,
)?;
let per_sm = resident.occupancy_max_active_blocks_per_multiprocessor(
RESIDENT_THREADS,
RESIDENT_SHARED_BYTES as usize,
None,
)?;
Ok(per_sm >= 1)
}
pub(crate) fn resident_device(capability: ComputeCapability, shared_optin_bytes: u32) -> bool {
capability == RESIDENT_CAPABILITY
&& shared_optin_bytes >= RESIDENT_SHARED_BYTES + RESIDENT_STATIC_SHARED_BYTES
}
pub(crate) fn tiled_budget(
runtime: &CudaRuntime,
kernels: &LoadedKernels,
) -> Result<Option<usize>, PlanError> {
let tiled = kernels.function(TILED_KERNEL)?;
let capacity = runtime.cooperative_capacity(&tiled, TILED_THREADS, 0)?;
Ok(runtime
.concurrent_cooperative_capacity(&tiled, TILED_THREADS, 0)?
.map(|joint| joint.min(capacity)))
}
pub(crate) fn wide(
runtime: &CudaRuntime,
kernels: &LoadedKernels,
batch: usize,
) -> Result<Self, PlanError> {
let wide = kernels.function(KERNEL)?;
let capacity = runtime.cooperative_capacity(&wide, THREADS, 0)?;
let concurrent_capacity = runtime.concurrent_cooperative_capacity(&wide, THREADS, 0)?;
let schedule = Schedule::for_groups(GROUPS, batch, capacity, concurrent_capacity)?;
Ok(Self::Wide(schedule))
}
fn function(self, kernels: &LoadedKernels) -> Result<CudaFunction, PlanError> {
let name = match self {
Self::Wide(_) => KERNEL,
Self::Tiled { .. } => TILED_KERNEL,
Self::Resident => RESIDENT_KERNEL,
};
Ok(kernels.function(name)?)
}
pub(crate) fn tiled(batch: usize, budget: Option<usize>) -> Option<Self> {
if batch < TILED_MIN_BATCH {
return None;
}
Self::tiled_fit(batch, budget)
}
pub(crate) fn tiled_fit(batch: usize, budget: Option<usize>) -> Option<Self> {
let tiles = batch.div_ceil(TILED_ROWS);
let fits = budget.is_some_and(|budget| 2 * TILED_GROUPS * tiles <= budget);
fits.then_some(Self::Tiled { tiles })
}
fn concurrent(self) -> bool {
match self {
Self::Wide(schedule) => schedule.concurrent,
Self::Tiled { .. } => true,
Self::Resident => false,
}
}
fn exchange_layout(self) -> Option<ExchangeLayout> {
match self {
Self::Wide(schedule) => Some(ExchangeLayout::new(schedule.tiles, STATE_TILE)),
Self::Tiled { tiles } => Some(ExchangeLayout::new(tiles, tiled_exchange::TILE)),
Self::Resident => None,
}
}
}
#[derive(Debug)]
struct InputProjection {
function: CudaFunction,
projection: LstmProjection,
}
impl InputProjection {
fn new(
device: &DeviceAttributes,
kernels: &LoadedKernels,
projection: LstmProjection,
) -> Result<Self, PlanError> {
let name = match projection {
LstmProjection::Small => SPK_LSTM_PROJECTION_SMALL,
LstmProjection::Large => SPK_LSTM_PROJECTION,
LstmProjection::Tensor => SPK_LSTM_PROJECTION_TF32,
};
let function = kernels.function(name)?;
if projection != LstmProjection::Tensor {
return Ok(Self {
function,
projection,
});
}
if kernels.tier() < PtxTier::Sm80 {
return Err(PlanError::DeviceUnsupported {
reason: format!(
"tensor projections need the sm80 tier, but {} is loaded",
kernels.tier()
),
});
}
if device.shared_optin_bytes() < TENSOR_SHARED_BYTES {
return Err(PlanError::DeviceUnsupported {
reason: format!(
"tensor projections need {TENSOR_SHARED_BYTES} shared bytes per block, \
but the GPU allows {}",
device.shared_optin_bytes()
),
});
}
function.set_attribute(
CUfunction_attribute::CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES,
TENSOR_SHARED_BYTES as i32,
)?;
Ok(Self {
function,
projection,
})
}
fn layout(&self) -> ProjectionLayout {
match self.projection {
LstmProjection::Tensor => ProjectionLayout::Tensor,
LstmProjection::Small | LstmProjection::Large => ProjectionLayout::Simt,
}
}
fn enqueue(
&self,
stream: &CudaStream,
input: &CudaView<'_, f32>,
weights: &CudaView<'_, f32>,
output: &mut CudaViewMut<'_, f32>,
rows: usize,
columns: usize,
) -> Result<(), CudaError> {
let (tile_columns, tile_rows, shared_mem_bytes) = match self.projection {
LstmProjection::Small => (64, 64, 0),
LstmProjection::Large => (128, 128, 0),
LstmProjection::Tensor => (256, 128, TENSOR_SHARED_BYTES),
};
let dimensions = [
to_u32("lstmproj projection rows", rows)?,
to_u32("lstmproj projection width", columns)?,
to_u32("lstmproj projection padded width", padded_input(columns))?,
];
let config = LaunchConfig {
grid_dim: (
GATE_COLUMNS as u32 / tile_columns,
dimensions[0].div_ceil(tile_rows),
1,
),
block_dim: (256, 1, 1),
shared_mem_bytes,
};
let mut builder = stream.launch_builder(&self.function);
builder.arg(input).arg(weights).arg(output);
for dimension in &dimensions {
builder.arg(dimension);
}
unsafe { builder.launch(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 })
}
impl super::DriverCandidate for Oxide {
const AREA: KernelModule = KernelModule::LstmProj;
fn driver_coverage(
tier: PtxTier,
_device: &DeviceAttributes,
_fp16: super::Fp16Policy,
) -> Coverage {
Self::coverage(tier)
}
fn speed_scope(
_boundary: crate::inference::cuda::implementation::BoundaryId,
batch: usize,
_math: CudaMath,
device: &DeviceAttributes,
_tier: PtxTier,
) -> Option<crate::inference::cuda::implementation::SpeedScope> {
let capability = device.capability();
([1, 32].contains(&batch)
&& [ComputeCapability::new(8, 9), ComputeCapability::new(12, 0)].contains(&capability))
.then_some(
crate::inference::cuda::implementation::SpeedScope::MeasuredCapability { capability },
)
}
fn speed_summary(_math: CudaMath) -> &'static str {
"do-lstm dev: library-free projection and recurrence faster on Ada 8.9 and Blackwell 12.0 at b1/b32 in both maths"
}
fn driver_pin(
_boundary: crate::inference::cuda::implementation::BoundaryId,
batch: usize,
math: CudaMath,
device: &DeviceAttributes,
tier: PtxTier,
_fp16: super::Fp16Policy,
) -> Result<super::ConfigPin, PlanError> {
Ok(super::ConfigPin::Lstm(LstmPin::Projected(projection_rule(
ProjectionTarget {
capability: device.capability(),
shared_optin_bytes: device.shared_optin_bytes(),
tier,
},
batch,
batch * 589,
math,
))))
}
}