use anyhow::Result;
use metrale_config::ModelConfig;
use crate::cli;
pub(super) fn ssm_h_fp16_preconditions(args: &cli::ServeArgs, config: &ModelConfig) -> Result<()> {
if !metrale_model_layers::layers::qwen3_ssm::ssm_h_fp16_enabled()
|| config.num_ssm_layers() == 0
{
return Ok(());
}
if args.dflash
&& args
.dflash_gamma
.is_some_and(|g| g > metrale_model_layers::layers::qwen3_ssm::MAX_F16_TWIN_DFLASH_GAMMA)
{
anyhow::bail!(
"--ssm-h-dtype f16 with --dflash-gamma {} : the verify width is gamma + 1 = {}, \
which dispatches gated_delta_rule_wy17 — the one WY family with no FP16 h-state \
twin, and the one that strides its h intermediates in FP32 elements. An FP32 \
kernel over an FP16 h-state emits fluent garbage rather than an error. Use \
--dflash-gamma <= {}, drop --dflash, or use --ssm-h-dtype f32.",
args.dflash_gamma.unwrap_or_default(),
args.dflash_gamma.unwrap_or_default() + 1,
metrale_model_layers::layers::qwen3_ssm::MAX_F16_TWIN_DFLASH_GAMMA,
);
}
if args.self_speculative || args.ngram_speculative {
anyhow::bail!(
"--ssm-h-dtype f16 supports --speculative (MTP) only. The self-speculative and \
ngram-speculative verify paths still write the h-state as FP32, and an FP32 \
kernel over an FP16 pool produces fluent garbage rather than an error. Run \
without --self-speculative/--ngram-speculative, or use --ssm-h-dtype f32."
);
}
if args.speculative && args.resolved_num_drafts() > 3 {
anyhow::bail!(
"--ssm-h-dtype f16 supports up to 3 drafts (K <= 4 verify rows); --num-drafts is \
{}. Wider verify widths dispatch the wyN (K=5..8) / wy17 arms, which have no \
FP16 h-state twin. Lower --num-drafts to 3, or use --ssm-h-dtype f32.",
args.resolved_num_drafts()
);
}
if !metrale_model_layers::layers::qwen3_ssm::gdn_fused_norm_enabled() {
anyhow::bail!(
"--ssm-h-dtype f16 requires --gdn-fused-norm — the non-fused decode arms \
(gated_delta_rule_decode, ..._decode_f32_strided) have no FP16 twin in stage 1."
);
}
if std::env::var("METRALE_GDN_FUSED_CONV").ok().as_deref() == Some("1") {
anyhow::bail!(
"--ssm-h-dtype f16 is incompatible with METRALE_GDN_FUSED_CONV=1 — gated_delta_rule_decode_f32_conv_norm has no FP16 twin in stage 1."
);
}
if config.linear_key_head_dim != 128 || config.linear_value_head_dim != 128 {
anyhow::bail!(
"--ssm-h-dtype f16 needs linear head dims 128/128 (the FP16 twins size their shared memory for k_dim == 128); this model is {}/{}",
config.linear_key_head_dim,
config.linear_value_head_dim
);
}
if metrale_model_layers::layers::qwen3_ssm::ssm_h_f16_pool_enabled() {
tracing::info!(
"--ssm-h-dtype f16-pool: GDN h-state stored FP16 AND every h pool SIZED at 2 \
bytes/element. Prefill runs its unchanged FP32 kernels over a per-slot FP32 \
staging blob and narrows back, so the pool holds FP16 at all times. NUMERICS: \
the recurrence now carries FP16 state across prefill chunk boundaries as well \
as decode steps, with round-to-nearest-even and no stochastic rounding — do NOT \
publish a number from this mode without ssm-state-poisoning-gate, decode-floor, \
bfcl-subset and the agentic gate."
);
} else {
tracing::info!(
"--ssm-h-dtype f16: GDN h-state stored FP16 during decode AND MTP verify (pool \
stays FP32-sized; prefill unchanged). Scan replica at n=128: 183 -> 84 ms/step."
);
}
Ok(())
}