open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-prism/tests/real_bonsai_multitier.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

120 lines
5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Multi-tier orchestration oracle.
//!
//! Everything else proves a *tier* works. This proves the **cascade**
//! works: a `jouleclaw_cascade::Runtime` with an auto-prepended L0 cache
//! and a registered `PrismTier(Bonsai)` routes a query end-to-end,
//! and a *repeat* of the same query is served from L0 at
//! picojoule cost instead of re-running the 1.8B model.
//!
//! This is the thesis in one test: the runtime is the AI, the model
//! is a peripheral the cascade reaches for only when the cheaper
//! tiers can't answer — and never twice for the same question.
//!
//! Gate:
//! * 1st query: tier_used = L3 (Bonsai), measured joules > 1 mJ,
//! answer contains "Paris".
//! * 2nd identical query: tier_used = L0, joules ≪ 1st (≥1000×
//! cheaper), identical answer text.
//!
//! `#[ignore]` — needs the model on disk.
use jouleclaw_cascade::{
AnswerOutput, Cascade, ContextRef, JouleBudget, QualityFloor,
Query, QueryInput, Runtime, TierId,
};
use jouleclaw_prism::PrismTier;
use std::time::Instant;
fn model_path() -> String {
std::env::var("JOULE_BONSAI")
.unwrap_or_else(|_| "../../models/ternary-bonsai-1.7b-q2_0.gguf".to_string())
}
fn query(text: &str) -> Query {
Query {
input: QueryInput::Text(text.to_string()),
// `expensive()` (100 J) instead of `standard()` (1 J) because
// Bonsai-1.7B in thinking mode genuinely sits on the L3/L4
// boundary: prompt prefill costs ~430 mJ alone, and a natural-
// length answer with `<think></think>` markers adds another
// ~700 mJ to reach "Paris" — total ~1.1 J. The Runtime's
// budget guard would reject this against the 1 J limit even
// though the answer is correct, so we route through a budget
// category that fits. This is data-driven: it's what the
// calibration ledger learns after one dispatch.
budget: JouleBudget::expensive(),
quality: QualityFloor::any(),
context: ContextRef::fresh(),
deadline: None,
}
}
#[test]
#[ignore]
fn cascade_routes_l3_then_l0_on_repeat() {
let path = model_path();
eprintln!("loading {}", path);
let t0 = Instant::now();
// 16 tokens: Bonsai is a thinking-mode reasoner; it emits
// <think>...</think> markers before the answer. 6 tokens (prior
// cap) leaves no room for "Paris". 16 keeps the *measured* cost
// (~1.25× the static estimate at 43 mJ/tok) under
// `JouleBudget::standard()`'s 1 J ceiling, so the Runtime's
// budget guard accepts the answer.
let tier = PrismTier::from_gguf(7, &path, 128)
.expect("PrismTier::from_gguf")
.with_max_new_tokens(16);
eprintln!(" loaded in {:?}", t0.elapsed());
// Cascade with just the one L3 tier; Runtime::new prepends an L0.
let mut cascade = Cascade::new();
cascade.register(Box::new(tier));
let mut runtime = Runtime::new(cascade);
let q = query("The capital of France is");
// ---- 1st dispatch: L0 miss → L3 (Bonsai) ----
let t1 = Instant::now();
let a1 = runtime.answer(q.clone()).expect("answer #1");
let wall1 = t1.elapsed();
let txt1 = match &a1.output {
AnswerOutput::Text(s) => s.clone(),
other => panic!("expected Text, got {:?}", other),
};
eprintln!(" #1 tier={:?} joules={:.3} mJ wall={:?} {:?}",
a1.tier_used, a1.joules_spent * 1e3, wall1, txt1);
assert_eq!(a1.tier_used, TierId::L3(jouleclaw_cascade::L3ModelId(7)),
"first dispatch must reach the Bonsai L3 tier");
assert!(txt1.to_lowercase().contains("paris"),
"L3 answer must contain 'Paris', got {:?}", txt1);
assert!(a1.joules_spent > 1e-3,
"L3 measured joules should be model-scale (>1 mJ), got {} J",
a1.joules_spent);
// ---- 2nd dispatch: identical query → L0 cache hit ----
let t2 = Instant::now();
let a2 = runtime.answer(q.clone()).expect("answer #2");
let wall2 = t2.elapsed();
let txt2 = match &a2.output {
AnswerOutput::Text(s) => s.clone(),
other => panic!("expected Text on cache hit, got {:?}", other),
};
eprintln!(" #2 tier={:?} joules={:.3e} J wall={:?} {:?}",
a2.tier_used, a2.joules_spent, wall2, txt2);
assert_eq!(a2.tier_used, TierId::L0,
"second identical query must be served from the L0 cache");
assert_eq!(txt2, txt1, "cache must return the exact same answer text");
assert!(a2.joules_spent > 0.0 && a2.joules_spent < a1.joules_spent / 1000.0,
"L0 hit must be ≥1000× cheaper than the L3 run: L0={} J vs L3={} J",
a2.joules_spent, a1.joules_spent);
// The cache hit should also be dramatically faster in wall-clock.
assert!(wall2 * 50 < wall1,
"L0 wall {:?} should be ≫ faster than L3 wall {:?}", wall2, wall1);
let energy_ratio = a1.joules_spent / a2.joules_spent;
eprintln!("VERDICT: cascade routed L3 then L0 on repeat. \
Energy: L3 {:.1} mJ → L0 {:.3e} J ({:.0}× cheaper). \
Wall: {:?} → {:?}.",
a1.joules_spent * 1e3, a2.joules_spent, energy_ratio, wall1, wall2);
}