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]>
120 lines
5 KiB
Rust
120 lines
5 KiB
Rust
//! End-to-end Phase 1.1 demo.
|
|
//!
|
|
//! Builds a graph that exercises every one of the eight math primitives,
|
|
//! compiles on the reference backend, runs it twice with identical inputs,
|
|
//! and verifies the outputs are byte-identical (the determinism oracle in
|
|
//! action).
|
|
|
|
use jouleclaw_core::graph::GraphBuilder;
|
|
use jouleclaw_core::op::{ActivationKind, NormKind};
|
|
use jouleclaw_core::tensor::{Dtype, LifetimeTier, Tensor, TensorMeta};
|
|
use jouleclaw_runtime::{compile, execute, ExecutionOptions, Runtime};
|
|
use std::collections::HashMap;
|
|
|
|
fn main() {
|
|
let seq = 4usize;
|
|
let d = 16usize;
|
|
let dff = 32usize;
|
|
|
|
// Graph: a small block exercising MatMul, Norm, Activation, Mul,
|
|
// Softmax, Add. Lookup and Sample are exercised in the unit tests.
|
|
let graph = {
|
|
let mut g = GraphBuilder::new();
|
|
let x = g.input("x", TensorMeta::new(Dtype::F32, &[seq, d]));
|
|
let w_norm = g.input("w_norm",
|
|
TensorMeta::new(Dtype::F32, &[d]).with_tier(LifetimeTier::Cold));
|
|
let w_proj = g.input("w_proj",
|
|
TensorMeta::new(Dtype::F32, &[d, dff]).with_tier(LifetimeTier::Cold));
|
|
let w_gate = g.input("w_gate",
|
|
TensorMeta::new(Dtype::F32, &[d, dff]).with_tier(LifetimeTier::Cold));
|
|
let w_down = g.input("w_down",
|
|
TensorMeta::new(Dtype::F32, &[dff, d]).with_tier(LifetimeTier::Cold));
|
|
|
|
let xn = g.norm(x, w_norm, NormKind::Rms, 1e-6);
|
|
let proj = g.matmul(xn, w_proj);
|
|
let gate = g.matmul(xn, w_gate);
|
|
let gate = g.activation(gate, ActivationKind::SiLU);
|
|
let hidden = g.mul(gate, proj);
|
|
let hidden = g.softmax(hidden, -1);
|
|
let y = g.matmul(hidden, w_down);
|
|
let out = g.add(x, y);
|
|
g.output("y", out);
|
|
g.build()
|
|
};
|
|
println!("Graph: {} nodes", graph.nodes.len());
|
|
|
|
// Build runtime with reference backend, compile.
|
|
let runtime = Runtime::boot();
|
|
println!("Registered {} kernels", runtime.kernels.iter().count());
|
|
let compiled = compile(graph, &runtime.kernels).expect("compile failed");
|
|
println!("Compiled: {} plan entries\n", compiled.plan.len());
|
|
|
|
// Bind inputs from a deterministic PRNG.
|
|
fn det_random(n: usize, seed: u64) -> Vec<f32> {
|
|
let mut s = seed;
|
|
(0..n).map(|_| {
|
|
s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
|
let bits = (s >> 40) as u32;
|
|
(bits as f32) * (1.0 / (1u32 << 24) as f32) - 0.5
|
|
}).collect()
|
|
}
|
|
fn bind(name: &str, shape: &[usize], seed: u64) -> (String, Tensor) {
|
|
let n = shape.iter().product::<usize>();
|
|
let data = det_random(n, seed);
|
|
(name.to_string(), Tensor::from_f32(TensorMeta::new(Dtype::F32, shape), &data))
|
|
}
|
|
|
|
let mut inputs = HashMap::new();
|
|
let (k, v) = bind("x", &[seq, d], 1); inputs.insert(k, v);
|
|
let (k, v) = bind("w_norm", &[d], 2); inputs.insert(k, v);
|
|
let (k, v) = bind("w_proj", &[d, dff], 3); inputs.insert(k, v);
|
|
let (k, v) = bind("w_gate", &[d, dff], 4); inputs.insert(k, v);
|
|
let (k, v) = bind("w_down", &[dff, d], 5); inputs.insert(k, v);
|
|
|
|
// Run twice with identical inputs.
|
|
let res1 = execute(&compiled, inputs.clone(), ExecutionOptions::default()).expect("exec1");
|
|
let res2 = execute(&compiled, inputs.clone(), ExecutionOptions::default()).expect("exec2");
|
|
|
|
println!("--- Run 1 ---");
|
|
println!("graph_hash: {}", hex(&res1.trace.graph_hash.0));
|
|
println!("output_hash: {}", hex(&res1.trace.output_hashes[0].0));
|
|
println!("joules: {:.6e}", res1.trace.joule_accounting.total_joules);
|
|
println!("wall_clock: {:?}", res1.trace.wall_clock);
|
|
|
|
println!("\n--- Run 2 ---");
|
|
println!("graph_hash: {}", hex(&res2.trace.graph_hash.0));
|
|
println!("output_hash: {}", hex(&res2.trace.output_hashes[0].0));
|
|
println!("joules: {:.6e}", res2.trace.joule_accounting.total_joules);
|
|
println!("wall_clock: {:?}", res2.trace.wall_clock);
|
|
|
|
// Determinism check.
|
|
assert_eq!(res1.trace.graph_hash.0, res2.trace.graph_hash.0,
|
|
"graph hash must be stable across runs");
|
|
assert_eq!(res1.trace.output_hashes, res2.trace.output_hashes,
|
|
"outputs must be byte-identical across runs");
|
|
|
|
let y1 = res1.outputs.get("y").unwrap();
|
|
let y2 = res2.outputs.get("y").unwrap();
|
|
assert_eq!(y1.meta.shape, vec![seq, d]);
|
|
assert_eq!(y1.storage.bytes, y2.storage.bytes);
|
|
|
|
let y1f = y1.as_f32_vec();
|
|
println!("\nOutput[0..8] = {:?}", &y1f[0..8.min(y1f.len())]);
|
|
|
|
println!("\n--- Joule accounting (Run 1) ---");
|
|
let mut by_op: Vec<_> = res1.trace.joule_accounting.by_op_kind.iter().collect();
|
|
by_op.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap());
|
|
for (op, j) in by_op {
|
|
println!(" {:>10?}: {:.6e} J", op, j);
|
|
}
|
|
|
|
println!("\nOK: graph and outputs are bit-identical across two runs.");
|
|
println!("The determinism oracle is in place; other backends verify against this baseline.");
|
|
}
|
|
|
|
fn hex(b: &[u8]) -> String {
|
|
use std::fmt::Write;
|
|
let mut s = String::with_capacity(b.len() * 2);
|
|
for x in b { write!(s, "{:02x}", x).unwrap(); }
|
|
s
|
|
}
|