use iroh::EndpointAddr; use serde::{Deserialize, Serialize}; use swactor::actor::{ActorAddress, ActorInterface}; use swactor::runtime::Ctx; use swactor_transport::{CodecRegistry, NetworkMessage}; use crate::orchestration::run_fsm as core; use crate::transport::json_codec::JsonCodec; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct StageRefWire { pub stage_index: u32, pub node_id: u64, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum OrchestratorMsg { ObservePoolReady { nodes: Vec, }, ObservePlanAvailable { run_id: u64, stages: Vec, }, ObserveStageReady { run_id: u64, stage_index: u32, }, ObserveNodeRuntimeReady { run_id: u64, node_id: u64, stage_index: u32, endpoint: EndpointAddr, node_actor: ActorAddress, datastream_publisher: ActorAddress, readiness_id: u64, }, ObserveNodeRuntimeReadyAck { run_id: u64, node_id: u64, stage_index: u32, readiness_id: u64, }, ObserveWeightsReady { run_id: u64, node_id: u64, stage_index: u32, }, ObserveTokenInEndpointReady, ObserveTokenOutEndpointReady, ObserveTokenReceived { sequence: u64, token_id: u32, eos: bool, }, ObserveStageFault { run_id: u64, stage_index: u32, }, ObserveEndpointFault { run_id: u64, endpoint: EndpointKindWire, }, ObserveStageStopped { run_id: u64, stage_index: u32, }, ObserveTokenEndpointsStopped, AdvanceTimeMs(u64), Snapshot { reply_to: ActorAddress, }, } impl NetworkMessage for OrchestratorMsg { fn type_tag() -> &'static str { "mvp_system::OrchestratorMsg" } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum EndpointKindWire { TokenIn, TokenOut, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SamplingDataWire { pub source_sequence: u64, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum TokenObjectPayloadWire { Prompt { tokens: Vec, }, Decode { token_id: u32, sampling: SamplingDataWire, }, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RunCommandWire { ProvisionStage { run_id: u64, stage_index: u32, node_id: u64, }, CreateTokenInEndpoint { run_id: u64, }, CreateTokenOutEndpoint { run_id: u64, }, InjectTokenObject { run_id: u64, sequence: u64, payload: TokenObjectPayloadWire, }, StopRun { run_id: u64, stage_index: u32, }, TearDownTokenEndpoints { run_id: u64, }, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum LifecycleEventWire { RunRejected { run_id: u64 }, RunFaulted { run_id: u64 }, RunCompleted { run_id: u64 }, RunOperatorStopped { run_id: u64 }, RunTornDown { run_id: u64 }, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum OrchestratorReport { Command(RunCommandWire), Lifecycle(LifecycleEventWire), NodeRuntimeReady { run_id: u64, node_id: u64, stage_index: u32, endpoint: EndpointAddr, node_actor: ActorAddress, datastream_publisher: ActorAddress, readiness_id: u64, }, NodeRuntimeReadyAck { run_id: u64, node_id: u64, stage_index: u32, readiness_id: u64, }, WeightsReady { run_id: u64, node_id: u64, stage_index: u32, }, StageReady { run_id: u64, stage_index: u32, }, StageFault { run_id: u64, stage_index: u32, }, Snapshot { commands: Vec, events: Vec, injected_sequences: Vec, }, } impl NetworkMessage for OrchestratorReport { fn type_tag() -> &'static str { "mvp_system::OrchestratorReport" } } pub struct OrchestratorActor { core: core::OrchestratorRun, report_to: Option, command_cursor: usize, event_cursor: usize, } impl OrchestratorActor { pub fn new(config: core::RunConfig, report_to: Option) -> Self { Self { core: core::OrchestratorRun::new(config), report_to, command_cursor: 0, event_cursor: 0, } } fn observe(&mut self, msg: OrchestratorMsg) { match msg { OrchestratorMsg::ObservePoolReady { nodes } => { self.core.observe(core::RunEvent::PoolReady { nodes: nodes.into_iter().map(core::NodeId).collect(), }); } OrchestratorMsg::ObservePlanAvailable { run_id, stages } => { self.core .observe(core::RunEvent::PlanAvailable(core::RunPlan { run_id: core::RunId(run_id), stages: stages .into_iter() .map(|stage| core::StageRef { stage_index: stage.stage_index, node_id: core::NodeId(stage.node_id), }) .collect(), })); } OrchestratorMsg::ObserveStageReady { run_id, stage_index, } => self.core.observe(core::RunEvent::StageReady { run_id: core::RunId(run_id), stage_index, }), OrchestratorMsg::ObserveNodeRuntimeReady { .. } => {} OrchestratorMsg::ObserveNodeRuntimeReadyAck { .. } => {} OrchestratorMsg::ObserveWeightsReady { .. } => {} OrchestratorMsg::ObserveTokenInEndpointReady => { self.core.observe(core::RunEvent::TokenInEndpointReady) } OrchestratorMsg::ObserveTokenOutEndpointReady => { self.core.observe(core::RunEvent::TokenOutEndpointReady) } OrchestratorMsg::ObserveTokenReceived { sequence, token_id, eos, } => self.core.observe(core::RunEvent::TokenReceived { sequence, token_id, eos, }), OrchestratorMsg::ObserveStageFault { run_id, stage_index, } => self.core.observe(core::RunEvent::StageFault { run_id: core::RunId(run_id), stage_index, reason: core::StageFaultReason::WorkerCrashed, }), OrchestratorMsg::ObserveEndpointFault { run_id, endpoint } => { self.core.observe(core::RunEvent::EndpointFault { run_id: core::RunId(run_id), endpoint: match endpoint { EndpointKindWire::TokenIn => core::EndpointKind::TokenIn, EndpointKindWire::TokenOut => core::EndpointKind::TokenOut, }, }); } OrchestratorMsg::ObserveStageStopped { run_id, stage_index, } => self.core.observe(core::RunEvent::StageStopped { run_id: core::RunId(run_id), stage_index, }), OrchestratorMsg::ObserveTokenEndpointsStopped => { self.core.observe(core::RunEvent::TokenEndpointsStopped) } OrchestratorMsg::AdvanceTimeMs(delta) => self.core.advance_time_ms(delta), OrchestratorMsg::Snapshot { .. } => {} } } fn drain_outputs(&mut self, ctx: &Ctx) { let Some(report_to) = self.report_to else { self.command_cursor = self.core.commands().len(); self.event_cursor = self.core.events().len(); return; }; for command in &self.core.commands()[self.command_cursor..] { let _ = ctx.send(report_to, OrchestratorReport::Command(command.into())); } self.command_cursor = self.core.commands().len(); for event in &self.core.events()[self.event_cursor..] { let _ = ctx.send(report_to, OrchestratorReport::Lifecycle(event.into())); } self.event_cursor = self.core.events().len(); } } impl ActorInterface for OrchestratorActor { type Incoming = OrchestratorMsg; type Response = (); fn handle(&mut self, ctx: &Ctx, msg: Self::Incoming) { match msg.clone() { OrchestratorMsg::ObserveNodeRuntimeReady { run_id, node_id, stage_index, endpoint, node_actor, datastream_publisher, readiness_id, } => { if let Some(report_to) = self.report_to { let _ = ctx.send( report_to, OrchestratorReport::NodeRuntimeReady { run_id, node_id, stage_index, endpoint, node_actor, datastream_publisher, readiness_id, }, ); } return; } OrchestratorMsg::ObserveNodeRuntimeReadyAck { run_id, node_id, stage_index, readiness_id, } => { if let Some(report_to) = self.report_to { let _ = ctx.send( report_to, OrchestratorReport::NodeRuntimeReadyAck { run_id, node_id, stage_index, readiness_id, }, ); } return; } OrchestratorMsg::ObserveWeightsReady { run_id, node_id, stage_index, } => { if let Some(report_to) = self.report_to { let _ = ctx.send( report_to, OrchestratorReport::WeightsReady { run_id, node_id, stage_index, }, ); } return; } OrchestratorMsg::Snapshot { reply_to } => { let _ = ctx.send( reply_to, OrchestratorReport::Snapshot { commands: self .core .commands() .iter() .map(RunCommandWire::from) .collect(), events: self .core .events() .iter() .map(LifecycleEventWire::from) .collect(), injected_sequences: self.core.injected_sequences(), }, ); return; } _ => {} } let direct_report = match msg.clone() { OrchestratorMsg::ObserveStageReady { run_id, stage_index, } => Some(OrchestratorReport::StageReady { run_id, stage_index, }), OrchestratorMsg::ObserveStageFault { run_id, stage_index, } => Some(OrchestratorReport::StageFault { run_id, stage_index, }), _ => None, }; self.observe(msg); if let (Some(report_to), Some(report)) = (self.report_to, direct_report) { let _ = ctx.send(report_to, report); } self.drain_outputs(ctx); } } impl From<&core::RunCommand> for RunCommandWire { fn from(command: &core::RunCommand) -> Self { match command { core::RunCommand::ProvisionStage { provision } => Self::ProvisionStage { run_id: provision.run_id.0, stage_index: provision.stage_index, node_id: provision.node_id.0, }, core::RunCommand::CreateTokenInEndpoint { run_id } => { Self::CreateTokenInEndpoint { run_id: run_id.0 } } core::RunCommand::CreateTokenOutEndpoint { run_id } => { Self::CreateTokenOutEndpoint { run_id: run_id.0 } } core::RunCommand::InjectTokenObject { run_id, object } => Self::InjectTokenObject { run_id: run_id.0, sequence: object.sequence, payload: (&object.payload).into(), }, core::RunCommand::StopRun { run_id, stage_index, } => Self::StopRun { run_id: run_id.0, stage_index: *stage_index, }, core::RunCommand::TearDownTokenEndpoints { run_id } => { Self::TearDownTokenEndpoints { run_id: run_id.0 } } } } } impl From<&core::LifecycleEvent> for LifecycleEventWire { fn from(event: &core::LifecycleEvent) -> Self { match event { core::LifecycleEvent::RunRejected { run_id, .. } => { Self::RunRejected { run_id: run_id.0 } } core::LifecycleEvent::RunFaulted { run_id, .. } => { Self::RunFaulted { run_id: run_id.0 } } core::LifecycleEvent::RunCompleted { run_id } => { Self::RunCompleted { run_id: run_id.0 } } core::LifecycleEvent::RunOperatorStopped { run_id } => { Self::RunOperatorStopped { run_id: run_id.0 } } core::LifecycleEvent::RunTornDown { run_id } => Self::RunTornDown { run_id: run_id.0 }, } } } pub fn register_codecs(registry: &mut CodecRegistry) { registry.register::(JsonCodec::::default()); registry.register::(JsonCodec::::default()); } impl From<&core::TokenObjectPayload> for TokenObjectPayloadWire { fn from(payload: &core::TokenObjectPayload) -> Self { match payload { core::TokenObjectPayload::Prompt { tokens } => Self::Prompt { tokens: tokens.clone(), }, core::TokenObjectPayload::Decode { token_id, sampling } => Self::Decode { token_id: *token_id, sampling: SamplingDataWire { source_sequence: sampling.source_sequence, }, }, } } } #[cfg(test)] mod tests { use super::*; use iroh::SecretKey; use swactor::config::RuntimeConfig; use swactor::runtime::Runtime; #[test] fn orchestrator_actor_reports_node_runtime_ready() { let runtime = Runtime::new(RuntimeConfig::default()); let reports = runtime .new_inbox::() .expect("orchestrator report inbox"); let report_to = *reports.addr(); let actor = runtime .spawn(OrchestratorActor::new( core::RunConfig { run_id: core::RunId(7), max_tokens: 1, prompt: Vec::new(), }, Some(report_to), )) .expect("spawn orchestrator actor"); let endpoint = EndpointAddr::new(SecretKey::from_bytes(&[8; 32]).public()); let node_actor = ActorAddress::new_random(); let datastream_publisher = ActorAddress::new_random(); runtime .send_to( actor, OrchestratorMsg::ObserveNodeRuntimeReady { run_id: 7, node_id: 11, stage_index: 3, endpoint: endpoint.clone(), node_actor, datastream_publisher, readiness_id: 99, }, ) .expect("send runtime ready"); runtime.tick(); assert_eq!( reports.try_recv(), Some(OrchestratorReport::NodeRuntimeReady { run_id: 7, node_id: 11, stage_index: 3, endpoint, node_actor, datastream_publisher, readiness_id: 99, }) ); assert_eq!(reports.try_recv(), None); } }