use tract_core::internal::natural_strides;
use tract_core::internal::*;
use tract_gpu::device::get_context;
use tract_gpu::ops::change_axes::GpuAxisOp;
use tract_gpu::ops::fused_output::{chain_shape, unchain_strides};
use tract_gpu::ops::pulse::copy_lane;
use tract_gpu::tensor::{DeviceTensor, DeviceTensorExt, IntoDevice};
use tract_gpu::turn_handler::make_tensor_for_node;
use tract_pulse_opl::ops::Delay;
use crate::kernels::matmul::{GgmlGemm, RingAxis};
use crate::ops::CudaGgmlGemm;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CudaRingGemm {
pub delay: Delay,
pub window_axis_ops: TVec<GpuAxisOp>,
}
impl CudaRingGemm {
pub fn slots(&self) -> usize {
self.delay.overlap + 1
}
fn window_fact(&self, slot: &TypedFact) -> TractResult<TypedFact> {
let mut fact = self.delay.output_facts(&[slot])?.remove(0);
for op in &self.window_axis_ops {
op.inner.change_shape(&mut fact.shape, false)?;
}
Ok(fact)
}
}
impl Op for CudaRingGemm {
fn name(&self) -> StaticName {
"CudaRingGemm".into()
}
fn info(&self) -> TractResult<Vec<String>> {
Ok(vec![
format!("axis: {} overlap: {}", self.delay.axis, self.delay.overlap),
format!(
"ring of {} slots, held as: {:?}",
self.slots(),
self.window_axis_ops.iter().map(|o| &o.inner).collect::<Vec<_>>()
),
])
}
op_as_typed_op!();
}
impl EvalOp for CudaRingGemm {
not_out_of_plan!();
fn state(&self, _ctx: &EvalContext) -> TractResult<Option<Box<dyn OpState>>> {
Ok(Some(Box::new(CudaRingGemmState {
ring: None,
slots_view: None,
table: None,
heads: tvec!(),
lanes: 0,
})))
}
}
impl TypedOp for CudaRingGemm {
fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
tract_gpu::utils::facts_to_device_facts(inputs, |facts| {
ensure!(facts.len() == 2);
let window = self.window_fact(facts[1])?;
CudaGgmlGemm.resolve_output_facts(&[facts[0], &window])
})
.with_context(|| format!("Error while computing output facts for {}", self.name()))
}
fn cost(&self, inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
tract_gpu::utils::get_device_facts(inputs, |facts| {
let window = self.window_fact(facts[1])?;
let mut cost = CudaGgmlGemm.cost(&[facts[0], &window])?;
cost.extend(self.delay.cost(&[facts[1]])?);
Ok(cost)
})
}
as_op!();
}
#[derive(Debug, Clone)]
pub struct CudaRingGemmState {
ring: Option<DeviceTensor>,
slots_view: Option<DeviceTensor>,
table: Option<DeviceTensor>,
heads: TVec<usize>,
lanes: usize,
}
impl CudaRingGemmState {
fn init(
&mut self,
op: &CudaRingGemm,
ctx: &EvalContext,
slot: &DeviceTensor,
) -> TractResult<()> {
let axis = op.delay.axis;
let max_lanes = ctx.seating.max_lanes();
let mut slots_shape: TVec<usize> = slot.shape().into();
slots_shape[axis] = op.slots();
let held = chain_shape(&slots_shape, &op.window_axis_ops, ctx.symbols)?;
let (unchained, mut strides) =
unchain_strides(&held, &natural_strides(&held), &op.window_axis_ops, ctx.symbols)?;
ensure!(
unchained == slots_shape,
"The ring's layout leads to {unchained:?}, not {slots_shape:?}"
);
let ring_shape: TVec<usize> = if max_lanes > 1 {
ensure!(axis > 0, "A ring on axis 0 leaves no axis 0 for the lanes");
let lane_span = held.iter().product::<usize>() / slots_shape[0];
ensure!(
slots_shape[0] == 1 || strides[0] as usize == lane_span,
"A ring held as {held:?} gives a stream stride of {}, not the {lane_span} a \
whole stream of it spans",
strides[0]
);
slots_shape[0] = max_lanes;
strides[0] = lane_span as isize;
tvec!(max_lanes, lane_span)
} else {
held
};
let ring = Tensor::zero_dt(slot.datum_type(), &ring_shape)?.into_device()?;
self.slots_view = Some(ring.reshaped(slots_shape)?.restrided(strides)?);
self.ring = Some(ring);
self.heads = tvec!(0; max_lanes);
self.lanes = max_lanes;
Ok(())
}
fn fill_table(
&mut self,
ctx: &EvalContext,
channels: usize,
n: usize,
slot_unit: usize,
) -> TractResult<()> {
let occupancy = ctx.seating.occupancy();
ensure!(channels.is_multiple_of(occupancy), "{channels} channels over {occupancy} seats");
let per_seat = channels / occupancy;
let mut table: Vec<i32> = vec![0; 2 * channels];
for ix in 0..occupancy {
let (seat, lane) = ctx.seating.address(ix);
let head = self.heads[lane.unwrap_or(0)];
for c in 0..per_seat {
let channel = seat.unwrap_or(0) * per_seat + c;
table[2 * channel] = (head * slot_unit) as i32;
table[2 * channel + 1] = ((lane.unwrap_or(0) * per_seat + c) * n) as i32;
}
}
self.table = Some(tensor1(&table).into_device()?);
Ok(())
}
}
impl OpState for CudaRingGemmState {
fn eval(
&mut self,
ctx: &EvalContext,
op: &dyn Op,
inputs: TVec<TValue>,
) -> TractResult<TVec<TValue>> {
let (act, slot) = args_2!(inputs);
let op = op.downcast_ref::<CudaRingGemm>().context("Wrong Op type")?;
let act = act.as_device_tensor().context("Expected a GPU tensor")?;
let slot = slot.as_device_tensor().context("Expected a GPU tensor")?;
let axis = op.delay.axis;
let slots = op.slots();
let max_lanes = ctx.seating.max_lanes();
let occupancy = ctx.seating.occupancy();
if self.ring.is_none() {
self.init(op, ctx, slot)?;
}
ensure!(
self.lanes == max_lanes,
"The ring holds {} lanes, this turn seats {max_lanes} of them",
self.lanes
);
if max_lanes > 1 {
ensure!(
slot.shape()[0] == occupancy,
"The turn's slot carries {} streams, this turn seats {occupancy}",
slot.shape()[0]
);
}
let mut window_shape: TVec<usize> = slot.shape().into();
window_shape[axis] = slots;
let w_shape = chain_shape(&window_shape, &op.window_axis_ops, ctx.symbols)?;
let out_shape = GgmlGemm.output_shape(act.shape(), &w_shape);
let out = make_tensor_for_node(ctx, act.datum_type(), &out_shape)?;
if out.len() == 0 {
return Ok(tvec!(out.into_tensor().into()));
}
let (n, k) = (w_shape[w_shape.len() - 2], w_shape[w_shape.len() - 1]);
let channels: usize = w_shape[..w_shape.len() - 2].iter().product();
let slots_view = self.slots_view.clone().unwrap();
let slot_span = slots_view.strides()[axis] as usize;
let (ring_axis, slot_unit) = if slot_span.is_multiple_of(k) {
ensure!(
slot_span * slots == n * k,
"A slot spans {slot_span} of a {n}x{k} window, which is not a run of whole rows"
);
(RingAxis::Rows, slot_span / k)
} else {
ensure!(
slot_span * slots == k && slot_span.is_multiple_of(2),
"A slot spans {slot_span} of a {n}x{k} window, which is neither a run of \
whole rows nor an even run of every row's columns"
);
(RingAxis::Contracted, slot_span / 2)
};
self.fill_table(ctx, channels, n, slot_unit)?;
let device = get_context()?;
for ix in 0..occupancy {
let (seat, lane) = ctx.seating.address(ix);
let head = self.heads[lane.unwrap_or(0)];
copy_lane(
&*device,
&slots_view,
lane,
(head + slots - 1) % slots,
slot,
seat,
0,
axis,
1,
)?;
self.heads[lane.unwrap_or(0)] = (head + 1) % slots;
}
let ring = self.ring.as_ref().unwrap();
let table = self.table.as_ref().unwrap();
crate::with_cuda_stream(|stream| {
GgmlGemm.dispatch_matvec_ring(stream, act, ring, &out, &w_shape, table, ring_axis)
})?;
Ok(tvec!(out.into_tensor().into()))
}
fn reset_lanes(&mut self, lanes: &[LaneId]) -> TractResult<()> {
let Some(ring) = self.ring.as_ref() else { return Ok(()) };
ensure!(
lanes.iter().all(|l| l.0 < self.lanes),
"The ring holds {} lanes, asked to reset {lanes:?}",
self.lanes
);
for lane in lanes {
self.heads[lane.0] = 0;
}
tract_gpu::ops::pulse::zero_lanes(&*get_context()?, ring, lanes, self.lanes > 1)
}
}