use cubecl::prelude::*;
use crate::launch::Relay;
use crate::*;
#[derive(CubeType)]
pub struct OnlineSoftmax<E: Float> {
m: Array<E>,
l: Array<E>,
#[cube(comptime)]
slices: usize,
#[cube(comptime)]
axis: Axis,
#[cube(comptime)]
slots: SliceSlots,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum SliceSlots {
Every,
Held,
}
#[derive(CubeType)]
pub struct RelayedFactors<E: Float> {
pub sum: Array<E>,
pub carried: Array<E>,
}
#[derive(CubeType)]
pub struct OnlineSoftmaxState<E: Float> {
pub max: Array<E>,
pub sum: Array<E>,
}
#[cube]
impl<E: Float> OnlineSoftmax<E> {
pub fn along(score: &Tile, #[comptime] axis: Axis) -> OnlineSoftmax<E> {
let slices = score.slices_per_unit(axis);
let slots = comptime!(match slices == score.place.space.slices_along(axis) {
true => SliceSlots::Every,
false => SliceSlots::Held,
});
OnlineSoftmax::<E>::empty(slices, axis, slots)
}
pub fn new(#[comptime] slices: usize, #[comptime] axis: Axis) -> OnlineSoftmax<E> {
OnlineSoftmax::<E>::empty(slices, axis, SliceSlots::Every)
}
pub fn step(&mut self, score: &Tile, bias: &Tile, scale: E) -> Array<E> {
let mut slices = score.along(self.axis);
slices.scale_add::<E>(scale, bias);
let max = slices.maxima::<E>(&self.m);
slices.exp_minus::<E>(&max);
let sum = slices.sums::<E>();
let mut correction = Array::<E>::new(self.slices);
#[unroll]
for i in 0..self.slices {
correction[i] = (self.m[i] - max[i]).exp();
self.l[i] = correction[i] * self.l[i] + sum[i];
self.m[i] = max[i];
}
correction
}
pub fn normalizer(&mut self, plane: &Region) -> Array<E> {
let axis = self.axis;
let planes = comptime!(
plane
.path
.planes_along(|along| along == axis)
.iter()
.map(|&(_, count)| count)
.product::<usize>()
);
let mut factor = Array::<E>::new(self.slices);
if comptime!(planes == 1) {
factor = self.recip_l();
} else {
let correction = self.merge_planes(plane.coord(axis), planes);
let recip = self.recip_l();
#[unroll]
for i in 0..self.slices {
factor[i] = correction[i] * recip[i];
}
}
factor
}
fn merge_planes(&mut self, plane: usize, #[comptime] planes: usize) -> Array<E> {
self.refuse_merging("OnlineSoftmax::normalizer");
let slices = self.slices;
let mut shared = Shared::<[E]>::new_slice(comptime!(planes * slices));
let writes = UNIT_POS_PLANE == 0;
let own = plane * slices;
if writes {
#[unroll]
for i in 0..slices {
shared[own + i] = self.m[i];
}
}
sync_cube();
let mut max = Array::<E>::new(slices);
#[unroll]
for i in 0..slices {
max[i] = E::min_value();
#[unroll]
for p in 0..planes {
max[i] = max[i].max(shared[p * slices + i]);
}
}
sync_cube();
let mut correction = Array::<E>::new(slices);
#[unroll]
for i in 0..slices {
correction[i] = (self.m[i] - max[i]).exp();
}
if writes {
#[unroll]
for i in 0..slices {
shared[own + i] = correction[i] * self.l[i];
}
}
sync_cube();
#[unroll]
for i in 0..slices {
let mut sum = E::from_int(0);
#[unroll]
for p in 0..planes {
sum += shared[p * slices + i];
}
self.l[i] = sum;
self.m[i] = max[i];
}
correction
}
pub fn relayed(
&mut self,
relay: &Relay<'_>,
carried_max: &mut Tile,
carried_sum: &mut Tile,
) -> RelayedFactors<E> {
self.refuse_merging("OnlineSoftmax::relayed");
let slices = self.slices;
let (first, last) = (relay.is_first(), relay.is_last());
let stored_max = carried_max.cells::<E>(slices);
let stored_sum = carried_sum.cells::<E>(slices);
let mut own = Array::<E>::new(slices);
let mut carried = Array::<E>::new(slices);
#[unroll]
for i in 0..slices {
let before_max = select(first, E::min_value(), stored_max[i]);
let before_sum = select(first, E::from_int(0), stored_sum[i]);
let merged = before_max.max(self.m[i]);
own[i] = AxisSlices::exp_minus_cell::<E>(self.m[i], merged);
carried[i] = AxisSlices::exp_minus_cell::<E>(before_max, merged);
self.l[i] = carried[i] * before_sum + own[i] * self.l[i];
self.m[i] = merged;
}
carried_max.set_cells(&self.m, slices);
carried_sum.set_cells(&self.l, slices);
let recip = self.recip_l();
#[unroll]
for i in 0..slices {
let normalize = select(last, recip[i], E::from_int(1));
own[i] *= normalize;
carried[i] *= normalize;
}
RelayedFactors::<E> { sum: own, carried }
}
pub fn state(&self) -> OnlineSoftmaxState<E> {
self.refuse_merging("OnlineSoftmax::state");
let mut max = Array::<E>::new(self.slices);
let mut sum = Array::<E>::new(self.slices);
#[unroll]
for i in 0..self.slices {
max[i] = self.m[i];
sum[i] = self.l[i];
}
OnlineSoftmaxState::<E> { max, sum }
}
pub fn store_rows(&self, score: &Tile, max: &mut Tile, sum: &mut Tile) {
if comptime!(self.slots == SliceSlots::Every) {
max.set_cells(&self.m, self.slices);
sum.set_cells(&self.l, self.slices);
} else {
let slices = score.along(self.axis);
slices.store_held::<E>(&self.m, max);
slices.store_held::<E>(&self.l, sum);
}
}
pub fn store_logsumexp(&self, score: &Tile, logsumexp: &mut Tile) {
let mut values = Array::<E>::new(self.slices);
#[unroll]
for i in 0..self.slices {
let live = self.l[i] > E::from_int(0);
values[i] = select(live, self.m[i] + self.l[i].ln(), E::min_value());
}
if comptime!(self.slots == SliceSlots::Every) {
logsumexp.set_cells(&values, self.slices);
} else {
score.along(self.axis).store_held::<E>(&values, logsumexp);
}
}
pub fn recip_l(&self) -> Array<E> {
let mut recip = Array::<E>::new(self.slices);
#[unroll]
for i in 0..self.slices {
let live = self.l[i] > E::from_int(0);
recip[i] = select(live, self.l[i].recip(), E::from_int(0));
}
recip
}
fn empty(
#[comptime] slices: usize,
#[comptime] axis: Axis,
#[comptime] slots: SliceSlots,
) -> OnlineSoftmax<E> {
let mut m = Array::<E>::new(slices);
let mut l = Array::<E>::new(slices);
#[unroll]
for i in 0..slices {
m[i] = E::min_value();
l[i] = E::from_int(0);
}
OnlineSoftmax::<E> {
m,
l,
slices,
axis,
slots,
}
}
fn refuse_merging(&self, #[comptime] op: &'static str) {
comptime!(assert!(
self.slots == SliceSlots::Every,
"{op}: this state was taken in from a manual-mma grid, each unit keeping the rows it \
holds cells of; a state merged with another holder's is taken in from a window or a \
cmma grid, or written out row by row first (OnlineSoftmax::store_rows)"
));
}
}