use crate::{
backend::{DeviceCapabilities, SessionCapabilities},
residency::CacheEvictionPolicy,
topology::ParallelTopology,
};
use serde::{Deserialize, Serialize};
pub const EXECUTION_PLAN_SCHEMA_VERSION: u32 = 4;
pub const DEFAULT_MAX_CACHED_SHARDS: usize = 4;
#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct BackendId(String);
impl BackendId {
pub fn new(value: impl Into<String>) -> Result<Self, ExecutionPlanError> {
let value = value.into();
if value.trim().is_empty() {
return Err(ExecutionPlanError::EmptyBackendId);
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for BackendId {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl<'de> Deserialize<'de> for BackendId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
pub struct DevicePlan {
pub(crate) backend: BackendId,
pub(crate) device: String,
}
impl<'de> Deserialize<'de> for DevicePlan {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawDevicePlan {
backend: BackendId,
device: String,
}
let raw = RawDevicePlan::deserialize(deserializer)?;
Self::new(raw.backend.0, raw.device).map_err(serde::de::Error::custom)
}
}
impl DevicePlan {
pub fn new(
backend: impl Into<String>,
device: impl Into<String>,
) -> Result<Self, ExecutionPlanError> {
let device = device.into();
if device.trim().is_empty() {
return Err(ExecutionPlanError::EmptyDeviceId);
}
Ok(Self {
backend: BackendId::new(backend)?,
device,
})
}
pub const fn backend(&self) -> &BackendId {
&self.backend
}
pub fn device(&self) -> &str {
&self.device
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResidencyPlan {
FullyResident,
LayerwiseHost {
device_layer_window: usize,
#[serde(skip_serializing_if = "Option::is_none")]
device_budget_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
host_budget_bytes: Option<u64>,
},
DenseDiskStream {
device_budget_bytes: u64,
host_budget_bytes: u64,
host_lookahead: usize,
background_queue: usize,
},
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct ExpertCachePlan {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) device_budget_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) host_budget_bytes: Option<u64>,
pub(crate) scratch_bytes: u64,
pub(crate) prefill_bank_bytes: u64,
pub(crate) eviction_policy: CacheEvictionPolicy,
}
impl ExpertCachePlan {
pub const fn new(
device_budget_bytes: Option<u64>,
host_budget_bytes: Option<u64>,
scratch_bytes: u64,
prefill_bank_bytes: u64,
eviction_policy: CacheEvictionPolicy,
) -> Self {
Self {
device_budget_bytes,
host_budget_bytes,
scratch_bytes,
prefill_bank_bytes,
eviction_policy,
}
}
pub const fn device_budget_bytes(&self) -> Option<u64> {
self.device_budget_bytes
}
pub const fn host_budget_bytes(&self) -> Option<u64> {
self.host_budget_bytes
}
pub const fn scratch_bytes(&self) -> u64 {
self.scratch_bytes
}
pub const fn prefill_bank_bytes(&self) -> u64 {
self.prefill_bank_bytes
}
pub const fn eviction_policy(&self) -> CacheEvictionPolicy {
self.eviction_policy
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
#[non_exhaustive]
pub enum DraftingPlan {
Disabled,
Embedded {
max_draft_tokens: usize,
lookahead: bool,
adaptive_lookahead: bool,
},
External {
model: String,
placement: DraftPlacementPlan,
max_draft_tokens: usize,
lookahead: bool,
adaptive_lookahead: bool,
},
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
#[non_exhaustive]
pub enum DraftPlacementPlan {
Target,
Device {
device: DevicePlan,
},
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
#[non_exhaustive]
pub enum WeightTransformationPlan {
PreserveCheckpoint,
Affine {
bits: i32,
group_size: i32,
},
MxFp4,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct ExecutionPlan {
pub(crate) schema_version: u32,
pub(crate) device: DevicePlan,
pub(crate) topology: ParallelTopology,
pub(crate) residency: ResidencyPlan,
pub(crate) weight_transformation: WeightTransformationPlan,
pub(crate) max_cached_shards: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) expert_cache: Option<ExpertCachePlan>,
pub(crate) drafting: DraftingPlan,
pub(crate) required_device_capabilities: DeviceCapabilities,
pub(crate) required_session_capabilities: SessionCapabilities,
}
impl ExecutionPlan {
pub fn fully_resident(device: DevicePlan) -> Self {
Self {
schema_version: EXECUTION_PLAN_SCHEMA_VERSION,
device,
topology: ParallelTopology::new(1, 1, 1, 1).expect("the singleton topology is valid"),
residency: ResidencyPlan::FullyResident,
weight_transformation: WeightTransformationPlan::PreserveCheckpoint,
max_cached_shards: DEFAULT_MAX_CACHED_SHARDS,
expert_cache: None,
drafting: DraftingPlan::Disabled,
required_device_capabilities: DeviceCapabilities::new(true, false, false),
required_session_capabilities: SessionCapabilities::default(),
}
}
pub const fn schema_version(&self) -> u32 {
self.schema_version
}
pub const fn device(&self) -> &DevicePlan {
&self.device
}
pub const fn topology(&self) -> &ParallelTopology {
&self.topology
}
pub const fn residency(&self) -> &ResidencyPlan {
&self.residency
}
pub const fn weight_transformation(&self) -> WeightTransformationPlan {
self.weight_transformation
}
pub const fn max_cached_shards(&self) -> usize {
self.max_cached_shards
}
pub const fn expert_cache(&self) -> Option<&ExpertCachePlan> {
self.expert_cache.as_ref()
}
pub const fn drafting(&self) -> &DraftingPlan {
&self.drafting
}
pub const fn required_device_capabilities(&self) -> &DeviceCapabilities {
&self.required_device_capabilities
}
pub const fn required_session_capabilities(&self) -> &SessionCapabilities {
&self.required_session_capabilities
}
pub fn with_topology(mut self, topology: ParallelTopology) -> Self {
self.topology = topology;
self
}
pub fn with_device(mut self, device: DevicePlan) -> Self {
self.device = device;
self
}
pub fn with_residency(mut self, residency: ResidencyPlan) -> Self {
self.residency = residency;
self
}
pub fn with_weight_transformation(mut self, transformation: WeightTransformationPlan) -> Self {
self.weight_transformation = transformation;
self
}
pub fn with_max_cached_shards(mut self, maximum: usize) -> Self {
self.max_cached_shards = maximum;
self
}
pub fn with_expert_cache(mut self, expert_cache: Option<ExpertCachePlan>) -> Self {
self.expert_cache = expert_cache;
self
}
pub fn with_drafting(mut self, drafting: DraftingPlan) -> Self {
self.drafting = drafting;
self
}
pub fn with_required_device_capabilities(mut self, capabilities: DeviceCapabilities) -> Self {
self.required_device_capabilities = capabilities;
self
}
pub fn with_required_session_capabilities(mut self, capabilities: SessionCapabilities) -> Self {
self.required_session_capabilities = capabilities;
self
}
pub fn validate_device_capabilities(
&self,
available: &DeviceCapabilities,
) -> Result<(), ExecutionPlanError> {
self.validate_structure()?;
for (required, supported, name) in [
(
self.required_device_capabilities.exact_completion(),
available.exact_completion(),
"exact_completion",
),
(
self.required_device_capabilities.transfers(),
available.transfers(),
"transfers",
),
(
self.required_device_capabilities.collectives(),
available.collectives(),
"collectives",
),
] {
if required && !supported {
return Err(ExecutionPlanError::Capability(name));
}
}
Ok(())
}
pub fn validate_session_capabilities(
&self,
available: &SessionCapabilities,
) -> Result<(), ExecutionPlanError> {
self.validate_structure()?;
self.required_session_capabilities
.validate(available)
.map_err(|error| ExecutionPlanError::Capability(error.capability()))
}
pub fn validate_structure(&self) -> Result<(), ExecutionPlanError> {
if self.schema_version != EXECUTION_PLAN_SCHEMA_VERSION {
return Err(ExecutionPlanError::Schema(self.schema_version));
}
if self.max_cached_shards == 0 {
return Err(ExecutionPlanError::ZeroMappedShards);
}
match &self.drafting {
DraftingPlan::Disabled => {}
DraftingPlan::Embedded {
max_draft_tokens, ..
} => {
if *max_draft_tokens == 0 {
return Err(ExecutionPlanError::ZeroDraftTokens);
}
}
DraftingPlan::External {
model,
max_draft_tokens,
..
} => {
if model.trim().is_empty() {
return Err(ExecutionPlanError::EmptyDraftModel);
}
if *max_draft_tokens == 0 {
return Err(ExecutionPlanError::ZeroDraftTokens);
}
}
}
ParallelTopology::new(
self.topology.tensor(),
self.topology.pipeline(),
self.topology.expert(),
self.topology.data(),
)
.map_err(|error| ExecutionPlanError::Topology(error.to_string()))?;
Ok(())
}
}
#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ExecutionPlanError {
#[error("execution-plan backend identifier must not be empty")]
EmptyBackendId,
#[error("execution-plan device identifier must not be empty")]
EmptyDeviceId,
#[error("unsupported execution-plan schema version {0}")]
Schema(u32),
#[error("execution plan requires unavailable capability {0}")]
Capability(&'static str),
#[error("execution-plan topology is invalid: {0}")]
Topology(String),
#[error("execution-plan max_cached_shards must be greater than zero")]
ZeroMappedShards,
#[error("execution-plan external draft model must not be empty")]
EmptyDraftModel,
#[error("execution-plan max_draft_tokens must be greater than zero")]
ZeroDraftTokens,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plan_round_trips_with_extensible_backend_identity() {
let plan = ExecutionPlan::fully_resident(DevicePlan::new("iree", "vulkan:2").unwrap());
let encoded = serde_json::to_vec(&plan).unwrap();
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&encoded).unwrap()["schema_version"],
4
);
assert_eq!(
serde_json::from_slice::<ExecutionPlan>(&encoded).unwrap(),
plan
);
}
#[test]
fn plan_capabilities_fail_closed() {
let mut plan = ExecutionPlan::fully_resident(DevicePlan::new("mlx", "metal:0").unwrap());
assert_eq!(
plan.validate_device_capabilities(&DeviceCapabilities::default()),
Err(ExecutionPlanError::Capability("exact_completion"))
);
plan.required_session_capabilities = plan
.required_session_capabilities
.with_activation_inspection(true);
assert_eq!(
plan.validate_session_capabilities(&SessionCapabilities::default()),
Err(ExecutionPlanError::Capability("activation_inspection"))
);
assert!(plan
.validate_device_capabilities(&DeviceCapabilities::new(true, false, false))
.is_ok());
assert!(plan
.validate_session_capabilities(
&SessionCapabilities::default().with_activation_inspection(true),
)
.is_ok());
}
#[test]
fn backend_and_device_identifiers_fail_closed_during_deserialization() {
assert!(serde_json::from_str::<DevicePlan>(r#"{"backend":"","device":"cpu:0"}"#).is_err());
assert!(serde_json::from_str::<DevicePlan>(r#"{"backend":"mlx","device":""}"#).is_err());
}
#[test]
fn speculative_plan_structure_fails_closed() {
let mut plan = ExecutionPlan::fully_resident(DevicePlan::new("mock", "gpu:0").unwrap());
plan.drafting = DraftingPlan::Embedded {
max_draft_tokens: 0,
lookahead: false,
adaptive_lookahead: false,
};
assert_eq!(
plan.validate_structure(),
Err(ExecutionPlanError::ZeroDraftTokens)
);
plan.drafting = DraftingPlan::External {
model: " ".into(),
placement: DraftPlacementPlan::Target,
max_draft_tokens: 1,
lookahead: false,
adaptive_lookahead: false,
};
assert_eq!(
plan.validate_structure(),
Err(ExecutionPlanError::EmptyDraftModel)
);
}
}