use std::collections::HashMap;
use fuel_graph::NodeId;
use fuel_ir::DeviceLocation;
use smallvec::SmallVec;
use super::cost::TransferEstimator;
type DeviceVec = SmallVec<[DeviceLocation; 4]>;
type CostVec = SmallVec<[u64; 4]>;
#[derive(Clone, Debug)]
pub struct ChainInput {
pub producer: NodeId,
pub edge_bytes: SmallVec<[u64; 2]>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RowStatus {
Open,
Chained,
Committed,
}
#[derive(Clone, Debug)]
struct DpRow {
node: NodeId,
devices: DeviceVec,
best: CostVec,
back: Vec<Vec<(NodeId, DeviceLocation)>>,
status: RowStatus,
committed: Option<DeviceLocation>,
}
#[derive(Debug, Default)]
pub struct PlacementDp {
rows: Vec<DpRow>,
by_node: HashMap<NodeId, usize>,
aliases: HashMap<NodeId, NodeId>,
}
impl PlacementDp {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn resolve(&self, id: NodeId) -> NodeId {
self.aliases.get(&id).copied().unwrap_or(id)
}
pub fn add_alias(&mut self, view: NodeId, src: NodeId) {
let root = self.resolve(src);
self.aliases.insert(view, root);
}
pub fn is_open(&self, id: NodeId) -> bool {
self.by_node
.get(&id)
.is_some_and(|&i| self.rows[i].status == RowStatus::Open)
}
pub fn committed_device(&self, id: NodeId) -> Option<DeviceLocation> {
self.by_node.get(&id).and_then(|&i| self.rows[i].committed)
}
pub fn push_row(
&mut self,
node: NodeId,
device_costs: &[(DeviceLocation, u64)],
fixed_inputs: &[(DeviceLocation, u64)],
chain_inputs: &[ChainInput],
est: &dyn TransferEstimator,
) {
let mut devices = DeviceVec::new();
let mut best = CostVec::new();
let mut back: Vec<Vec<(NodeId, DeviceLocation)>> = Vec::with_capacity(device_costs.len());
for &(d, kernel_ns) in device_costs {
let mut acc = kernel_ns;
for &(src, bytes) in fixed_inputs {
acc = acc.saturating_add(est.estimate_transfer_ns(src, d, bytes));
}
let mut bp = Vec::with_capacity(chain_inputs.len());
for ci in chain_inputs {
let Some(&p_idx) = self.by_node.get(&ci.producer) else {
continue;
};
let p = &self.rows[p_idx];
let mut choice: Option<(DeviceLocation, u64)> = None;
for (j, &pd) in p.devices.iter().enumerate() {
let mut t = p.best[j];
for &b in &ci.edge_bytes {
t = t.saturating_add(est.estimate_transfer_ns(pd, d, b));
}
if choice.is_none_or(|(_, c)| t < c) {
choice = Some((pd, t));
}
}
if let Some((pd, c)) = choice {
acc = acc.saturating_add(c);
bp.push((ci.producer, pd));
}
}
devices.push(d);
best.push(acc);
back.push(bp);
}
for ci in chain_inputs {
if let Some(&p_idx) = self.by_node.get(&ci.producer)
&& self.rows[p_idx].status == RowStatus::Open
{
self.rows[p_idx].status = RowStatus::Chained;
}
}
let idx = self.rows.len();
self.rows.push(DpRow {
node,
devices,
best,
back,
status: RowStatus::Open,
committed: None,
});
self.by_node.insert(node, idx);
}
pub fn close_toward(
&mut self,
row_node: NodeId,
consumer_device: DeviceLocation,
edge_bytes: &[u64],
est: &dyn TransferEstimator,
) -> Vec<(NodeId, DeviceLocation)> {
let mut out = Vec::new();
let Some(&idx) = self.by_node.get(&row_node) else {
return out;
};
if self.rows[idx].status != RowStatus::Open {
return out;
}
let row = &self.rows[idx];
let mut choice: Option<(DeviceLocation, u64)> = None;
for (j, &d) in row.devices.iter().enumerate() {
let mut t = row.best[j];
for &b in edge_bytes {
t = t.saturating_add(est.estimate_transfer_ns(d, consumer_device, b));
}
if choice.is_none_or(|(_, c)| t < c) {
choice = Some((d, t));
}
}
if let Some((d, _)) = choice {
self.commit_row(idx, d, &mut out);
}
out
}
pub fn finish(
&mut self,
exit_loc: Option<DeviceLocation>,
exit_bytes: impl Fn(NodeId) -> u64,
est: &dyn TransferEstimator,
) -> Vec<(NodeId, DeviceLocation)> {
let mut out = Vec::new();
for idx in 0..self.rows.len() {
if self.rows[idx].status != RowStatus::Open {
continue;
}
let row = &self.rows[idx];
let mut choice: Option<(DeviceLocation, u64)> = None;
for (j, &d) in row.devices.iter().enumerate() {
let mut t = row.best[j];
if let Some(loc) = exit_loc {
t = t.saturating_add(est.estimate_transfer_ns(d, loc, exit_bytes(row.node)));
}
if choice.is_none_or(|(_, c)| t < c) {
choice = Some((d, t));
}
}
if let Some((d, _)) = choice {
self.commit_row(idx, d, &mut out);
}
}
for idx in 0..self.rows.len() {
if self.rows[idx].status == RowStatus::Committed {
continue;
}
let row = &self.rows[idx];
let mut choice: Option<(DeviceLocation, u64)> = None;
for (j, &d) in row.devices.iter().enumerate() {
if choice.is_none_or(|(_, c)| row.best[j] < c) {
choice = Some((d, row.best[j]));
}
}
if let Some((d, _)) = choice {
self.commit_row(idx, d, &mut out);
}
}
out
}
fn commit_row(
&mut self,
start: usize,
device: DeviceLocation,
out: &mut Vec<(NodeId, DeviceLocation)>,
) {
let mut stack = vec![(start, device)];
while let Some((idx, dev)) = stack.pop() {
if self.rows[idx].status == RowStatus::Committed {
continue;
}
self.rows[idx].status = RowStatus::Committed;
self.rows[idx].committed = Some(dev);
out.push((self.rows[idx].node, dev));
let Some(di) = self.rows[idx].devices.iter().position(|&d| d == dev) else {
continue;
};
let back = self.rows[idx].back[di].clone();
for (p_node, p_dev) in back {
if let Some(&p_idx) = self.by_node.get(&p_node) {
stack.push((p_idx, p_dev));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use smallvec::smallvec;
const CPU: DeviceLocation = DeviceLocation::Cpu;
const GPU: DeviceLocation = DeviceLocation::Cuda { gpu_id: 0 };
struct Est {
latency: u64,
per_byte: u64,
}
impl TransferEstimator for Est {
fn estimate_transfer_ns(
&self,
src: DeviceLocation,
dst: DeviceLocation,
bytes: u64,
) -> u64 {
if src == dst {
return 0;
}
self.latency
.saturating_add(bytes.saturating_mul(self.per_byte))
}
}
fn chain(producer: NodeId, bytes: u64) -> ChainInput {
ChainInput {
producer,
edge_bytes: smallvec![bytes],
}
}
#[test]
fn extension_carries_accumulated_cost_and_backpointers() {
let est = Est {
latency: 1000,
per_byte: 0,
};
let mut dp = PlacementDp::new();
dp.push_row(NodeId(0), &[(CPU, 800), (GPU, 100)], &[(CPU, 4)], &[], &est);
assert!(dp.is_open(NodeId(0)));
dp.push_row(
NodeId(1),
&[(CPU, 800), (GPU, 100)],
&[],
&[chain(NodeId(0), 4)],
&est,
);
assert!(
!dp.is_open(NodeId(0)),
"chained producer leaves the frontier"
);
assert!(dp.is_open(NodeId(1)));
let commits = dp.finish(Some(CPU), |_| 4, &est);
assert_eq!(dp.committed_device(NodeId(1)), Some(CPU));
assert_eq!(dp.committed_device(NodeId(0)), Some(CPU));
assert_eq!(commits.len(), 2);
}
#[test]
fn exit_pricing_flips_the_local_winner() {
let est = Est {
latency: 1000,
per_byte: 0,
};
let mut dp = PlacementDp::new();
dp.push_row(NodeId(0), &[(CPU, 1050), (GPU, 100)], &[], &[], &est);
dp.finish(Some(CPU), |_| 4, &est);
assert_eq!(dp.committed_device(NodeId(0)), Some(CPU));
let mut dp2 = PlacementDp::new();
dp2.push_row(NodeId(0), &[(CPU, 1050), (GPU, 100)], &[], &[], &est);
dp2.finish(None, |_| 4, &est);
assert_eq!(dp2.committed_device(NodeId(0)), Some(GPU));
}
#[test]
fn close_toward_prices_the_boundary_crossing() {
let est = Est {
latency: 10,
per_byte: 1,
};
let mut dp = PlacementDp::new();
dp.push_row(NodeId(0), &[(CPU, 300), (GPU, 100)], &[], &[], &est);
let commits = dp.close_toward(NodeId(0), CPU, &[1024], &est);
assert_eq!(commits, vec![(NodeId(0), CPU)]);
assert_eq!(dp.committed_device(NodeId(0)), Some(CPU));
assert!(dp.close_toward(NodeId(0), GPU, &[1024], &est).is_empty());
}
#[test]
fn duplicated_edges_price_per_occurrence() {
let est = Est {
latency: 1000,
per_byte: 0,
};
let mut dp = PlacementDp::new();
dp.push_row(NodeId(0), &[(CPU, 0), (GPU, 0)], &[], &[], &est);
let ci = ChainInput {
producer: NodeId(0),
edge_bytes: smallvec![4, 4],
};
dp.push_row(NodeId(1), &[(CPU, 0), (GPU, 0)], &[], &[ci], &est);
dp.finish(Some(CPU), |_| 4, &est);
assert_eq!(dp.committed_device(NodeId(1)), Some(CPU));
assert_eq!(dp.committed_device(NodeId(0)), Some(CPU));
}
#[test]
fn aliases_flatten_through_view_chains() {
let mut dp = PlacementDp::new();
let est = Est {
latency: 0,
per_byte: 0,
};
dp.push_row(NodeId(0), &[(CPU, 0)], &[], &[], &est);
dp.add_alias(NodeId(1), NodeId(0));
dp.add_alias(NodeId(2), NodeId(1));
assert_eq!(dp.resolve(NodeId(2)), NodeId(0));
assert_eq!(dp.resolve(NodeId(1)), NodeId(0));
assert_eq!(dp.resolve(NodeId(3)), NodeId(3), "no alias → identity");
assert!(dp.is_open(dp.resolve(NodeId(2))));
}
#[test]
fn saturating_costs_never_overflow() {
let est = Est {
latency: u64::MAX,
per_byte: u64::MAX,
};
let mut dp = PlacementDp::new();
dp.push_row(
NodeId(0),
&[(CPU, u64::MAX), (GPU, u64::MAX)],
&[(CPU, u64::MAX), (GPU, u64::MAX)],
&[],
&est,
);
dp.push_row(
NodeId(1),
&[(CPU, u64::MAX)],
&[],
&[chain(NodeId(0), u64::MAX)],
&est,
);
let commits = dp.finish(Some(GPU), |_| u64::MAX, &est);
assert_eq!(commits.len(), 2, "all rows commit despite saturated costs");
}
}