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]>
137 lines
6 KiB
Rust
137 lines
6 KiB
Rust
//! Chunked prefill parity oracle: prefilling a prompt in chunks of size C
|
|
//! must produce the same final logits as prefilling it as a single forward
|
|
//! pass. This is a baseline correctness gate before exposing a public
|
|
//! `extend_tokens_chunked` API — if the existing scatter path already
|
|
//! supports per-chunk prefill, we get continuous-batching-style memory
|
|
//! bounds for free.
|
|
//!
|
|
//! The argmax test is the strict gate (greedy decode reproducibility);
|
|
//! we also report max-abs logit drift to surface any fp32 ordering effects.
|
|
//!
|
|
//! `#[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;
|
|
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 chunked_prefill_matches_monolithic() {
|
|
let path = model_path();
|
|
eprintln!("loading {path}");
|
|
let model = read_gguf_file(&path).expect("load model");
|
|
let vocab = Vocab::from_gguf(&model).expect("vocab");
|
|
|
|
// A long-ish prompt that exercises non-trivial prefill. Bonsai is
|
|
// happy with ~50 tokens of mixed text.
|
|
let prompt = "You are an assistant that explains physics clearly. \
|
|
The user asks a question and you reply with a brief, \
|
|
pedagogical explanation suitable for an undergraduate \
|
|
student.\nUser: What is the capital of";
|
|
let tokens = vocab.encode_bpe_regex(prompt, true);
|
|
let n = tokens.len();
|
|
eprintln!(" prompt = {} tokens", n);
|
|
assert!(n >= 32, "prompt too short to chunk meaningfully");
|
|
|
|
let max_seq = 256usize;
|
|
let cfg = GenerateConfig { max_new_tokens: 1, ..GenerateConfig::default() };
|
|
|
|
// ── MONOLITHIC: prefill the whole prompt in one call ──
|
|
let t = Instant::now();
|
|
let mut conv_mono = Conversation::with_runtime(
|
|
&model, &vocab, max_seq, Runtime::boot()).expect("mono conv");
|
|
let mono_stream = conv_mono.extend_tokens(tokens.clone(), &cfg)
|
|
.expect("mono extend");
|
|
let mono_ms = t.elapsed().as_secs_f64() * 1000.0;
|
|
let mono_tok = mono_stream.peek_next_token().expect("mono tok");
|
|
eprintln!(" MONOLITHIC prefill ({} tokens) in {:.1} ms — next_id={}",
|
|
n, mono_ms, mono_tok);
|
|
|
|
// ── CHUNKED: split prompt into chunks of CHUNK_SIZE and call
|
|
// extend_tokens once per chunk. Each call advances current_seq
|
|
// and scatters the new K/V at the right positions; the next
|
|
// chunk's attention attends over [0..current_seq+chunk_size]. ──
|
|
const CHUNK_SIZE: usize = 8;
|
|
let t = Instant::now();
|
|
let mut conv_chunked = Conversation::with_runtime(
|
|
&model, &vocab, max_seq, Runtime::boot()).expect("chunked conv");
|
|
let mut chunk_count = 0usize;
|
|
let mut last_tok: Option<u32> = None;
|
|
let mut i = 0usize;
|
|
while i < n {
|
|
let end = (i + CHUNK_SIZE).min(n);
|
|
let chunk: Vec<u32> = tokens[i..end].to_vec();
|
|
let stream = conv_chunked.extend_tokens(chunk, &cfg)
|
|
.expect("chunked extend");
|
|
last_tok = stream.peek_next_token();
|
|
chunk_count += 1;
|
|
i = end;
|
|
}
|
|
let chunked_ms = t.elapsed().as_secs_f64() * 1000.0;
|
|
let chunked_tok = last_tok.expect("chunked tok");
|
|
eprintln!(" CHUNKED prefill ({} chunks of ≤{}) in {:.1} ms — next_id={}",
|
|
chunk_count, CHUNK_SIZE, chunked_ms, chunked_tok);
|
|
|
|
// ── Correctness gate: argmax must match. ──
|
|
assert_eq!(chunked_tok, mono_tok,
|
|
"CHUNKED argmax {} != MONOLITHIC argmax {} — \
|
|
chunked prefill diverges. Per-chunk scatter/attention is \
|
|
not numerically equivalent to a single forward.",
|
|
chunked_tok, mono_tok);
|
|
|
|
eprintln!("VERDICT: chunked prefill produces bit-identical argmax \
|
|
({}). The existing in-place scatter path already supports \
|
|
continuous-batching-style chunked prefill — wall-clock cost \
|
|
{:.1} ms (chunked, {} chunks) vs {:.1} ms (monolithic). \
|
|
Substrate for paged-KV / chunked decode is in place.",
|
|
mono_tok, chunked_ms, chunk_count, mono_ms);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn extend_tokens_chunked_matches_monolithic() {
|
|
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 prompt = "You are an assistant that explains physics clearly. \
|
|
The user asks a question and you reply with a brief, \
|
|
pedagogical explanation suitable for an undergraduate \
|
|
student.\nUser: What is the capital of";
|
|
let tokens = vocab.encode_bpe_regex(prompt, true);
|
|
let max_seq = 256usize;
|
|
let cfg = GenerateConfig { max_new_tokens: 1, ..GenerateConfig::default() };
|
|
|
|
// Monolithic reference.
|
|
let mut conv_mono = Conversation::with_runtime(
|
|
&model, &vocab, max_seq, Runtime::boot()).expect("mono");
|
|
let stream_mono = conv_mono.extend_tokens(tokens.clone(), &cfg)
|
|
.expect("mono extend");
|
|
let mono_tok = stream_mono.peek_next_token().expect("mono tok");
|
|
|
|
// Public chunked API — same call shape as extend_tokens plus a
|
|
// chunk_size. Intermediate sampling is suppressed by dropping
|
|
// intermediate streams internally; the returned stream is over
|
|
// the last chunk's logits.
|
|
let mut conv_chunked = Conversation::with_runtime(
|
|
&model, &vocab, max_seq, Runtime::boot()).expect("chunked");
|
|
let stream_chunked = conv_chunked
|
|
.extend_tokens_chunked(tokens, 8, &cfg)
|
|
.expect("chunked extend");
|
|
let chunked_tok = stream_chunked.peek_next_token().expect("chunked tok");
|
|
|
|
assert_eq!(chunked_tok, mono_tok,
|
|
"extend_tokens_chunked must match monolithic argmax — got \
|
|
{} vs {}", chunked_tok, mono_tok);
|
|
eprintln!("VERDICT: extend_tokens_chunked argmax {} matches \
|
|
monolithic — public chunked-prefill API is correct.", mono_tok);
|
|
}
|