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]>
98 lines
3.7 KiB
Rust
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);
|
|
}
|