open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-lmm/tests/cascade_lfm2_vl.rs
Transaction Science 05dd2e0c4c jouleclaw Phase 4: model tiers — liquid + prism + mrl + lmm + ebm
Five tier crates ported. 589 tests pass across Phase 1-4, zero
failures, zero warnings.

jouleclaw-liquid
  Ported from pattern-lang/liquid (~1123 LOC, 5 files).
  L3 tier — Closed-form Continuous-time (CfC) recurrent language
  models. The SSM class with O(L) state evolution vs transformer
  O(L²) attention. This is the v0.3 "SSM bet" from the architecture
  proposal — already shipping in the JouleClaw reference impl on
  day one.

jouleclaw-prism
  Ported from pattern-lang/prism (~3066 LOC, 6 files).
  L3 tier — 1-bit / ternary BitNet-class kernels. Ternary transformer
  forward pass. The lowest-joule local-model tier. Provides the
  TernaryDecoder backbone that jouleclaw-lmm builds on.

jouleclaw-mrl
  Ported from pattern-lang/mrl (~1464 LOC, 7 files).
  L2 tier — Matryoshka Representation Learning nested embeddings.
  Sub-millijoule retrieval tier. Embedder + GGUF embedder +
  matryoshka truncation + nearest-neighbor picker + retrieval.

jouleclaw-lmm
  Ported from pattern-lang/lmm (~1819 LOC, 6 files).
  L3 tier — Large Multimodal Model. Vision + text + audio pipeline
  on top of jouleclaw-prism's TernaryDecoder backbone. Real VL path
  (LFM2.5-VL preprocessing via the `image` crate, JPEG/PNG only —
  keeps the dep footprint minimal). The internal `prism::` references
  rewritten to `jouleclaw_prism::` during port.

jouleclaw-ebm
  Ported from pattern-lang/ebm (~1738 LOC, 7 files).
  Energy-based constraint-satisfaction tier — SAT, n-queens, sudoku,
  graph coloring, generic solver. Deterministic compute priced in
  joules, sits at the L1/L3 boundary depending on the problem class.

Bare crate-name references in integration tests fixed:
  `use liquid::...` → `use jouleclaw_liquid::...` (and same for the
  other 4 ported tiers). Initial sed pass only caught `joule_*`
  patterns; this second pass caught the bare-name ones.

gemma4 deferred. It depends on `joule-l1` and `pattern-core`
(OpenIE IP that doesn't port). Will land in Phase 6 once the
thin `LawfulTier` trait surface ships in jouleclaw-cascade or a
new jouleclaw-l1 crate so consumers can plug their own lawful
lexicon in.

Phase 5 next: omni-modal generation — diffusion (40+ samplers from
efficient-genai's ALGORITHM_CATALOG), Stable Diffusion / SDXL / SD3 /
Flux paths, MusicGen, Whisper, Gaussian3D, video synthesis,
cross-modal fusion. Source codebase: efficient-genai.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 17:39:05 -04:00

98 lines
3.7 KiB
Rust

//! Cascade integration oracle: real LFM2-VL multimodal model dispatched
//! as an `LmmTier` inside the joule cascade.
//!
//! Loads `lfm2.5-vl-450m-q8_0.gguf` (text backbone) + matching mmproj
//! via `LmmTier::from_lfm_vl_gguf`, submits a `Multimodal` Query with a
//! real JPEG, asserts the returned Answer is `Text` non-empty, and
//! reports the kernel joule receipt.
//!
//! Slow (no KV cache on the multimodal path yet); 4 tokens of caption
//! is enough to prove the cascade integration without burning minutes.
//!
//! `#[ignore]` — needs the two GGUFs + a test image on disk.
//!
//! cargo test --release -p lmm --test cascade_lfm2_vl \
//! -- --ignored --nocapture
use jouleclaw_cascade::{
AnswerOutput, ContextRef, JouleBudget, QualityFloor,
Query, QueryInput, Tier, TraceOutcome,
};
use jouleclaw_lmm::LmmTier;
use std::time::Instant;
fn text_path() -> String {
std::env::var("JOULE_LFM2VL_TEXT")
.unwrap_or_else(|_| "../../models/lfm2.5-vl-450m-q8_0.gguf".into())
}
fn mmproj_path() -> String {
std::env::var("JOULE_LFM2VL_MMPROJ")
.unwrap_or_else(|_| "../../models/lfm2.5-vl-450m-mmproj-q8_0.gguf".into())
}
fn image_path() -> String {
std::env::var("JOULE_VL_IMAGE")
.unwrap_or_else(|_| "../../data/LARC/assets/collection.jpg".into())
}
#[test]
#[ignore]
fn lfm2_vl_dispatches_as_lmm_tier_with_real_caption() {
let t0 = Instant::now();
let max_new = std::env::var("JOULE_VL_MAX_NEW")
.ok().and_then(|s| s.parse::<usize>().ok())
.unwrap_or(4);
let mut tier = LmmTier::from_lfm_vl_gguf(
12, text_path(), mmproj_path(),
).expect("LmmTier::from_lfm_vl_gguf");
tier.max_new_tokens = max_new;
eprintln!(" loaded LFM2-VL tier in {:?} max_new={}",
t0.elapsed(), max_new);
let image_bytes = std::fs::read(image_path()).expect("read test image");
eprintln!(" image: {} bytes", image_bytes.len());
let q = Query {
input: QueryInput::Multimodal {
text: "Describe this image:".into(),
images: vec![image_bytes],
audio: vec![],
},
budget: JouleBudget::standard(),
quality: QualityFloor::any(),
context: ContextRef::fresh(),
deadline: None,
};
let est = tier.estimate_cost(&q).expect("estimate_cost");
eprintln!(" estimate: {:.3} mJ", est.joules * 1e3);
assert!(est.joules.is_finite() && est.joules > 0.0);
let t1 = Instant::now();
// Bypass the budget guard; the no-cache loop is slow and the
// measured joules will far exceed the standard 1 J budget at
// captioning lengths. The point of this oracle is correctness, not
// efficiency — that's a follow-up (cache-aware multimodal prefill).
let ans = tier.try_answer(&q, f64::INFINITY).expect("try_answer");
let elapsed = t1.elapsed();
match &ans.output {
AnswerOutput::Text(s) => {
eprintln!(" >>> joules_spent={:.3} mJ wall={:?}",
ans.joules_spent * 1e3, elapsed);
eprintln!(" >>> {:?}", s);
assert!(!s.is_empty(),
"caption empty — likely a stuck-EOS or dead-vision bug");
assert!(s.chars().any(|c| c.is_ascii_alphabetic()),
"caption has no letters: {:?}", s);
}
other => panic!("expected Text answer, got {:?}", other),
}
assert!(ans.joules_spent > 0.0, "joule receipt must be positive");
assert!(matches!(ans.trace.attempts[0].outcome, TraceOutcome::Hit),
"ExecutionTrace should record a Hit, got {:?}", ans.trace.attempts);
eprintln!("VERDICT: LFM2-VL-450M dispatchable through LmmTier with a \
real multimodal caption. Measured {:.3} mJ over {} new tokens, \
wall {:?}.", ans.joules_spent * 1e3, max_new, elapsed);
}