use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use metrale_gpu_runtime::gpu::DevicePtr;
use metrale_model_engine::traits::{Model, SequenceState};
use metrale_scheduler::{
DecodeRows, DeviceError, DeviceFacts, DeviceIo, EffectOutcome, Readback, Ticket,
};
use super::sync_device::SyncDeviceIo;
use super::{Effect, StepOutcome, StepPlan, StepResult};
use crate::scheduler::{LoraAck, LoraCommand};
pub const RING_SLOTS: usize = 3;
const POLL_SPINS: usize = 2_000;
#[derive(Debug, Default)]
pub struct AsyncRouterStats {
pub ahead_launches: AtomicU64,
pub reserved_blocks: AtomicU64,
pub reserved_blocks_max_inflight: AtomicU64,
pub rollbacks: AtomicU64,
pub rollback_released_blocks: AtomicU64,
}
impl AsyncRouterStats {
pub fn snapshot(&self) -> std::collections::BTreeMap<String, f64> {
[
("ahead_launches", &self.ahead_launches),
("reserved_blocks", &self.reserved_blocks),
(
"reserved_blocks_max_inflight",
&self.reserved_blocks_max_inflight,
),
("rollbacks", &self.rollbacks),
("rollback_released_blocks", &self.rollback_released_blocks),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v.load(Ordering::Relaxed) as f64))
.collect()
}
}
pub static STATS: AsyncRouterStats = AsyncRouterStats {
ahead_launches: AtomicU64::new(0),
reserved_blocks: AtomicU64::new(0),
reserved_blocks_max_inflight: AtomicU64::new(0),
rollbacks: AtomicU64::new(0),
rollback_released_blocks: AtomicU64::new(0),
};
struct LedgerEntry {
ticket: Ticket,
awaited: bool,
rows: Vec<(usize, usize)>,
}
struct InFlight {
slot: usize,
rows: usize,
logits: DevicePtr,
fed: bool,
}
pub struct AsyncDeviceIo {
inner: SyncDeviceIo,
max_rows: usize,
ring: *mut u8,
ring_bytes: usize,
events: [u64; RING_SLOTS],
busy: RefCell<VecDeque<usize>>,
in_flight: RefCell<HashMap<u32, InFlight>>,
next_ticket: Cell<u32>,
pending_reserve: RefCell<Vec<(usize, usize)>>,
ledger: RefCell<VecDeque<LedgerEntry>>,
last_device_argmax: Cell<bool>,
}
unsafe impl Send for AsyncDeviceIo {}
impl AsyncDeviceIo {
pub fn new(model: Arc<dyn Model>, max_rows: usize) -> anyhow::Result<Self> {
if !model.supports_device_token_feed() {
anyhow::bail!("the model has no device token feed");
}
let max_rows = max_rows.max(1);
let ring_bytes = RING_SLOTS * max_rows * 4;
let ring = model.alloc_host_pinned(ring_bytes)?;
let mut events = [0u64; RING_SLOTS];
for e in events.iter_mut() {
*e = model.create_event()?;
}
Ok(Self {
inner: SyncDeviceIo::new(model),
max_rows,
ring,
ring_bytes,
events,
busy: RefCell::new(VecDeque::new()),
in_flight: RefCell::new(HashMap::new()),
next_ticket: Cell::new(0),
pending_reserve: RefCell::new(Vec::new()),
ledger: RefCell::new(VecDeque::new()),
last_device_argmax: Cell::new(false),
})
}
fn slot_ptr(&self, slot: usize) -> *mut u32 {
unsafe { self.ring.add(slot * self.max_rows * 4) as *mut u32 }
}
fn take_slot(&self) -> Result<usize, DeviceError> {
let busy = self.busy.borrow();
let slot = (0..RING_SLOTS).find(|s| !busy.contains(s)).ok_or_else(|| {
DeviceError::Forward(anyhow::anyhow!(
"readback ring exhausted: {RING_SLOTS} steps unread"
))
})?;
drop(busy);
self.busy.borrow_mut().push_back(slot);
Ok(slot)
}
fn issue(
&self,
logits: DevicePtr,
masks: &[metrale_scheduler::RowMask],
fed: bool,
device_argmax: bool,
) -> Result<Ticket, DeviceError> {
let n = masks.len();
if n > self.max_rows {
return Err(DeviceError::Readback(anyhow::anyhow!(
"{n} rows exceed the readback ring width {}",
self.max_rows
)));
}
let slot = self.take_slot()?;
if device_argmax {
let dst =
unsafe { std::slice::from_raw_parts_mut(self.slot_ptr(slot) as *mut u8, n * 4) };
self.inner
.model()
.device_argmax_to_host(dst, Some(self.events[slot]))
.map_err(DeviceError::Readback)?;
} else {
self.inner
.model()
.argmax_batch_to_feed(logits, masks, self.slot_ptr(slot), self.events[slot], 0)
.map_err(DeviceError::Readback)?;
}
let ticket = Ticket(self.next_ticket.get());
self.next_ticket.set(ticket.0.wrapping_add(1));
self.in_flight.borrow_mut().insert(
ticket.0,
InFlight {
slot,
rows: n,
logits,
fed,
},
);
self.attribute_reservations(ticket);
Ok(ticket)
}
fn attribute_reservations(&self, ticket: Ticket) {
let mut ledger = self.ledger.borrow_mut();
ledger.retain(|e| !e.awaited);
let rows = std::mem::take(&mut *self.pending_reserve.borrow_mut());
ledger.push_back(LedgerEntry {
ticket,
awaited: false,
rows,
});
let held: usize = ledger
.iter()
.flat_map(|e| e.rows.iter().map(|(_, b)| b))
.sum();
STATS
.reserved_blocks_max_inflight
.fetch_max(held as u64, Ordering::Relaxed);
}
fn arm(&self, masks: &[metrale_scheduler::RowMask]) -> Result<bool, DeviceError> {
let armed = self
.inner
.model()
.arm_device_argmax(masks)
.map_err(DeviceError::Forward)?;
self.last_device_argmax.set(armed);
Ok(armed)
}
fn disarm(&self, e: anyhow::Error) -> DeviceError {
if let Err(d) = self.inner.model().arm_device_argmax(&[]) {
tracing::error!("device argmax disarm: {d:#}");
}
self.last_device_argmax.set(false);
DeviceError::Forward(e)
}
fn wait(&self, event: u64) -> Result<(), DeviceError> {
let m = self.inner.model();
for _ in 0..POLL_SPINS {
if m.event_query(event).map_err(DeviceError::Readback)? {
return Ok(());
}
std::hint::spin_loop();
}
m.event_synchronize(event).map_err(DeviceError::Readback)
}
}
impl DeviceIo for AsyncDeviceIo {
type Seq = SequenceState;
type Ptr = DevicePtr;
type Model = dyn Model + 'static;
type LoraCmd = LoraCommand;
type LoraAck = LoraAck;
fn model(&self) -> &(dyn Model + 'static) {
self.inner.model()
}
fn facts(&self) -> DeviceFacts {
self.inner.facts()
}
fn max_depth(&self) -> usize {
2
}
fn launch(
&self,
plan: StepPlan<'_>,
rows: &mut [&mut SequenceState],
) -> Result<Ticket, DeviceError> {
match plan {
StepPlan::Decode {
tokens,
ctx,
readback: Readback::ArgmaxMasked { masks },
} => {
let m = self.inner.model();
let armed = self.arm(&masks)?;
let logits = m
.decode_batch(&tokens, rows, 0)
.map_err(|e| self.disarm(e))?;
super::sync_device::commit_decode_ctx(m, ctx, rows);
self.issue(logits, &masks, false, armed)
}
StepPlan::DecodeFed {
sources,
masks,
ctx,
} => {
let m = self.inner.model();
let armed = self.arm(&masks)?;
let logits = m
.decode_batch_fed(&sources, rows, 0)
.map_err(|e| self.disarm(e))?;
super::sync_device::commit_decode_ctx(m, ctx, rows);
STATS.ahead_launches.fetch_add(1, Ordering::Relaxed);
self.issue(logits, &masks, true, armed)
}
other => {
self.last_device_argmax.set(false);
let ticket = self.inner.launch(other, rows)?;
self.next_ticket
.set(self.next_ticket.get().max(ticket.0.wrapping_add(1)));
Ok(ticket)
}
}
}
fn await_result(&self, ticket: Ticket) -> Result<StepResult, DeviceError> {
let Some(step) = self.in_flight.borrow_mut().remove(&ticket.0) else {
return self.inner.await_result(ticket);
};
let waited = self.wait(self.events[step.slot]);
self.busy.borrow_mut().retain(|s| *s != step.slot);
if let Some(entry) = self
.ledger
.borrow_mut()
.iter_mut()
.find(|e| e.ticket == ticket)
{
entry.awaited = true;
}
if step.fed {
self.inner.model().fed_step_settled();
}
waited?;
let ids =
unsafe { std::slice::from_raw_parts(self.slot_ptr(step.slot), step.rows) }.to_vec();
Ok(StepResult {
ticket,
outcome: StepOutcome::Decode {
logits: step.logits,
rows: DecodeRows::Tokens(ids),
},
})
}
fn apply(&self, effect: Effect<'_>) -> Result<EffectOutcome, DeviceError> {
match effect {
Effect::ReserveKv { seq } => {
let slot_idx = seq.slot_idx;
let out = self.inner.apply(Effect::ReserveKv { seq })?;
if let EffectOutcome::Reserved { blocks } = out {
STATS
.reserved_blocks
.fetch_add(blocks as u64, Ordering::Relaxed);
self.pending_reserve.borrow_mut().push((slot_idx, blocks));
}
Ok(out)
}
Effect::Rollback { seq } => {
let blocks = {
let mut ledger = self.ledger.borrow_mut();
ledger
.iter_mut()
.rev()
.find_map(|e| {
e.rows
.iter()
.position(|(s, _)| *s == seq.slot_idx)
.map(|i| e.rows.remove(i).1)
})
.unwrap_or(0)
};
STATS.rollbacks.fetch_add(1, Ordering::Relaxed);
STATS
.rollback_released_blocks
.fetch_add(blocks as u64, Ordering::Relaxed);
self.inner
.model()
.release_decode_blocks(seq, blocks)
.map_err(DeviceError::Effect)?;
Ok(EffectOutcome::Unit)
}
Effect::ReadLogits { .. } if self.last_device_argmax.get() => {
Err(DeviceError::Readback(anyhow::anyhow!(
"the last step ended in the device argmax: its logits were not gathered"
)))
}
other => self.inner.apply(other),
}
}
fn lora(&mut self, cmd: LoraCommand) -> Result<LoraAck, String> {
self.inner.lora(cmd)
}
fn teardown(self: Box<Self>) -> anyhow::Result<()> {
let Self {
inner,
ring,
ring_bytes,
events,
..
} = *self;
{
let m = inner.model();
for e in events {
m.destroy_event(e)?;
}
m.free_host_pinned(ring, ring_bytes)?;
}
Box::new(inner).teardown()
}
}