Three crates ported. All 440 tests pass across Phase 1+2+3.
jouleclaw-backend-reference
Ported from pattern-lang/joule-backend-reference (~3556 LOC, 17 files).
Pure-Rust scalar kernels — the determinism oracle every other backend
validates against. Activation, conv1d, elementwise, lookup (incl. bit
and ternary), matmul (incl. bit, q8_0, stq1_0, ternary), norm, rope,
sample, shape, softmax.
jouleclaw-backend-apple
Ported from pattern-lang/joule-backend-apple (~1116 LOC, 4 files).
Accelerate (cblas_sgemm via direct FFI; build.rs links the
framework), Metal compute kernels, MPSMatrixMultiplication. On M5,
MPS matmul auto-routes through the GPU's Neural Accelerators
(Tensor APIs). Two Rust-2024 extern-block fixes (unsafe extern "C")
in accelerate.rs and mps_matmul.rs.
jouleclaw-runtime
Ported from pattern-lang/joule-runtime (~10218 LOC, 10 files).
The runtime kernel: chat, embed, generate, streaming, compile,
execute, validate, plus the speculative-decoding primitives
(drafter, PLD = prompt lookup decoding). Wires the cascade
(jouleclaw-cascade), the topology (jouleclaw-topology), the format
loader (jouleclaw-loader-gguf), and both backends together.
One Rust-2024 binding-mode fix in compile.rs (|(_, &d)| d == 0 →
|(_, d)| **d == 0). Three types in jouleclaw-loader-gguf
(KvQuant, KvSnapshot, ShortConvStateSnapshot) gained
serde::{Serialize, Deserialize} derives so the runtime's
ConversationCheckpoint can be serialized; serde added to the
loader-gguf Cargo.toml.
Day-one model coverage inherits everything mistral.rs's parent
loaders support — Llama (incl. Llama 4), Mistral (incl. Small 4),
Qwen (3, 3-VL, 3 MoE), Gemma (incl. Gemma 4 with MTP drafter), Phi
(incl. Phi-4 MM), DeepSeek V3, Granite 4 — through this runtime's
PLD + drafter paths.
31 integration test files cover: alias-energy, blocks, boot
validation, causal, chat, chunked prefill, concat-and-offset, embed,
end-to-end model, generation (incl. inplace), GQA, inplace-alias
proof, integration, KV cache (incl. inplace), multihead, prefix
cache, real Bonsai (incl. streaming), real Liquid LFM2, real Tencent
HYMT, real TinyLlama, repeat, rope-and-scaling, sampling features,
scatter (incl. inplace), shape, slice (incl. slice-attention),
streaming, text-to-logits, validation.
Phase 4 next: the model tiers — gemma4, liquid (CfC SSM), prism (1-bit
ternary BitNet), mrl (Matryoshka embeddings), lmm (vision/text/audio),
ebm (energy-based constraint satisfaction). All depend on the runtime
+ backends shipped in this commit.
Deferred to later phases: HAL from efficient-genai + the discrete-GPU
backends (CUDA, ROCm, Vulkan) — these are needed for Phase 5
omni-modal (diffusion, stable-diffusion, gaussian3d, video), not for
Phase 4 text/code/SSM model tiers which run on Apple + reference.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
213 lines
9.3 KiB
Rust
213 lines
9.3 KiB
Rust
//! Prefix cache oracle: replaying a checkpointed prefix produces
|
|
//! identical logits to a fresh full prefill, and is faster.
|
|
//!
|
|
//! Two-conversation parity:
|
|
//! * `control`: fresh Conversation, prefill [prefix ; suffix] in one
|
|
//! call, sample the last position.
|
|
//! * `replay`: fresh Conversation, prefill only `prefix`, take a
|
|
//! `ConversationCheckpoint`, build a new Conversation from it, then
|
|
//! prefill `suffix` and sample the last position.
|
|
//!
|
|
//! The argmax token must match (the most stringent greedy-decode gate);
|
|
//! we also report the wall-clock prefill costs to demonstrate the win.
|
|
//!
|
|
//! `#[ignore]` — needs Bonsai-1.7B on disk.
|
|
|
|
use jouleclaw_loader_gguf::read_gguf_file;
|
|
use jouleclaw_loader_gguf::tokenizer::Vocab;
|
|
use jouleclaw_runtime::generate::GenerateConfig;
|
|
use jouleclaw_runtime::streaming::{Conversation, PrefixCache};
|
|
use jouleclaw_runtime::Runtime;
|
|
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())
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn replayed_checkpoint_matches_fresh_prefill_and_is_faster() {
|
|
let path = model_path();
|
|
eprintln!("loading {path}");
|
|
let model = read_gguf_file(&path).expect("load model");
|
|
let vocab = Vocab::from_gguf(&model).expect("vocab");
|
|
|
|
// Pick a "system" prefix that's reasonably long (the win scales
|
|
// with prefix size) and a short suffix.
|
|
let prefix_text = "You are a helpful assistant. Always answer concisely.\n\
|
|
User: What is the capital of";
|
|
let suffix_text = " France? A:";
|
|
|
|
let prefix_tokens = vocab.encode_bpe_regex(prefix_text, true);
|
|
let suffix_tokens = vocab.encode_bpe_regex(suffix_text, false);
|
|
let full_tokens: Vec<u32> = prefix_tokens.iter()
|
|
.chain(suffix_tokens.iter()).copied().collect();
|
|
eprintln!(" prefix={} tokens, suffix={} tokens, full={} tokens",
|
|
prefix_tokens.len(), suffix_tokens.len(), full_tokens.len());
|
|
|
|
let max_seq = 256usize;
|
|
let cfg = GenerateConfig {
|
|
max_new_tokens: 1,
|
|
..GenerateConfig::default()
|
|
};
|
|
|
|
// ── CONTROL: fresh conv, single prefill of full sequence ──
|
|
let t_ctrl = Instant::now();
|
|
let mut conv_ctrl = Conversation::with_runtime(
|
|
&model, &vocab, max_seq, Runtime::boot()).expect("ctrl conv");
|
|
let stream_ctrl = conv_ctrl
|
|
.extend_tokens(full_tokens.clone(), &cfg)
|
|
.expect("ctrl extend");
|
|
let ctrl_prefill_ms = t_ctrl.elapsed().as_secs_f64() * 1000.0;
|
|
let ctrl_tok = stream_ctrl.peek_next_token()
|
|
.expect("ctrl must have a sampled first token");
|
|
eprintln!(" CONTROL prefill {} tokens in {:.1} ms — next_id={}",
|
|
full_tokens.len(), ctrl_prefill_ms, ctrl_tok);
|
|
|
|
// ── WARM: fresh conv, prefill ONLY the prefix, take checkpoint ──
|
|
let mut conv_warm = Conversation::with_runtime(
|
|
&model, &vocab, max_seq, Runtime::boot()).expect("warm conv");
|
|
let t_warm = Instant::now();
|
|
let _ = conv_warm.extend_tokens(prefix_tokens.clone(), &cfg)
|
|
.expect("warm extend");
|
|
let warm_prefill_ms = t_warm.elapsed().as_secs_f64() * 1000.0;
|
|
let checkpoint = conv_warm.checkpoint();
|
|
eprintln!(" WARM prefix prefill {} tokens in {:.1} ms — \
|
|
checkpoint bytes={:.1} MB",
|
|
prefix_tokens.len(), warm_prefill_ms,
|
|
checkpoint.bytes() as f64 / 1e6);
|
|
|
|
// Stash into a PrefixCache and look it back up to exercise that
|
|
// surface too.
|
|
let mut pcache = PrefixCache::new(500_000_000);
|
|
pcache.insert(checkpoint);
|
|
let (cached, matched_len) = pcache.lookup(&full_tokens)
|
|
.expect("must find cached prefix");
|
|
assert_eq!(matched_len, prefix_tokens.len(),
|
|
"PrefixCache::lookup should match the full cached prefix length");
|
|
|
|
// ── REPLAY: resume from checkpoint, prefill only the suffix ──
|
|
let t_replay = Instant::now();
|
|
let mut conv_replay = Conversation::from_checkpoint(
|
|
&model, &vocab, Runtime::boot(), cached,
|
|
).expect("from_checkpoint");
|
|
let replay_restore_ms = t_replay.elapsed().as_secs_f64() * 1000.0;
|
|
let t_replay_pref = Instant::now();
|
|
let stream_replay = conv_replay
|
|
.extend_tokens(suffix_tokens.clone(), &cfg)
|
|
.expect("replay extend");
|
|
let replay_prefill_ms = t_replay_pref.elapsed().as_secs_f64() * 1000.0;
|
|
let replay_tok = stream_replay.peek_next_token()
|
|
.expect("replay must have a sampled first token");
|
|
eprintln!(" REPLAY restore in {:.1} ms + suffix prefill {} tokens in {:.1} ms \
|
|
— next_id={}",
|
|
replay_restore_ms, suffix_tokens.len(),
|
|
replay_prefill_ms, replay_tok);
|
|
|
|
// ── Correctness gate: argmax token must match ──
|
|
assert_eq!(replay_tok, ctrl_tok,
|
|
"REPLAY argmax {} != CONTROL argmax {} — prefix-cache restore \
|
|
lost numerical fidelity",
|
|
replay_tok, ctrl_tok);
|
|
|
|
// ── Win gate: the replay prefill (suffix only) must be cheaper
|
|
// than the control prefill (full sequence). Restore + suffix
|
|
// combined should still beat control in most realistic cases
|
|
// (prefill is quadratic in length and the suffix is short). ──
|
|
let total_replay_ms = replay_restore_ms + replay_prefill_ms;
|
|
eprintln!(" speedup: control {:.1} ms vs replay {:.1} ms (restore+suffix) = {:.2}x",
|
|
ctrl_prefill_ms, total_replay_ms,
|
|
ctrl_prefill_ms / total_replay_ms.max(1.0));
|
|
assert!(replay_prefill_ms < ctrl_prefill_ms,
|
|
"suffix-only prefill {:.1} ms must beat full prefill {:.1} ms",
|
|
replay_prefill_ms, ctrl_prefill_ms);
|
|
|
|
eprintln!("VERDICT: ConversationCheckpoint replays exactly — \
|
|
argmax {} matched on both paths, suffix prefill cut from \
|
|
{:.1} ms to {:.1} ms. PrefixCache substrate is live.",
|
|
replay_tok, ctrl_prefill_ms, replay_prefill_ms);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn persistent_prefix_cache_roundtrip_replay_matches_in_process() {
|
|
let path = model_path();
|
|
eprintln!("loading {path}");
|
|
let model = read_gguf_file(&path).expect("load model");
|
|
let vocab = Vocab::from_gguf(&model).expect("vocab");
|
|
|
|
let prefix_text = "You are a helpful assistant. Always answer concisely.\n\
|
|
User: What is the capital of";
|
|
let suffix_text = " France? A:";
|
|
let prefix_tokens = vocab.encode_bpe_regex(prefix_text, true);
|
|
let suffix_tokens = vocab.encode_bpe_regex(suffix_text, false);
|
|
let full_tokens: Vec<u32> = prefix_tokens.iter()
|
|
.chain(suffix_tokens.iter()).copied().collect();
|
|
|
|
let max_seq = 256usize;
|
|
let cfg = GenerateConfig {
|
|
max_new_tokens: 1,
|
|
..GenerateConfig::default()
|
|
};
|
|
|
|
// Capture a checkpoint for the prefix.
|
|
let mut conv_warm = Conversation::with_runtime(
|
|
&model, &vocab, max_seq, Runtime::boot()).expect("warm conv");
|
|
let _ = conv_warm.extend_tokens(prefix_tokens.clone(), &cfg)
|
|
.expect("warm extend");
|
|
let checkpoint = conv_warm.checkpoint();
|
|
|
|
// CONTROL: replay from the live in-process checkpoint and capture
|
|
// the argmax. The previous test already proved this matches a
|
|
// fresh full prefill; we use it as the reference here.
|
|
let mut cache_a = PrefixCache::new(500_000_000);
|
|
cache_a.insert(checkpoint);
|
|
let (cp_a, _) = cache_a.lookup(&full_tokens).expect("hit A");
|
|
let mut conv_a = Conversation::from_checkpoint(
|
|
&model, &vocab, Runtime::boot(), cp_a).expect("conv_a");
|
|
let stream_a = conv_a.extend_tokens(suffix_tokens.clone(), &cfg)
|
|
.expect("conv_a extend");
|
|
let argmax_a = stream_a.peek_next_token().expect("conv_a tok");
|
|
eprintln!(" IN-PROCESS replay argmax = {}", argmax_a);
|
|
|
|
// PERSIST: save to disk, drop, reload, replay.
|
|
let cache_path = std::env::temp_dir().join("joule_prefix_cache_test.bin");
|
|
let _ = std::fs::remove_file(&cache_path);
|
|
cache_a.save_to_file(&cache_path).expect("save_to_file");
|
|
let cache_size = std::fs::metadata(&cache_path).expect("stat").len();
|
|
eprintln!(" wrote cache to {} — {:.1} MB on disk",
|
|
cache_path.display(), cache_size as f64 / 1e6);
|
|
drop(cache_a);
|
|
|
|
let t_load = Instant::now();
|
|
let mut cache_b = PrefixCache::load_from_file(&cache_path, 500_000_000)
|
|
.expect("load_from_file");
|
|
let load_ms = t_load.elapsed().as_secs_f64() * 1000.0;
|
|
eprintln!(" reloaded cache in {:.1} ms — {} entries, {:.1} MB resident",
|
|
load_ms, cache_b.len(), cache_b.current_bytes() as f64 / 1e6);
|
|
|
|
let (cp_b, matched_len) = cache_b.lookup(&full_tokens)
|
|
.expect("hit B");
|
|
assert_eq!(matched_len, prefix_tokens.len(),
|
|
"disk-reloaded cache must find the same prefix length");
|
|
|
|
let mut conv_b = Conversation::from_checkpoint(
|
|
&model, &vocab, Runtime::boot(), cp_b).expect("conv_b");
|
|
let stream_b = conv_b.extend_tokens(suffix_tokens.clone(), &cfg)
|
|
.expect("conv_b extend");
|
|
let argmax_b = stream_b.peek_next_token().expect("conv_b tok");
|
|
eprintln!(" PERSISTED replay argmax = {}", argmax_b);
|
|
|
|
assert_eq!(argmax_a, argmax_b,
|
|
"argmax after save→load→replay {} must match in-process replay {}",
|
|
argmax_b, argmax_a);
|
|
|
|
// Cleanup.
|
|
let _ = std::fs::remove_file(&cache_path);
|
|
|
|
eprintln!("VERDICT: PrefixCache survives save→load→replay with \
|
|
bit-identical argmax ({}). A cold-started process can resume \
|
|
from a warm system-prompt cache without any model-side \
|
|
re-prefill.", argmax_a);
|
|
}
|