use std::time::Duration;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::{ResourceRequirements, Workload};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Region3D {
pub x: f64,
pub y: f64,
pub z: f64,
pub radius: f64,
}
impl Region3D {
pub fn new(x: f64, y: f64, z: f64, radius: f64) -> Self {
Self { x, y, z, radius }
}
pub fn center_distance_sq(&self, other: &Region3D) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
let dz = self.z - other.z;
dx * dx + dy * dy + dz * dz
}
pub fn overlaps(&self, other: &Region3D) -> bool {
let r = self.radius + other.radius;
self.center_distance_sq(other) <= r * r
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CoPlacement {
SameNode,
InterconnectLocalGpu,
Spread,
}
impl CoPlacement {
pub fn requires_same_node(&self) -> bool {
matches!(self, CoPlacement::SameNode | CoPlacement::InterconnectLocalGpu)
}
pub fn wants_gpu_locality(&self) -> bool {
matches!(self, CoPlacement::InterconnectLocalGpu)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MemberRole {
World,
Agent,
}
impl MemberRole {
pub fn token(&self) -> &'static str {
match self {
MemberRole::World => "world",
MemberRole::Agent => "agent",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentPolicy {
pub name: String,
pub resources: ResourceRequirements,
}
impl AgentPolicy {
pub fn new(name: impl Into<String>, resources: ResourceRequirements) -> Self {
Self {
name: name.into(),
resources,
}
}
pub fn gpu(
name: impl Into<String>,
cpu_millis: u64,
memory_mb: u64,
gpu_memory_mb: u64,
) -> Self {
Self::new(
name,
ResourceRequirements::new()
.cpu(cpu_millis)
.memory(memory_mb)
.gpu(1, gpu_memory_mb),
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimWorld {
pub resources: ResourceRequirements,
}
impl SimWorld {
pub fn new(resources: ResourceRequirements) -> Self {
Self { resources }
}
pub fn cpu(cpu_millis: u64, memory_mb: u64) -> Self {
Self::new(ResourceRequirements::new().cpu(cpu_millis).memory(memory_mb))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimCell {
pub id: String,
pub region: Option<Region3D>,
pub world: SimWorld,
pub agents: Vec<AgentPolicy>,
pub tick: Duration,
pub next_deadline: DateTime<Utc>,
pub co_placement: CoPlacement,
pub priority: i32,
}
#[derive(Debug, Clone)]
pub struct SimMember {
pub workload: Workload,
pub role: MemberRole,
pub ordinal: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemberIdentity {
pub cell_id: String,
pub role: MemberRole,
pub ordinal: usize,
}
pub const MEMBER_ID_PREFIX: &str = "simcell";
pub fn member_id(cell_id: &str, role: MemberRole, ordinal: usize) -> String {
format!("{MEMBER_ID_PREFIX}/{cell_id}/{}/{ordinal}", role.token())
}
pub fn co_placement_key(cell_id: &str) -> String {
format!("{MEMBER_ID_PREFIX}/{cell_id}")
}
pub fn parse_member_id(id: &str) -> Option<MemberIdentity> {
let rest = id.strip_prefix(MEMBER_ID_PREFIX)?.strip_prefix('/')?;
let mut parts = rest.splitn(3, '/');
let cell_id = parts.next()?.to_string();
let role = match parts.next()? {
"world" => MemberRole::World,
"agent" => MemberRole::Agent,
_ => return None,
};
let ordinal: usize = parts.next()?.parse().ok()?;
if cell_id.is_empty() {
return None;
}
Some(MemberIdentity {
cell_id,
role,
ordinal,
})
}
impl SimCell {
pub fn new(id: impl Into<String>, world: SimWorld, tick: Duration) -> Self {
let tick_chrono = chrono::Duration::from_std(tick)
.unwrap_or_else(|_| chrono::Duration::milliseconds(0));
Self {
id: id.into(),
region: None,
world,
agents: Vec::new(),
tick,
next_deadline: Utc::now() + tick_chrono,
co_placement: CoPlacement::SameNode,
priority: 0,
}
}
pub fn with_region(mut self, region: Region3D) -> Self {
self.region = Some(region);
self
}
pub fn with_agent(mut self, agent: AgentPolicy) -> Self {
self.agents.push(agent);
self
}
pub fn with_co_placement(mut self, policy: CoPlacement) -> Self {
self.co_placement = policy;
self
}
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn with_next_deadline(mut self, deadline: DateTime<Utc>) -> Self {
self.next_deadline = deadline;
self
}
pub fn member_count(&self) -> usize {
1 + self.agents.len()
}
pub fn advance_deadline(&mut self) {
let tick_chrono = chrono::Duration::from_std(self.tick)
.unwrap_or_else(|_| chrono::Duration::milliseconds(0));
self.next_deadline += tick_chrono;
}
pub fn expand(&self) -> Vec<SimMember> {
let mut members = Vec::with_capacity(self.member_count());
let world_workload = Workload::new(
member_id(&self.id, MemberRole::World, 0),
format!("{}-world", self.id),
)
.with_resources(self.world.resources.clone())
.with_priority(self.priority);
members.push(SimMember {
workload: world_workload,
role: MemberRole::World,
ordinal: 0,
});
for (i, agent) in self.agents.iter().enumerate() {
let agent_workload = Workload::new(
member_id(&self.id, MemberRole::Agent, i),
format!("{}-{}", self.id, agent.name),
)
.with_resources(agent.resources.clone())
.with_priority(self.priority);
members.push(SimMember {
workload: agent_workload,
role: MemberRole::Agent,
ordinal: i,
});
}
members
}
pub fn members(&self) -> Vec<Workload> {
self.expand().into_iter().map(|m| m.workload).collect()
}
pub fn gang_group(&self) -> GangGroup {
GangGroup {
id: co_placement_key(&self.id),
members: self.expand(),
all_or_nothing: true,
co_placement: self.co_placement,
}
}
}
#[derive(Debug, Clone)]
pub struct GangGroup {
pub id: String,
pub members: Vec<SimMember>,
pub all_or_nothing: bool,
pub co_placement: CoPlacement,
}
impl GangGroup {
pub fn len(&self) -> usize {
self.members.len()
}
pub fn is_empty(&self) -> bool {
self.members.is_empty()
}
pub fn total_gpu_count(&self) -> u32 {
self.members
.iter()
.map(|m| m.workload.resources.gpu_count)
.sum()
}
pub fn workloads(&self) -> impl Iterator<Item = &Workload> {
self.members.iter().map(|m| &m.workload)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_cell() -> SimCell {
SimCell::new("alpha", SimWorld::cpu(2000, 4096), Duration::from_millis(50))
.with_agent(AgentPolicy::gpu("policy-a", 250, 512, 8192))
.with_agent(AgentPolicy::gpu("policy-b", 250, 512, 8192))
.with_co_placement(CoPlacement::InterconnectLocalGpu)
.with_priority(100)
}
#[test]
fn expands_to_world_plus_agents() {
let cell = sample_cell();
let members = cell.expand();
assert_eq!(members.len(), 3);
assert_eq!(cell.member_count(), 3);
assert_eq!(members[0].role, MemberRole::World);
assert_eq!(members[0].workload.resources.gpu_count, 0);
assert_eq!(members[0].workload.resources.cpu_millis, 2000);
assert_eq!(members[1].role, MemberRole::Agent);
assert_eq!(members[1].workload.resources.gpu_count, 1);
assert_eq!(members[1].workload.resources.gpu_memory_mb, 8192);
assert_eq!(members[1].workload.priority, 100);
assert_eq!(members[2].ordinal, 1);
}
#[test]
fn member_ids_are_deterministic_and_parseable() {
let cell = sample_cell();
let members = cell.members();
assert_eq!(members[0].id, "simcell/alpha/world/0");
assert_eq!(members[1].id, "simcell/alpha/agent/0");
assert_eq!(members[2].id, "simcell/alpha/agent/1");
let id = parse_member_id(&members[2].id).unwrap();
assert_eq!(id.cell_id, "alpha");
assert_eq!(id.role, MemberRole::Agent);
assert_eq!(id.ordinal, 1);
assert!(parse_member_id("some-other-workload").is_none());
assert!(parse_member_id("simcell//world/0").is_none());
}
#[test]
fn gang_group_carries_co_placement_and_gpu_totals() {
let cell = sample_cell();
let group = cell.gang_group();
assert_eq!(group.id, "simcell/alpha");
assert!(group.all_or_nothing);
assert_eq!(group.co_placement, CoPlacement::InterconnectLocalGpu);
assert_eq!(group.len(), 3);
assert_eq!(group.total_gpu_count(), 2);
}
#[test]
fn region_overlap_is_symmetric_and_correct() {
let a = Region3D::new(0.0, 0.0, 0.0, 5.0);
let b = Region3D::new(8.0, 0.0, 0.0, 5.0); let c = Region3D::new(20.0, 0.0, 0.0, 1.0);
assert!(a.overlaps(&b));
assert!(b.overlaps(&a));
assert!(!a.overlaps(&c));
}
#[test]
fn deadline_defaults_and_advances_by_tick() {
let mut cell = SimCell::new("t", SimWorld::cpu(100, 128), Duration::from_millis(100));
let first = cell.next_deadline;
cell.advance_deadline();
let delta = cell.next_deadline - first;
assert_eq!(delta.num_milliseconds(), 100);
}
}