Skip to main content

candle_mi/diffusion/
config.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Configuration for the `MDLM` masked-diffusion backend.
4//!
5//! [`MdlmConfig`] is parsed from a `kuleshov-group/mdlm-owt`-style
6//! `config.json` (model type `"mdlm"`).  Unlike
7//! [`TransformerConfig`](crate::TransformerConfig), `MDLM` is a bidirectional
8//! `DiT` with `adaLN` conditioning, so it carries its own small config rather
9//! than reusing the causal-decoder one.
10
11use serde_json::Value;
12
13use crate::error::{MIError, Result};
14
15/// `MDLM` masked-diffusion model configuration.
16///
17/// Parsed from the upstream `config.json`.  The released `mdlm-owt`
18/// checkpoint is time-independent (`time_conditioning = false`), so the
19/// `DiT` conditioning vector is a constant computed once at load time.
20#[derive(Debug, Clone)]
21pub struct MdlmConfig {
22    /// Hidden dimension (`d_model`; upstream key `hidden_dim`).
23    pub hidden_dim: usize,
24    /// Number of `DiT` blocks (upstream key `n_blocks`).
25    pub n_blocks: usize,
26    /// Number of attention heads (upstream key `n_heads`).
27    pub n_heads: usize,
28    /// Per-head dimension (`hidden_dim / n_heads`).
29    pub head_dim: usize,
30    /// Conditioning dimension fed to the `adaLN` modulation (upstream key `cond_dim`).
31    pub cond_dim: usize,
32    /// Vocabulary size, including the `[MASK]` token (upstream key `vocab_size`).
33    pub vocab_size: usize,
34    /// Maximum sequence length (upstream key `model_length`).
35    pub model_length: usize,
36    /// Feed-forward expansion ratio (`MDLM` uses `4`; not stored in `config.json`).
37    pub mlp_ratio: usize,
38    /// Rotary base frequency (`MDLM` hard-codes `10000`).
39    pub rope_theta: f64,
40    /// `LayerNorm` epsilon (`MDLM` uses `1e-5`).
41    pub norm_eps: f64,
42    /// Token id of the absorbing `[MASK]` state (`vocab_size - 1`).
43    pub mask_token_id: u32,
44    /// Whether the network conditions on diffusion time.  The released
45    /// checkpoint sets this `false`, so the conditioning vector is constant.
46    pub time_conditioning: bool,
47}
48
49impl MdlmConfig {
50    /// Parse an [`MdlmConfig`] from a `HuggingFace` `config.json` value.
51    ///
52    /// # Errors
53    ///
54    /// Returns [`MIError::Config`] if a required key
55    /// (`hidden_dim`, `n_blocks`, `n_heads`, `cond_dim`, `vocab_size`) is
56    /// missing or not a non-negative integer, or if `hidden_dim` is not
57    /// divisible by `n_heads`.
58    pub fn from_hf_config(config: &Value) -> Result<Self> {
59        let hidden_dim = get_usize(config, "hidden_dim")?;
60        let n_heads = get_usize(config, "n_heads")?;
61        if n_heads == 0 || !hidden_dim.is_multiple_of(n_heads) {
62            return Err(MIError::Config(format!(
63                "hidden_dim {hidden_dim} not divisible by n_heads {n_heads}"
64            )));
65        }
66        let vocab_size = get_usize(config, "vocab_size")?;
67        // The absorbing [MASK] state is the final vocab index (GPT-2's 50257
68        // tokens 0..=50256 plus [MASK] = 50257, for vocab_size 50258).
69        let mask_token_id = u32::try_from(vocab_size.saturating_sub(1)).map_err(|e| {
70            MIError::Config(format!("vocab_size {vocab_size} does not fit in u32: {e}"))
71        })?;
72
73        Ok(Self {
74            hidden_dim,
75            n_blocks: get_usize(config, "n_blocks")?,
76            n_heads,
77            head_dim: hidden_dim / n_heads,
78            cond_dim: get_usize(config, "cond_dim")?,
79            vocab_size,
80            model_length: get_usize_or(config, "model_length", 1024),
81            mlp_ratio: 4,
82            rope_theta: 10_000.0,
83            norm_eps: 1e-5,
84            mask_token_id,
85            time_conditioning: get_bool_or(config, "time_conditioning", false),
86        })
87    }
88}
89
90/// Read a required non-negative integer config field as `usize`.
91///
92/// # Errors
93///
94/// Returns [`MIError::Config`] if the key is absent or
95/// not a `u64`.
96fn get_usize(config: &Value, key: &str) -> Result<usize> {
97    let value = config
98        .get(key)
99        .and_then(Value::as_u64)
100        .ok_or_else(|| MIError::Config(format!("missing or non-integer `{key}` in MDLM config")))?;
101    // CAST: u64 → usize, model dimensions fit in usize on 64-bit targets
102    #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
103    Ok(value as usize)
104}
105
106/// Read an optional non-negative integer config field as `usize`, falling
107/// back to `default` when absent.
108fn get_usize_or(config: &Value, key: &str, default: usize) -> usize {
109    config
110        .get(key)
111        .and_then(Value::as_u64)
112        .map_or(default, |v| {
113            // CAST: u64 → usize, model dimensions fit in usize on 64-bit targets
114            #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
115            {
116                v as usize
117            }
118        })
119}
120
121/// Read an optional boolean config field, falling back to `default` when absent.
122fn get_bool_or(config: &Value, key: &str, default: bool) -> bool {
123    config.get(key).and_then(Value::as_bool).unwrap_or(default)
124}
125
126#[cfg(test)]
127#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
128mod tests {
129    use super::*;
130
131    fn owt_config() -> Value {
132        serde_json::json!({
133            "model_type": "mdlm",
134            "hidden_dim": 768,
135            "n_blocks": 12,
136            "n_heads": 12,
137            "cond_dim": 128,
138            "vocab_size": 50258,
139            "model_length": 1024,
140            "time_conditioning": false
141        })
142    }
143
144    #[test]
145    fn parses_mdlm_owt_config() {
146        let cfg = MdlmConfig::from_hf_config(&owt_config()).unwrap();
147        assert_eq!(cfg.hidden_dim, 768);
148        assert_eq!(cfg.n_blocks, 12);
149        assert_eq!(cfg.n_heads, 12);
150        assert_eq!(cfg.head_dim, 64);
151        assert_eq!(cfg.cond_dim, 128);
152        assert_eq!(cfg.vocab_size, 50258);
153        assert_eq!(cfg.model_length, 1024);
154        assert_eq!(cfg.mask_token_id, 50257);
155        assert!(!cfg.time_conditioning);
156    }
157
158    #[test]
159    fn rejects_indivisible_head_count() {
160        let mut cfg = owt_config();
161        cfg["n_heads"] = serde_json::json!(7);
162        assert!(MdlmConfig::from_hf_config(&cfg).is_err());
163    }
164
165    #[test]
166    fn missing_required_key_errors() {
167        let mut cfg = owt_config();
168        // BORROW: remove a required key to exercise the error path.
169        cfg.as_object_mut().unwrap().remove("hidden_dim");
170        assert!(MdlmConfig::from_hf_config(&cfg).is_err());
171    }
172}