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]>
101 lines
3.5 KiB
Rust
101 lines
3.5 KiB
Rust
//! Production-surface test: `PrismTier::with_drafter(...)` glues
|
|
//! Bonsai-1.7B as drafter onto Bonsai-4B target, dispatched through
|
|
//! the cascade as a single tier.
|
|
//!
|
|
//! Validates:
|
|
//! - The builder loads both models and rejects mismatched vocab
|
|
//! sizes at build time.
|
|
//! - `try_answer` routes through `jouleclaw_runtime::extend_with_drafter`
|
|
//! when a drafter is configured, producing a correct answer.
|
|
//! - The drafter path produces semantically-correct output (contains
|
|
//! "Paris" for the canonical capital query).
|
|
//!
|
|
//! `#[ignore]` — needs Bonsai 1.7B and 4B GGUFs on disk.
|
|
//!
|
|
//! Run:
|
|
//! cargo test --release -p prism --test drafter_tier
|
|
//! -- --ignored --nocapture
|
|
|
|
use jouleclaw_cascade::{
|
|
AnswerOutput, ContextRef, JouleBudget, QualityFloor, Query, QueryInput, Tier,
|
|
};
|
|
use jouleclaw_prism::PrismTier;
|
|
use std::time::Instant;
|
|
|
|
fn bonsai_path(n: &str) -> String {
|
|
format!("../../models/{n}")
|
|
}
|
|
|
|
fn text_query(s: &str) -> Query {
|
|
Query {
|
|
input: QueryInput::Text(s.to_string()),
|
|
// Drafter path runs BOTH models — total joules scales up.
|
|
// expensive() is the right ceiling.
|
|
budget: JouleBudget::expensive(),
|
|
quality: QualityFloor::any(),
|
|
context: ContextRef::fresh(),
|
|
deadline: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn prism_tier_with_drafter_answers_correctly() {
|
|
let target_path = bonsai_path("ternary-bonsai-4b-q2_0.gguf");
|
|
let drafter_path = bonsai_path("ternary-bonsai-1.7b-q2_0.gguf");
|
|
eprintln!("target: {target_path}");
|
|
eprintln!("drafter: {drafter_path}");
|
|
|
|
let t0 = Instant::now();
|
|
let mut tier = PrismTier::from_gguf(42, &target_path, 128)
|
|
.expect("target load")
|
|
.with_max_new_tokens(16)
|
|
.with_drafter(&drafter_path, 128)
|
|
.expect("drafter load")
|
|
.with_drafter_lookahead(4);
|
|
eprintln!("loaded both models in {:?}", t0.elapsed());
|
|
|
|
let q = text_query("The capital of France is");
|
|
let t = Instant::now();
|
|
let ans = tier.try_answer(&q, q.budget.hard_limit).expect("answer");
|
|
let wall = t.elapsed();
|
|
|
|
let text = match &ans.output {
|
|
AnswerOutput::Text(s) => s.clone(),
|
|
other => panic!("expected Text, got {:?}", other),
|
|
};
|
|
eprintln!(
|
|
"VERDICT: wall={:.3}s joules_spent={:.1} mJ text={:?}",
|
|
wall.as_secs_f64(), ans.joules_spent * 1e3, text,
|
|
);
|
|
|
|
assert!(text.to_lowercase().contains("paris"),
|
|
"PrismTier with drafter must reach 'Paris': {text:?}");
|
|
assert!(ans.joules_spent > 1e-3,
|
|
"drafter path should spend > 1 mJ (target+drafter compute); got {} J",
|
|
ans.joules_spent);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn with_drafter_rejects_mismatched_vocab() {
|
|
// Bonsai-1.7B (qwen3, vocab ~152K) and lfm2-350M (different vocab
|
|
// family) must not pair. The build-time vocab-size check catches
|
|
// it before any inference attempt.
|
|
let target_path = bonsai_path("ternary-bonsai-1.7b-q2_0.gguf");
|
|
let mismatch_path = bonsai_path("lfm2-350m-q8_0.gguf");
|
|
|
|
// Both files must exist for this test to be meaningful — skip if
|
|
// either is missing.
|
|
if !std::path::Path::new(&mismatch_path).exists() {
|
|
eprintln!("skipping: {mismatch_path} not present");
|
|
return;
|
|
}
|
|
|
|
let tier = PrismTier::from_gguf(1, &target_path, 128)
|
|
.expect("target load")
|
|
.with_drafter(&mismatch_path, 128);
|
|
assert!(tier.is_err(),
|
|
"expected vocab-mismatch rejection when pairing Bonsai with LFM2");
|
|
eprintln!("vocab mismatch correctly rejected");
|
|
}
|