Skip to main content

cortiq_engine/
moss_imagegen.rs

1//! Staged native MOSS-Image generation.
2//!
3//! A MOSS CMF is intentionally one portable file, but it is *not* resident
4//! as one GPU model: Qwen3.5 conditioning is loaded/run/dropped first, then
5//! the MOSS DiT owns the device through the Euler loop, then Flux2 VAE is
6//! loaded for decoding.  This preserves the official component-offload
7//! contract and makes the q4tp profile viable on 24 GB Vulkan and Metal.
8
9use crate::Pipeline;
10use crate::moss_image::MossDit;
11use crate::sampler::{SamplerConfig, SplitMix64};
12use crate::vae::Flux2VaeDecoder;
13use cortiq_core::CmfModel;
14use std::path::Path;
15use std::sync::Arc;
16use std::time::Instant;
17
18/// `header.arch.arch_name` written by `imagine-pack` for MOSS-Image.
19pub const ARCH_NAME: &str = "moss_image";
20pub const MAX_TOKENS: usize = 4096;
21
22/// The portable defaults serialized by the MOSS packer.  The source release
23/// uses 1024², 50 flow-matching Euler steps, CFG 3, shift 5, and skips main
24/// layer 22 only for the unconditional pass.
25#[derive(Clone, Debug)]
26pub struct MossDefaults {
27    pub height: usize,
28    pub width: usize,
29    pub steps: usize,
30    pub cfg: f32,
31    pub shift: f32,
32    pub negative_prompt: String,
33    pub max_sequence_length: usize,
34    pub uncond_skip_layers: Vec<usize>,
35}
36
37impl Default for MossDefaults {
38    fn default() -> Self {
39        Self {
40            height: 1024,
41            width: 1024,
42            steps: 50,
43            cfg: 3.0,
44            shift: 5.0,
45            negative_prompt: String::new(),
46            max_sequence_length: MAX_TOKENS,
47            uncond_skip_layers: vec![22],
48        }
49    }
50}
51
52impl MossDefaults {
53    pub fn from_json(v: &serde_json::Value) -> Self {
54        let d = Self::default();
55        let usize_or =
56            |key: &str, fallback| v[key].as_u64().map(|x| x as usize).unwrap_or(fallback);
57        let layers = v["uncond_skip_layers"]
58            .as_array()
59            .map(|a| {
60                a.iter()
61                    .filter_map(serde_json::Value::as_u64)
62                    .map(|i| i as usize)
63                    .collect()
64            })
65            .unwrap_or(d.uncond_skip_layers.clone());
66        Self {
67            height: usize_or("height", d.height),
68            width: usize_or("width", d.width),
69            steps: usize_or("steps", d.steps),
70            cfg: v["cfg"].as_f64().map(|x| x as f32).unwrap_or(d.cfg),
71            shift: v["shift"].as_f64().map(|x| x as f32).unwrap_or(d.shift),
72            negative_prompt: v["negative_prompt"]
73                .as_str()
74                .unwrap_or(&d.negative_prompt)
75                .to_string(),
76            max_sequence_length: usize_or("max_sequence_length", d.max_sequence_length),
77            uncond_skip_layers: layers,
78        }
79    }
80
81    pub fn of(model: &CmfModel) -> Self {
82        model
83            .tensor_bytes("moss.defaults_json")
84            .ok()
85            .and_then(|b| serde_json::from_slice::<serde_json::Value>(b).ok())
86            .map(|v| Self::from_json(&v))
87            .unwrap_or_default()
88    }
89}
90
91/// Scheduler facts that alter the numerical Euler grid. They live in the
92/// container so a future MOSS variant cannot silently inherit this release's
93/// 1000-step / training-shift-1 assumptions.
94#[derive(Clone, Copy, Debug)]
95struct MossScheduler {
96    num_train_timesteps: usize,
97    training_shift: f32,
98}
99
100impl MossScheduler {
101    fn of(model: &CmfModel) -> Result<Self, String> {
102        let json: serde_json::Value = serde_json::from_slice(
103            model
104                .tensor_bytes("moss.scheduler_json")
105                .map_err(|e| e.to_string())?,
106        )
107        .map_err(|e| format!("moss.scheduler_json: {e}"))?;
108        let num_train_timesteps = json["num_train_timesteps"]
109            .as_u64()
110            .ok_or("MOSS scheduler missing num_train_timesteps")?
111            as usize;
112        let training_shift = json["training_shift"]
113            .as_f64()
114            .ok_or("MOSS scheduler missing training_shift")? as f32;
115        if num_train_timesteps < 2 || !training_shift.is_finite() || training_shift <= 0.0 {
116            return Err("MOSS scheduler has invalid training grid/shift".into());
117        }
118        Ok(Self {
119            num_train_timesteps,
120            training_shift,
121        })
122    }
123}
124
125/// User-overridable MOSS image recipe.
126#[derive(Clone, Debug)]
127pub struct MossParams {
128    pub height: usize,
129    pub width: usize,
130    pub steps: usize,
131    pub cfg: f32,
132    pub shift: f32,
133    pub max_tokens: usize,
134    pub seed: u64,
135    pub num_images: usize,
136    pub negative_prompt: Option<String>,
137    /// `None` means the source release default `[22]`; `Some(vec![])`
138    /// intentionally disables the negative branch skip.
139    pub uncond_skip_layers: Option<Vec<usize>>,
140    /// Optional positive-branch skip-layer guidance (SLG) block indices.
141    /// An empty list disables the third DiT forward.
142    pub slg_layers: Vec<usize>,
143    /// SLG correction strength. It is ignored when `slg_layers` is empty.
144    pub slg_scale: f32,
145    /// Inclusive start of the denoising-fraction interval where both source
146    /// negative-branch skipping and SLG are active.
147    pub slg_start: f32,
148    /// Exclusive end of the denoising-fraction interval where both source
149    /// negative-branch skipping and SLG are active.
150    pub slg_stop: f32,
151}
152
153impl MossParams {
154    pub fn from_defaults(d: &MossDefaults) -> Self {
155        Self {
156            height: d.height,
157            width: d.width,
158            steps: d.steps,
159            cfg: d.cfg,
160            shift: d.shift,
161            max_tokens: d.max_sequence_length,
162            seed: 42,
163            num_images: 1,
164            negative_prompt: None,
165            uncond_skip_layers: None,
166            slg_layers: Vec::new(),
167            slg_scale: 0.0,
168            slg_start: 0.0,
169            slg_stop: 1.0,
170        }
171    }
172}
173
174impl Default for MossParams {
175    fn default() -> Self {
176        Self::from_defaults(&MossDefaults::default())
177    }
178}
179
180/// Generated RGB image, row-major interleaved u8.
181pub struct MossImage {
182    pub rgb: Vec<u8>,
183    pub height: usize,
184    pub width: usize,
185    pub seed: u64,
186}
187
188impl MossImage {
189    pub fn save(&self, path: &Path) -> Result<(), String> {
190        if path
191            .extension()
192            .and_then(|e| e.to_str())
193            .is_some_and(|e| e.eq_ignore_ascii_case("ppm"))
194        {
195            let mut ppm = format!("P6\n{} {}\n255\n", self.width, self.height).into_bytes();
196            ppm.extend_from_slice(&self.rgb);
197            return std::fs::write(path, ppm).map_err(|e| format!("{}: {e}", path.display()));
198        }
199        image::RgbImage::from_raw(self.width as u32, self.height as u32, self.rgb.clone())
200            .ok_or("MOSS output dimensions overflow")?
201            .save(path)
202            .map_err(|e| format!("{}: {e}", path.display()))
203    }
204}
205
206#[derive(Clone, Debug, Default)]
207pub struct MossTimings {
208    pub text_encode: f64,
209    pub dit_load: f64,
210    pub text_prepare: f64,
211    pub steps: Vec<f64>,
212    pub vae: f64,
213    pub total: f64,
214}
215
216impl MossTimings {
217    pub fn median_step(&self) -> f64 {
218        if self.steps.is_empty() {
219            return 0.0;
220        }
221        let mut v = self.steps.clone();
222        v.sort_by(f64::total_cmp);
223        v[v.len() / 2]
224    }
225}
226
227/// Source MOSS V2 grid: shifted inference samples are snapped to the nearest
228/// unshifted training sigma grid before Euler integration.  The returned
229/// vector includes terminal zero; its denoising length can be lower than the
230/// requested count only when nearest-grid snaps coalesce adjacent values.
231pub fn moss_sigmas(steps: usize, shift: f32, training_shift: f32) -> Result<Vec<f32>, String> {
232    moss_sigmas_for_training_grid(steps, shift, training_shift, 1000)
233}
234
235fn moss_sigmas_for_training_grid(
236    steps: usize,
237    shift: f32,
238    training_shift: f32,
239    total: usize,
240) -> Result<Vec<f32>, String> {
241    if steps == 0
242        || total < 2
243        || !shift.is_finite()
244        || !training_shift.is_finite()
245        || shift <= 0.0
246        || training_shift <= 0.0
247    {
248        return Err("MOSS steps and shifts must be finite positive values".into());
249    }
250    let grid = |count: usize, s: f32| -> Vec<f32> {
251        let lower = s / (total as f32 + s - 1.0);
252        if count == 1 {
253            return vec![1.0];
254        }
255        (0..count)
256            .map(|i| {
257                let v = 1.0 + (lower - 1.0) * (i as f32 / (count - 1) as f32);
258                s * v / (1.0 + (s - 1.0) * v)
259            })
260            .collect()
261    };
262    let proposed = grid(steps, shift);
263    let mut training = grid(total, training_shift);
264    training.reverse();
265    let mut snapped = Vec::with_capacity(steps + 1);
266    for sigma in proposed {
267        let upper = training.partition_point(|v| *v < sigma).min(total - 1);
268        let lower = upper.saturating_sub(1);
269        let choose = if (sigma - training[lower]).abs() <= (training[upper] - sigma).abs() {
270            lower
271        } else {
272            upper
273        };
274        let value = training[choose];
275        if snapped.last().copied() != Some(value) {
276            snapped.push(value);
277        }
278    }
279    if snapped.is_empty()
280        || snapped
281            .windows(2)
282            .any(|s| !s[0].is_finite() || s[0] <= s[1])
283    {
284        return Err("MOSS scheduler produced an invalid non-descending sigma grid".into());
285    }
286    snapped.push(0.0);
287    Ok(snapped)
288}
289
290fn prompt_ids(pipeline: &Pipeline, prompt: &str, max: usize) -> Result<Vec<u32>, String> {
291    if max == 0 || max > MAX_TOKENS {
292        return Err(format!("--max-sequence-length must be in 1..={MAX_TOKENS}"));
293    }
294    let messages = [("user".to_string(), prompt.to_string())];
295    // The stored official Jinja template receives add_generation_prompt=true
296    // and enable_thinking=true, exactly as MOSSImagePipeline.encode_prompt.
297    let mut ids = pipeline
298        .tokenizer
299        .apply_chat_template_opts(&messages, Some(true));
300    ids.truncate(max);
301    if ids.is_empty() {
302        return Err("MOSS template/tokenizer produced no tokens".into());
303    }
304    Ok(ids)
305}
306
307fn gauss(n: usize, seed: u64) -> Vec<f32> {
308    let mut rng = SplitMix64::new(seed);
309    let mut uniform = || (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
310    let mut out = Vec::with_capacity(n);
311    while out.len() < n {
312        let a = uniform().max(1e-300);
313        let b = uniform();
314        let r = (-2.0 * a.ln()).sqrt();
315        let phase = 2.0 * std::f64::consts::PI * b;
316        out.push((r * phase.cos()) as f32);
317        if out.len() < n {
318            out.push((r * phase.sin()) as f32);
319        }
320    }
321    out
322}
323
324/// The official pipeline measures the active skip/SLG interval over the
325/// scheduler's *actual* denoising step count, with an inclusive start and an
326/// exclusive stop. Keeping this pure makes the boundary contract explicit.
327fn moss_skip_active(step: usize, total_steps: usize, start: f32, stop: f32) -> bool {
328    total_steps > 0
329        && start <= step as f32 / (total_steps as f32)
330        && step as f32 / (total_steps as f32) < stop
331}
332
333/// Apply MOSS's source CFG/SLG formulation in fp32. `prediction` begins as
334/// the positive conditional prediction. If SLG is active, `conditional` is a
335/// retained copy of that original positive forward and `skipped` is the extra
336/// skipped-positive forward. This matches `MOSSImagePipeline.__call__` exactly:
337/// CFG first, then the SLG correction; without CFG SLG starts from `skipped`.
338fn apply_moss_guidance(
339    prediction: &mut [f32],
340    conditional: Option<&[f32]>,
341    uncond: Option<&[f32]>,
342    cfg: f32,
343    skipped: Option<&[f32]>,
344    slg_scale: f32,
345) -> Result<(), String> {
346    if let Some(uncond) = uncond {
347        if uncond.len() != prediction.len() {
348            return Err("MOSS unconditional prediction shape mismatch".into());
349        }
350        for (v, &u) in prediction.iter_mut().zip(uncond) {
351            *v = u + cfg * (*v - u);
352        }
353    }
354    match (conditional, skipped) {
355        (None, None) => Ok(()),
356        (Some(conditional), Some(skipped)) => {
357            if conditional.len() != prediction.len() || skipped.len() != prediction.len() {
358                return Err("MOSS SLG prediction shape mismatch".into());
359            }
360            if uncond.is_some() {
361                for ((v, &c), &s) in prediction.iter_mut().zip(conditional).zip(skipped) {
362                    *v += slg_scale * (c - s);
363                }
364            } else {
365                for ((v, &c), &s) in prediction.iter_mut().zip(conditional).zip(skipped) {
366                    *v = s + slg_scale * (c - s);
367                }
368            }
369            Ok(())
370        }
371        _ => Err("MOSS SLG needs both conditional and skipped predictions".into()),
372    }
373}
374
375fn pixels_to_u8(rgb: &[f32], height: usize, width: usize) -> Result<Vec<u8>, String> {
376    let plane = height
377        .checked_mul(width)
378        .ok_or("MOSS image dimensions overflow")?;
379    if rgb.len() != plane * 3 {
380        return Err(format!(
381            "Flux2 RGB has {} values, expected {}",
382            rgb.len(),
383            plane * 3
384        ));
385    }
386    let mut out = vec![0u8; plane * 3];
387    for px in 0..plane {
388        for c in 0..3 {
389            out[px * 3 + c] =
390                ((rgb[c * plane + px] * 0.5 + 0.5).clamp(0.0, 1.0) * 255.0).round_ties_even() as u8;
391        }
392    }
393    Ok(out)
394}
395
396/// Generate MOSS images from a complete single-file CMF.  Stages deliberately
397/// own independent device scopes, so mapped Qwen3.5 weights are released
398/// before the DiT, and DiT planes are released before the VAE decoder.
399pub fn generate_images(
400    model_path: &Path,
401    prompt: &str,
402    p: &MossParams,
403    mut progress: impl FnMut(usize, usize, usize),
404) -> Result<(Vec<MossImage>, MossTimings), String> {
405    if p.height == 0 || p.width == 0 || p.height % 16 != 0 || p.width % 16 != 0 {
406        return Err(format!(
407            "MOSS --height/--width must be positive multiples of 16 (got {}x{})",
408            p.width, p.height
409        ));
410    }
411    if p.height > 4096 || p.width > 4096 {
412        return Err("MOSS dimensions may not exceed 4096".into());
413    }
414    if p.num_images == 0
415        || p.steps == 0
416        || !p.cfg.is_finite()
417        || p.cfg < 0.0
418        || !p.shift.is_finite()
419        || p.shift <= 0.0
420    {
421        return Err("invalid MOSS steps, cfg or shift".into());
422    }
423    if !p.slg_scale.is_finite() || p.slg_scale < 0.0 {
424        return Err("MOSS --slg-scale must be finite and nonnegative".into());
425    }
426    if !p.slg_start.is_finite()
427        || !p.slg_stop.is_finite()
428        || p.slg_start < 0.0
429        || p.slg_start > p.slg_stop
430        || p.slg_stop > 1.0
431    {
432        return Err("MOSS requires 0 <= --slg-start <= --slg-stop <= 1".into());
433    }
434    let all_started = Instant::now();
435    let model = CmfModel::open(model_path).map_err(|e| format!("{}: {e}", model_path.display()))?;
436    if model.header.arch.arch_name != ARCH_NAME {
437        return Err(format!(
438            "{} has architecture {:?}, expected {ARCH_NAME}",
439            model_path.display(),
440            model.header.arch.arch_name
441        ));
442    }
443    let defaults = MossDefaults::of(&model);
444    let scheduler = MossScheduler::of(&model)?;
445    let neg_prompt = p
446        .negative_prompt
447        .clone()
448        .unwrap_or_else(|| defaults.negative_prompt.clone());
449    // The official path performs CFG whenever cfg != 1 (including cfg=0).
450    let do_cfg = p.cfg != 1.0;
451    let mut timing = MossTimings::default();
452
453    // Every component gets a separately opened CMF mapping below.  This is
454    // intentional rather than redundant: backend weight caches are keyed by
455    // a model UID.  Dropping each stage's mapping before the next one makes
456    // text → DiT → VAE *actual* staged GPU residency on Metal as well as
457    // WGPU/Vulkan, while the OS page cache still shares the file contents.
458    drop(model);
459
460    // ── Qwen3.5 conditioning (then release all text weights/device maps) ──
461    let text_started = Instant::now();
462    let (positive, positive_tokens, negative, negative_tokens) = {
463        // Register this file with the stage guard even when the selected
464        // backend ultimately declines text kernels: a future opt-in
465        // `CMF_MOSS_TE_GPU=1` must not leave its mapped Qwen planes resident
466        // when the much larger DiT begins.
467        let mut stage = crate::gpu::image_stage_scope();
468        let stage_model = Arc::new(
469            CmfModel::open(model_path).map_err(|e| format!("{}: {e}", model_path.display()))?,
470        );
471        stage.track_model(stage_model.uid());
472        // Hybrid GDN text encoding is normally faster/more stable on the
473        // host; opt in to its device experiments with CMF_MOSS_TE_GPU=1.
474        let _cpu =
475            (std::env::var("CMF_MOSS_TE_GPU").as_deref() != Ok("1")).then(crate::gpu::pause_gpu);
476        let mut encoder = Pipeline::from_encoder_model(&stage_model, SamplerConfig::default())
477            .map_err(|e| format!("MOSS Qwen3.5 encoder: {e}"))?;
478        let pos_ids = prompt_ids(&encoder, prompt, p.max_tokens)?;
479        let pos_tokens = pos_ids.len();
480        let pos = encoder.encode_hidden_states(&pos_ids)?;
481        let (neg, neg_tokens) = if do_cfg {
482            let ids = prompt_ids(&encoder, &neg_prompt, p.max_tokens)?;
483            let n = ids.len();
484            (Some(encoder.encode_hidden_states(&ids)?), Some(n))
485        } else {
486            (None, None)
487        };
488        (pos, pos_tokens, neg, neg_tokens)
489    };
490    timing.text_encode = text_started.elapsed().as_secs_f64();
491
492    // Loading/dequant metadata of Flux2 is cheap compared with the 42-block
493    // DiT and can happen on a CPU thread while denoising uses the GPU.  Do
494    // not prewarm its device chain here: Metal/WGPU contexts are shared, and
495    // concurrent queue use would defeat the staged-residency contract (and
496    // has historically been a source of driver hangs).
497    let vae_loader = {
498        let path = model_path.to_path_buf();
499        std::thread::spawn(move || -> Result<Flux2VaeDecoder, String> {
500            let stage_model =
501                Arc::new(CmfModel::open(&path).map_err(|e| format!("{}: {e}", path.display()))?);
502            Flux2VaeDecoder::from_cmf(&stage_model)
503        })
504    };
505
506    // ── MOSS DiT ──
507    let (latents, latent_h, latent_w) = {
508        let mut stage = crate::gpu::image_stage_scope();
509        let stage_model = Arc::new(
510            CmfModel::open(model_path).map_err(|e| format!("{}: {e}", model_path.display()))?,
511        );
512        stage.track_model(stage_model.uid());
513        let started = Instant::now();
514        let dit = MossDit::from_cmf(&stage_model)?;
515        timing.dit_load = started.elapsed().as_secs_f64();
516        let positive = dit.prepare_text(&positive, positive_tokens)?;
517        let negative = match (&negative, negative_tokens) {
518            (Some(v), Some(n)) => Some((dit.prepare_text(v, n)?, n)),
519            _ => None,
520        };
521        timing.text_prepare = started.elapsed().as_secs_f64() - timing.dit_load;
522        let sigmas = moss_sigmas_for_training_grid(
523            p.steps,
524            p.shift,
525            scheduler.training_shift,
526            scheduler.num_train_timesteps,
527        )?;
528        let actual_steps = sigmas.len() - 1;
529        let skips = p
530            .uncond_skip_layers
531            .as_deref()
532            .unwrap_or(&defaults.uncond_skip_layers);
533        if skips.iter().any(|&i| i >= dit.cfg.num_layers) {
534            return Err(format!(
535                "MOSS uncond skip layer outside 0..{}",
536                dit.cfg.num_layers
537            ));
538        }
539        if p.slg_layers.iter().any(|&i| i >= dit.cfg.num_layers) {
540            return Err(format!("MOSS SLG layer outside 0..{}", dit.cfg.num_layers));
541        }
542        let (lh, lw) = (p.height / 16, p.width / 16);
543        let mut completed = Vec::with_capacity(p.num_images);
544        for image in 0..p.num_images {
545            let mut latent = gauss(
546                dit.cfg.latent_channels * lh * lw,
547                p.seed.wrapping_add(image as u64),
548            );
549            for step in 0..actual_steps {
550                let clock = Instant::now();
551                // The source FlowMatch scheduler exposes timestep = sigma ×
552                // its recorded training-grid length.
553                let t = sigmas[step] * scheduler.num_train_timesteps as f32;
554                // The source defines this interval over actual scheduler
555                // timesteps (after its nearest-grid snap), not requested
556                // `--steps`.  It gates both the negative-branch skip and
557                // optional positive-branch SLG.
558                let skip_active = moss_skip_active(step, actual_steps, p.slg_start, p.slg_stop);
559                let mut prediction =
560                    dit.forward(&latent, lh, lw, t, &positive, positive_tokens, &[])?;
561                let use_slg = skip_active && !p.slg_layers.is_empty() && p.slg_scale > 0.0;
562                let conditional = use_slg.then(|| prediction.clone());
563                let uncond = if let Some((negative, neg_len)) = &negative {
564                    let uncond_skips = if skip_active { skips } else { &[] };
565                    Some(dit.forward(&latent, lh, lw, t, negative, *neg_len, uncond_skips)?)
566                } else {
567                    None
568                };
569                let skipped = if conditional.is_some() {
570                    Some(dit.forward(
571                        &latent,
572                        lh,
573                        lw,
574                        t,
575                        &positive,
576                        positive_tokens,
577                        &p.slg_layers,
578                    )?)
579                } else {
580                    None
581                };
582                apply_moss_guidance(
583                    &mut prediction,
584                    conditional.as_deref(),
585                    uncond.as_deref(),
586                    p.cfg,
587                    skipped.as_deref(),
588                    p.slg_scale,
589                )?;
590                let dt = sigmas[step + 1] - sigmas[step];
591                for (x, v) in latent.iter_mut().zip(&prediction) {
592                    *x += dt * v;
593                }
594                if latent.iter().any(|v| !v.is_finite()) {
595                    return Err(format!(
596                        "MOSS image {image}: non-finite latent at step {}",
597                        step + 1
598                    ));
599                }
600                timing.steps.push(clock.elapsed().as_secs_f64());
601                progress(image, step + 1, actual_steps);
602            }
603            completed.push(latent);
604        }
605        (completed, lh, lw)
606    };
607
608    // ── Flux2 VAE ──
609    let vae_started = Instant::now();
610    let images = {
611        let _stage = crate::gpu::image_stage_scope();
612        let vae = vae_loader
613            .join()
614            .map_err(|_| "MOSS VAE loader panicked".to_string())??;
615        // Prewarm only after the DiT stage and its owner buffers are gone.
616        // This keeps the VAE's reusable chain hot without overlapping two
617        // independent GPU command streams.
618        if crate::gpu::enabled_here() {
619            let _ = vae.prewarm();
620        }
621        let mut images = Vec::with_capacity(latents.len());
622        for (image, latent) in latents.iter().enumerate() {
623            let rgb = vae.decode_fast(latent, latent_h, latent_w)?;
624            images.push(MossImage {
625                rgb: pixels_to_u8(&rgb, p.height, p.width)?,
626                height: p.height,
627                width: p.width,
628                seed: p.seed.wrapping_add(image as u64),
629            });
630        }
631        images
632    };
633    timing.vae = vae_started.elapsed().as_secs_f64();
634    timing.total = all_started.elapsed().as_secs_f64();
635    Ok((images, timing))
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641
642    fn contract() -> serde_json::Value {
643        serde_json::from_str(include_str!(concat!(
644            env!("CARGO_MANIFEST_DIR"),
645            "/tests/fixtures/moss_image_contract.json"
646        )))
647        .expect("valid MOSS contract fixture")
648    }
649
650    fn f32_le(path: &std::path::Path) -> Vec<f32> {
651        let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
652        assert_eq!(bytes.len() % 4, 0, "{} is f32-le", path.display());
653        bytes
654            .chunks_exact(4)
655            .map(|b| f32::from_le_bytes(b.try_into().expect("four bytes")))
656            .collect()
657    }
658
659    #[test]
660    fn scheduler_is_descending_and_ends_at_zero() {
661        let s = moss_sigmas(50, 5.0, 1.0).expect("valid scheduler");
662        assert_eq!(s.first().copied(), Some(1.0));
663        assert_eq!(s.last().copied(), Some(0.0));
664        assert!(s.windows(2).all(|p| p[0] > p[1]));
665    }
666
667    /// Keep the portable scheduler tied to values emitted by the pinned
668    /// official `MOSSImageScheduler`, not just its qualitative shape.  The
669    /// source's `torch.linspace` operates in f32, hence a small tolerance
670    /// for values such as 0.46300003 that occur after shift/snap arithmetic.
671    #[test]
672    fn scheduler_matches_official_contract() {
673        let fixture = contract();
674        for schedule in fixture["scheduler"]["schedules"]
675            .as_array()
676            .expect("scheduler cases")
677        {
678            let requested = schedule["steps_requested"].as_u64().expect("step count") as usize;
679            let want = schedule["sigmas"].as_array().expect("sigmas");
680            let got = moss_sigmas(requested, 5.0, 1.0).expect("official schedule");
681            assert_eq!(got.len(), want.len(), "{requested} steps");
682            for (index, (actual, expected)) in got.iter().zip(want).enumerate() {
683                let expected = expected.as_f64().expect("numeric sigma") as f32;
684                assert!(
685                    (actual - expected).abs() <= 2e-6,
686                    "{requested} steps, sigma {index}: got {actual:?}, expected {expected:?}"
687                );
688            }
689        }
690    }
691
692    #[test]
693    fn skip_guidance_window_matches_source_boundaries() {
694        // The source uses `start <= step_index / num_steps < stop`.
695        assert!(moss_skip_active(0, 10, 0.0, 1.0));
696        assert!(moss_skip_active(9, 10, 0.0, 1.0));
697        assert!(!moss_skip_active(10, 10, 0.0, 1.0));
698        assert!(!moss_skip_active(0, 10, 0.1, 0.9));
699        assert!(moss_skip_active(1, 10, 0.1, 0.9));
700        assert!(!moss_skip_active(9, 10, 0.1, 0.9));
701        assert!(!moss_skip_active(0, 0, 0.0, 1.0));
702    }
703
704    #[test]
705    fn standard_slg_matches_source_cfg_order() {
706        let conditional = [2.0, -1.0];
707        let uncond = [1.0, 3.0];
708        let skipped = [0.0, 1.0];
709
710        // Source: CFG first, then `slg_scale * (cond - cond_skip)`.
711        let mut with_cfg = conditional;
712        apply_moss_guidance(
713            &mut with_cfg,
714            Some(&conditional),
715            Some(&uncond),
716            3.0,
717            Some(&skipped),
718            2.0,
719        )
720        .unwrap();
721        assert_eq!(with_cfg, [8.0, -13.0]);
722
723        // With guidance_scale=1 source replaces the normal conditional output
724        // with `skipped + slg_scale * (cond - skipped)`.
725        let mut without_cfg = conditional;
726        apply_moss_guidance(
727            &mut without_cfg,
728            Some(&conditional),
729            None,
730            1.0,
731            Some(&skipped),
732            2.0,
733        )
734        .unwrap();
735        assert_eq!(without_cfg, [4.0, -3.0]);
736    }
737
738    #[test]
739    fn fixed_rng_is_repeatable() {
740        assert_eq!(gauss(11, 42), gauss(11, 42));
741        assert_ne!(gauss(11, 42), gauss(11, 43));
742    }
743
744    /// This opt-in integration gate proves that the embedded tokenizer and
745    /// stored Jinja template preserve MOSS's nonstandard thinking suffix.
746    /// It deliberately needs a real packed CMF, so ordinary source-only CI
747    /// remains lightweight; the release pod sets `CMF_MOSS_TEST_MODEL`.
748    #[test]
749    fn packed_template_matches_source_contract_when_model_supplied() {
750        let Ok(path) = std::env::var("CMF_MOSS_TEST_MODEL") else {
751            return;
752        };
753        let model = Arc::new(CmfModel::open(&path).expect("open supplied MOSS CMF"));
754        assert_eq!(model.header.arch.arch_name, ARCH_NAME);
755        let encoder = Pipeline::from_encoder_model(&model, SamplerConfig::default())
756            .expect("open packed MOSS text encoder");
757        for case in contract()["tokenizer"]["cases"]
758            .as_array()
759            .expect("tokenizer cases")
760        {
761            let got = prompt_ids(
762                &encoder,
763                case["prompt"].as_str().expect("prompt"),
764                MAX_TOKENS,
765            )
766            .expect("apply stored MOSS template");
767            let want: Vec<u32> = case["ids"]
768                .as_array()
769                .expect("token ids")
770                .iter()
771                .map(|id| id.as_u64().expect("integer token") as u32)
772                .collect();
773            assert_eq!(got, want, "prompt {:?}", case["prompt"]);
774        }
775    }
776
777    /// End-to-end Flux2 decoder oracle.  The raw tensor fixtures are emitted
778    /// by the pinned official pipeline and intentionally live outside Git;
779    /// release validation supplies their directory on the pod.  Run it with
780    /// `CMF_GPU=0` for an architecture-independent f32 comparison.
781    #[test]
782    fn packed_flux2_decoder_matches_source_oracle_when_supplied() {
783        let (Ok(model_path), Ok(oracle_dir)) = (
784            std::env::var("CMF_MOSS_TEST_MODEL"),
785            std::env::var("CMF_MOSS_ORACLE_DIR"),
786        ) else {
787            return;
788        };
789        let model = CmfModel::open(&model_path).expect("open supplied MOSS CMF");
790        let vae = Flux2VaeDecoder::from_cmf(&model).expect("open packed Flux2 decoder");
791        let root = std::path::Path::new(&oracle_dir);
792        let packed = f32_le(&root.join("vae_packed_latent.f32"));
793        let want = f32_le(&root.join("vae_decode_ref.f32"));
794        let got = vae
795            .decode_fast(&packed, 3, 5)
796            .expect("decode source packed latent");
797        assert_eq!(got.len(), want.len());
798        let mut max_abs = 0.0f32;
799        let mut mean_abs = 0.0f64;
800        for (&actual, &expected) in got.iter().zip(&want) {
801            let delta = (actual - expected).abs();
802            max_abs = max_abs.max(delta);
803            mean_abs += delta as f64;
804        }
805        mean_abs /= got.len() as f64;
806        assert!(
807            max_abs <= 5e-3 && mean_abs <= 2e-4,
808            "Flux2 oracle drift: max_abs={max_abs:.6}, mean_abs={mean_abs:.6}"
809        );
810    }
811}