use anyhow::Result;
use metrale_config::ModelConfig;
use crate::cli;
mod decode_ring;
mod gpu_backend;
mod headroom;
mod mamba2_spec;
mod per_sequence_state;
mod post_load_audit;
mod refusal;
pub(crate) mod reserve_plan;
pub(crate) mod runtime_headroom;
mod ssm_h_fp16;
#[cfg(any(feature = "cuda", feature = "metal"))]
pub(crate) use gpu_backend::init_gpu_backend;
pub(crate) use headroom::PostLoadInputs;
pub(crate) use post_load_audit::post_load_memory_audit;
use {
mamba2_spec::refuse_speculation_over_mamba2, per_sequence_state::per_sequence_reserve,
ssm_h_fp16::ssm_h_fp16_preconditions,
};
pub(crate) struct ReservePreflight {
pub(crate) inference_reserve: usize,
pub(crate) buffer_arena_bytes: usize,
pub(crate) gdn_two_phase_bytes: usize,
pub(crate) ssm_prefill_chunk: usize,
pub(crate) max_batch_tokens_pre: usize,
pub(crate) plan: reserve_plan::ReservePlan,
}
pub(crate) fn preflight_reserve(
args: &cli::ServeArgs,
config: &ModelConfig,
free_mem: usize,
post_load: &PostLoadInputs<'_>,
) -> Result<ReservePreflight> {
let spec_on_pool = args.speculative_proposer_requested();
refuse_speculation_over_mamba2(args, config)?;
ssm_h_fp16_preconditions(args, config)?;
let pool_num_drafts = if args.dflash {
args.serve_dflash_gamma()
} else {
args.resolved_num_drafts()
};
let ceiling = args.max_batch_size.ceiling();
let h_f16_pool = metrale_model_layers::layers::qwen3_ssm::ssm_h_f16_pool_enabled();
let rollback = metrale_model_layers::ssm_reserve::ssm_rollback_mode();
let unit = metrale_model_layers::ssm_reserve::PoolPlan::new(
config,
&metrale_model_layers::ssm_reserve::pool_counts(
&metrale_model_layers::ssm_reserve::PoolShape {
max_slots: 1,
spec: spec_on_pool,
num_intermediates: pool_num_drafts + 1,
num_drafts: pool_num_drafts,
uniform_h: args.dflash,
rollback,
},
),
h_f16_pool,
)?;
let (h_state_bytes, conv_state_bytes) = (unit.h_f32_unit, unit.conv_unit);
let spec_tokens_pre = spec_reserve_tokens(args);
let ssm_prefill_chunk: usize = if config.num_ssm_layers() > 0 {
if args.max_prefill_tokens != 8192 && args.max_prefill_tokens > 0 {
args.max_seq_len.min(args.max_prefill_tokens)
} else {
args.max_seq_len.min(8192)
}
} else {
0
};
let user_set_prefill_pre = args.max_prefill_tokens != 8192;
let prefill_budget_pre = if user_set_prefill_pre && args.max_prefill_tokens > 0 {
args.max_prefill_tokens
} else if ssm_prefill_chunk > 0 {
ssm_prefill_chunk
} else if args.max_prefill_tokens > 0 {
args.max_prefill_tokens
} else {
args.max_seq_len
};
let max_batch_tokens_pre = prefill_budget_pre.max(spec_tokens_pre).max(ceiling);
let buffer_arena_bytes = metrale_gpu_runtime::buffers::BufferSizes::from_config(
config,
max_batch_tokens_pre,
args.max_seq_len,
args.block_size,
ceiling,
)
.total_bytes();
let marconi = metrale_model_layers::ssm_reserve::marconi_snapshot_slots(
args.ssm_cache_slots,
metrale_model_layers::ssm_reserve::prefix_caching_active(
args.prefix_caching_enabled(),
config.kv_only_prefix_cache_is_safe(),
),
);
if let Some(reason) = marconi.skip_reason {
tracing::info!(
"SSM snapshot pool: Marconi region SKIPPED ({}) — {} slot(s) x {} layer(s) \
= {} MB not reserved (restore with --enable-prefix-caching, or \
METRALE_SSM_MARCONI_FULL to over-reserve)",
reason,
args.ssm_cache_slots,
config.num_ssm_layers(),
(args.ssm_cache_slots * config.num_ssm_layers() * (h_state_bytes + conv_state_bytes))
/ (1024 * 1024),
);
}
let per_seq_blob = config.num_ssm_layers() * (h_state_bytes + conv_state_bytes);
let marconi_bytes = marconi.slots * per_seq_blob;
let gdn_two_phase_bytes: usize = {
let key_dim = config.linear_num_key_heads * config.linear_key_head_dim;
let value_dim = config.linear_num_value_heads * config.linear_value_head_dim;
let nv = config.linear_num_value_heads;
let conv_dim = key_dim * 2 + value_dim;
if conv_dim > 0 && config.num_ssm_layers() > 0 {
let sl = max_batch_tokens_pre;
sl * conv_dim * 2 + sl * nv * 2 * 4 + sl * value_dim * 2 + sl * value_dim * 2
} else {
0
}
};
let per_sequence_bytes = per_sequence_reserve(args, config) / ceiling.max(1);
let mut plan = reserve_plan::ReservePlan {
config: config.clone(),
spec: spec_on_pool,
num_drafts: pool_num_drafts,
uniform_h: args.dflash,
rollback,
h_f16_pool,
marconi_bytes,
gdn_two_phase_bytes,
budget_bytes: (post_load.total_mem as f64 * args.gpu_memory_utilization) as usize,
per_sequence_bytes,
ring_slots: 0,
per_seq_blob,
};
let at_ceiling = plan.terms(ceiling)?;
let fixed_reserve = at_ceiling.fixed();
let ssm_pool_bytes = at_ceiling.ssm_pool;
let ssm_h_stage_bytes = at_ceiling.ssm_h_stage;
let runtime_headroom = at_ceiling.runtime;
let ring_requested = decode_ring::requested_slots(args, config);
let ring_slot_bytes = decode_ring::slot_bytes(args, per_seq_blob);
let yardstick =
headroom::post_load_yardstick(args, config, post_load, fixed_reserve, buffer_arena_bytes);
let fit = decode_ring::autofit(
args,
ring_requested,
ring_slot_bytes,
per_seq_blob,
fixed_reserve + buffer_arena_bytes,
free_mem,
&yardstick,
);
tracing::info!("{}", fit.decision);
if let Some(warning) = &fit.warning {
tracing::warn!("SSM decode-rollback ring auto-fit — {}", warning);
}
let ssm_snapshot_bytes = marconi_bytes + fit.slots * ring_slot_bytes;
plan.ring_slots = fit.slots;
let inference_reserve: usize = plan.inference_reserve(ceiling)?;
let total_reserve = inference_reserve + buffer_arena_bytes;
if total_reserve > free_mem {
return Err(refusal::reserve_refusal(
args,
config,
refusal::Refusal {
total_reserve,
free_mem,
seq_len_independent: ssm_pool_bytes
+ ssm_h_stage_bytes
+ ssm_snapshot_bytes
+ runtime_headroom.total(),
ring_requested,
ring_slots: fit.slots,
per_seq_blob,
ring_pinned: metrale_model_layers::ssm_reserve::published_decode_ring_slots()
.is_some(),
},
));
}
tracing::info!(
"Preflight reserve: inference={} MB, buffer_arena={} MB (pre-load free: {:.1} GB); {}",
inference_reserve / (1024 * 1024),
buffer_arena_bytes / (1024 * 1024),
free_mem as f64 / (1024.0 * 1024.0 * 1024.0),
decode_ring::formula(fit.slots, ceiling, per_seq_blob),
);
let t = plan.terms(ceiling)?;
let mb = |b: usize| b / (1024 * 1024);
tracing::info!(
"Preflight reserve terms at {} slot(s): ssm_pool={} MB, ssm_h_stage={} MB, \
replay_ring={} MB, marconi={} MB ({} slots), gdn_two_phase={} MB ({} tokens), \
runtime headroom={} MB (gdn carry stash {} + driver fixed {} + driver bookkeeping {}), \
per_sequence={} MB, decode_ring={} MB",
ceiling,
mb(t.ssm_pool),
mb(t.ssm_h_stage),
mb(t.replay_ring),
mb(t.marconi),
marconi.slots,
mb(t.gdn_two_phase),
max_batch_tokens_pre,
mb(t.runtime.total()),
mb(t.runtime.carry_stash),
mb(t.runtime.driver_fixed),
mb(t.runtime.driver_bookkeeping),
mb(t.per_sequence),
mb(t.decode_ring),
);
Ok(ReservePreflight {
inference_reserve,
buffer_arena_bytes,
gdn_two_phase_bytes,
ssm_prefill_chunk,
max_batch_tokens_pre,
plan,
})
}
pub(crate) fn spec_reserve_tokens(args: &cli::ServeArgs) -> usize {
if args.dflash {
args.serve_dflash_gamma() + 1
} else if args.speculative || args.self_speculative || args.ngram_speculative {
args.verify_pool_drafts() + 2
} else {
1
}
}