use std::panic::Location;
use crate::{Array, DType, OpErrorKind, Result, ops};
fn check(op: &str, rows: &Array, draft: &Array, eos: &Array) -> std::result::Result<i32, String> {
if rows.ndim() != 2 || !matches!(rows.dtype(), DType::F32 | DType::F16 | DType::BF16) {
return Err(format!(
"{op}: the verification rows must be [N + 1, vocab] floats, got {:?} {}",
rows.shape(),
rows.dtype().name()
));
}
for (name, a) in [("draft", draft), ("eos", eos)] {
if a.ndim() != 1 || !matches!(a.dtype(), DType::U32 | DType::I32) {
return Err(format!(
"{op}: `{name}` must be a [n] uint32 or int32 array, got {:?} {}",
a.shape(),
a.dtype().name()
));
}
}
let n = draft.shape()[0];
if rows.shape()[0] != n + 1 {
return Err(format!(
"{op}: {} rows for a draft of {n} tokens (N + 1 needed)",
rows.shape()[0]
));
}
Ok(n)
}
fn is_eos(tokens: &Array, eos: &Array) -> Array {
let t = tokens.astype(DType::U32).reshape([-1, 1]);
let e = eos.astype(DType::U32).reshape([1, -1]);
t.equal(&e).any(-1)
}
fn finish(ok: &Array, next: &Array, draft: &Array, eos: &Array) -> Array {
let prefix = ok.astype(DType::U32).cumprod(0);
let eos_in = prefix.multiply(is_eos(draft, eos).astype(DType::U32));
let before = eos_in.cumsum(0).subtract(&eos_in); let keep = prefix.multiply(before.equal(0u32).astype(DType::U32));
let accepted = keep.sum(..).astype(DType::U32);
let chosen = next
.take(&accepted.reshape([1]), 0)
.squeeze(..)
.astype(DType::U32);
let hit = eos_in
.multiply(&keep)
.sum(..)
.greater(0u32)
.add(is_eos(&chosen.reshape([1]), eos).squeeze(..)) .astype(DType::U32);
ops::stack(&[accepted, chosen, hit], 0)
}
#[track_caller]
pub fn accept_greedy(logits: &Array, draft: &Array, eos: &Array) -> Array {
let at = Location::caller();
Array::sticky("accept_greedy", at, &[logits, draft, eos], || {
let n = check("accept_greedy", logits, draft, eos).map_err(invalid)?;
let choice = logits.argmax(-1).astype(DType::U32);
let ok = draft.astype(DType::U32).equal(choice.i(0..n));
Ok(finish(&ok, &choice, draft, eos))
})
}
fn invalid(m: String) -> crate::Error {
crate::Error::Op {
op: "speculative",
at: Some(Location::caller()),
kind: OpErrorKind::InvalidArgument(m),
}
}
#[track_caller]
pub fn accept_sampled(
probs: &Array,
draft: &Array,
draft_probs: Option<&Array>,
eos: &Array,
key: &Array,
) -> Array {
let at = Location::caller();
let mut inputs = vec![probs, draft, eos, key];
inputs.extend(draft_probs);
Array::sticky("accept_sampled", at, &inputs, || {
let n = check("accept_sampled", probs, draft, eos).map_err(invalid)?;
let v = probs.shape()[1];
let p = probs.astype(DType::F32);
let d = draft.astype(DType::U32).reshape([n, 1]);
let q = match draft_probs {
Some(q) => {
if q.shape() != [n, v] {
return Err(invalid(format!(
"accept_sampled: `draft_probs` must be [{n}, {v}], got {:?}",
q.shape()
)));
}
q.astype(DType::F32)
}
None => ops::arange(0, v, 1)
.reshape([1, v])
.equal(&d)
.astype(DType::F32),
};
let [uniform_key, next_key] = crate::random::split_n::<2>(key);
let p_draft = p.i(0..n).take_along_axis(&d, -1).reshape([n]);
let q_draft = q.take_along_axis(&d, -1).reshape([n]);
let u = crate::random::uniform(0.0, 1.0, [n], DType::F32, &uniform_key);
let ok = u.multiply(&q_draft).less(&p_draft);
let residual = p.i(0..n).subtract(&q).maximum(0.0);
let mass = residual.sum_keepdims(-1);
let residual = mass
.greater(0.0)
.select(residual.divide(mass.maximum(f32::MIN_POSITIVE)), p.i(0..n));
let rows = ops::concatenate(&[residual, p.i(n..n + 1)], 0);
let keys = crate::random::split(&next_key, (n + 1) as usize);
let next = ops::stack(
&(0..=n)
.map(|i| {
crate::random::categorical(&rows.i(i).log(), -1, &keys.i(i)).astype(DType::U32)
})
.collect::<Vec<_>>(),
0,
);
Ok(finish(&ok, &next, draft, eos))
})
}
pub fn read(result: &Array) -> Result<(usize, u32, bool)> {
match result.to_vec::<u32>()?.as_slice() {
&[accepted, next, eos] => Ok((accepted as usize, next, eos != 0)),
v => Err(crate::Error::InvalidArgument(format!(
"speculative::read: an acceptance result has 3 values, got {}",
v.len()
))),
}
}