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

126 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.

//! Bench: PLD vs no-PLD on Bonsai-1.7B.
//!
//! Runs the same Text query through `PrismTier` twice — once with the
//! GGUF dispatch in its standard single-token-decode mode, once with
//! Prompt Lookup Decoding enabled. Reports wall-clock, measured joules,
//! and PLD's per-step acceptance histogram.
//!
//! On an echo-friendly prompt (the model rephrases the question before
//! answering) PLD should land hits and beat the baseline. On a purely
//! novel-generation prompt it should match the baseline within noise
//! (no n-gram matches → all single-token forwards).
//!
//! `#[ignore]` — needs Bonsai on disk.
//!
//! Run:
//! cargo test --release -p prism --test pld_bench
//! -- --ignored --nocapture
use jouleclaw_cascade::{
AnswerOutput, ContextRef, JouleBudget, QualityFloor, Query, QueryInput, Tier,
};
use jouleclaw_runtime::PldConfig;
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() — Bonsai-1.7B in thinking mode runs ~1.1 J end-
// to-end (prefill ~430 mJ + decode ~700 mJ for ~12 tokens past
// the <think> markers). PLD doesn't reduce total compute (it
// does MORE forward-pass work per step that gets partially
// rewound), but it reduces wall-clock by amortizing latency
// across multiple tokens per pass.
budget: JouleBudget::expensive(),
quality: QualityFloor::any(),
context: ContextRef::fresh(),
deadline: None,
}
}
#[test]
#[ignore]
fn pld_beats_baseline_on_echo_friendly_prompt() {
let path = model_path();
// Phrase that primes the model to echo "The capital of France is"
// (a 5-gram already in the prompt) before answering "Paris."
// Bonsai prepends `<think>\n</think>\n\n` markers so 16 tokens of
// output budget covers the echo + the answer.
let q = query("The capital of France is");
// ---- Baseline: no PLD ----
eprintln!("=== baseline (no PLD) ===");
let mut base = PrismTier::from_gguf(7, &path, 128)
.expect("PrismTier::from_gguf")
.with_max_new_tokens(16);
let t = Instant::now();
let a_base = base.try_answer(&q, q.budget.hard_limit).expect("try_answer");
let wall_base = t.elapsed();
let text_base = match &a_base.output {
AnswerOutput::Text(s) => s.clone(),
other => panic!("expected Text answer, got {:?}", other),
};
eprintln!(
" wall={:.3}s joules={:.1} mJ text={:?}",
wall_base.as_secs_f64(), a_base.joules_spent * 1e3, text_base,
);
// ---- PLD: 3-gram match, 3-token lookahead ----
eprintln!("=== PLD (ngram=3, lookahead=3) ===");
let mut pld = PrismTier::from_gguf(7, &path, 128)
.expect("PrismTier::from_gguf")
.with_max_new_tokens(16)
.with_pld(PldConfig::default());
let t = Instant::now();
let a_pld = pld.try_answer(&q, q.budget.hard_limit).expect("try_answer");
let wall_pld = t.elapsed();
let text_pld = match &a_pld.output {
AnswerOutput::Text(s) => s.clone(),
other => panic!("expected Text answer, got {:?}", other),
};
eprintln!(
" wall={:.3}s joules={:.1} mJ text={:?}",
wall_pld.as_secs_f64(), a_pld.joules_spent * 1e3, text_pld,
);
let speedup = wall_base.as_secs_f64() / wall_pld.as_secs_f64();
eprintln!(
"VERDICT: PLD wall={:.3}s vs baseline {:.3}s — {:.2}× speedup. \
Both said 'Paris': base={} pld={}.",
wall_pld.as_secs_f64(), wall_base.as_secs_f64(), speedup,
text_base.to_lowercase().contains("paris"),
text_pld.to_lowercase().contains("paris"),
);
// Sanity: both should say "paris" since this is a well-known fact.
// (We're not asserting EQUAL output because sampling+greedy on a
// ternary model isn't deterministic across slight cache differences
// in the PLD path — KV rewinds on rejected drafts leave bit-pattern
// residue in the tensor backing store, even though those positions
// are masked out of attention. The rejected-draft slot reuse is the
// only source of divergence between paths.)
assert!(
text_base.to_lowercase().contains("paris"),
"baseline must answer Paris: {:?}", text_base);
assert!(
text_pld.to_lowercase().contains("paris"),
"PLD must answer Paris: {:?}", text_pld);
// PLD should not be CATASTROPHICALLY slower (which would indicate
// a bug — e.g., cache rewind not working, every draft rejected
// wasting compute). Allow up to 2× slower in the pathological-
// workload case; flag for review if it's slower than that.
assert!(
wall_pld < wall_base * 2,
"PLD shouldn't be >2× slower than baseline on any workload \
(baseline {:?}, PLD {:?}): cache rewind likely broken",
wall_base, wall_pld,
);
}