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]>
136 lines
5.1 KiB
Rust
136 lines
5.1 KiB
Rust
//! Integration tests for the Phase 1.1 reference path.
|
|
|
|
use jouleclaw_core::determinism::DeterminismMode;
|
|
use jouleclaw_core::error::Error;
|
|
use jouleclaw_core::graph::GraphBuilder;
|
|
use jouleclaw_core::op::SamplerKind;
|
|
use jouleclaw_core::tensor::{Dtype, Tensor, TensorMeta};
|
|
use jouleclaw_runtime::{compile, execute, ExecutionOptions, Runtime};
|
|
use std::collections::HashMap;
|
|
|
|
/// Lookup: index a 4-row, 3-col F32 table with known indices.
|
|
#[test]
|
|
fn lookup_returns_correct_rows() {
|
|
let mut g = GraphBuilder::new();
|
|
let idx = g.input("idx", TensorMeta::new(Dtype::I32, &[3]));
|
|
let table = g.input("table", TensorMeta::new(Dtype::F32, &[4, 3]));
|
|
let out = g.lookup(idx, table);
|
|
g.output("y", out);
|
|
let graph = g.build();
|
|
|
|
let runtime = Runtime::boot();
|
|
let compiled = compile(graph, &runtime.kernels).unwrap();
|
|
|
|
// Build I32 indices [2, 0, 3].
|
|
let idx_meta = TensorMeta::new(Dtype::I32, &[3]);
|
|
let idx_bytes: Vec<u8> = [2i32, 0, 3].iter()
|
|
.flat_map(|v| v.to_le_bytes()).collect();
|
|
let idx_tensor = Tensor {
|
|
meta: idx_meta,
|
|
storage: std::sync::Arc::new(jouleclaw_core::tensor::TensorStorage { bytes: idx_bytes, mapped: None }),
|
|
};
|
|
|
|
let table_data: Vec<f32> = vec![
|
|
0.1, 0.2, 0.3, // row 0
|
|
1.1, 1.2, 1.3, // row 1
|
|
2.1, 2.2, 2.3, // row 2
|
|
3.1, 3.2, 3.3, // row 3
|
|
];
|
|
let table_tensor = Tensor::from_f32(TensorMeta::new(Dtype::F32, &[4, 3]), &table_data);
|
|
|
|
let mut inputs = HashMap::new();
|
|
inputs.insert("idx".into(), idx_tensor);
|
|
inputs.insert("table".into(), table_tensor);
|
|
|
|
let res = execute(&compiled, inputs, ExecutionOptions::default()).unwrap();
|
|
let y = res.outputs.get("y").unwrap().as_f32_vec();
|
|
|
|
// Expected: rows [2, 0, 3] in order.
|
|
let expected = vec![2.1, 2.2, 2.3, 0.1, 0.2, 0.3, 3.1, 3.2, 3.3];
|
|
assert_eq!(y, expected);
|
|
}
|
|
|
|
/// Greedy sample picks the argmax with low-index tie-break.
|
|
#[test]
|
|
fn sample_greedy_picks_argmax() {
|
|
let mut g = GraphBuilder::new();
|
|
let logits = g.input("logits", TensorMeta::new(Dtype::F32, &[5]));
|
|
let out = g.sample(logits, SamplerKind::Greedy, None);
|
|
g.output("y", out);
|
|
let graph = g.build();
|
|
|
|
let runtime = Runtime::boot();
|
|
let compiled = compile(graph, &runtime.kernels).unwrap();
|
|
|
|
// Logits with the max at index 3.
|
|
let l = vec![0.1f32, -1.0, 0.5, 2.5, 1.7];
|
|
let mut inputs = HashMap::new();
|
|
inputs.insert("logits".into(),
|
|
Tensor::from_f32(TensorMeta::new(Dtype::F32, &[5]), &l));
|
|
|
|
let res = execute(&compiled, inputs, ExecutionOptions::default()).unwrap();
|
|
let y = res.outputs.get("y").unwrap();
|
|
let mut b = [0u8; 4];
|
|
b.copy_from_slice(&y.storage.bytes[..4]);
|
|
let id = i32::from_le_bytes(b);
|
|
assert_eq!(id, 3);
|
|
}
|
|
|
|
/// Seeded TopK is deterministic given the same seed, different from the
|
|
/// argmax in general.
|
|
#[test]
|
|
fn sample_topk_is_seed_deterministic() {
|
|
let mut g = GraphBuilder::new();
|
|
let logits = g.input("logits", TensorMeta::new(Dtype::F32, &[8]));
|
|
let out = g.sample(logits, SamplerKind::TopK { k: 4 }, Some(42));
|
|
g.output("y", out);
|
|
let graph = g.build();
|
|
|
|
let runtime = Runtime::boot();
|
|
let compiled = compile(graph, &runtime.kernels).unwrap();
|
|
|
|
let l = vec![0.1f32, -1.0, 0.5, 2.5, 1.7, 1.6, 0.0, -2.0];
|
|
let mut inputs = HashMap::new();
|
|
inputs.insert("logits".into(),
|
|
Tensor::from_f32(TensorMeta::new(Dtype::F32, &[8]), &l));
|
|
|
|
let res1 = execute(&compiled, inputs.clone(), ExecutionOptions::default()).unwrap();
|
|
let res2 = execute(&compiled, inputs, ExecutionOptions::default()).unwrap();
|
|
assert_eq!(res1.outputs.get("y").unwrap().storage.bytes,
|
|
res2.outputs.get("y").unwrap().storage.bytes,
|
|
"seeded TopK must produce identical results across runs");
|
|
}
|
|
|
|
/// Strict-deterministic mode rejects an unseeded sampler.
|
|
#[test]
|
|
fn strict_mode_rejects_unseeded_sampler() {
|
|
let mut g = GraphBuilder::new();
|
|
let logits = g.input("logits", TensorMeta::new(Dtype::F32, &[4]));
|
|
// No seed — this op's determinism class becomes Stochastic.
|
|
let out = g.sample(logits, SamplerKind::TopK { k: 2 }, None);
|
|
g.output("y", out);
|
|
let graph = g.build();
|
|
|
|
let runtime = Runtime::boot();
|
|
let compiled = compile(graph, &runtime.kernels).unwrap();
|
|
|
|
let l = vec![0.1f32, -1.0, 0.5, 2.5];
|
|
let mut inputs = HashMap::new();
|
|
inputs.insert("logits".into(),
|
|
Tensor::from_f32(TensorMeta::new(Dtype::F32, &[4]), &l));
|
|
|
|
// Strict mode — the kernel itself reports SeededStochastic, but it will
|
|
// fail at execute time when it sees no seed in attrs. Either rejection
|
|
// is acceptable; what matters is that strict mode produces an error
|
|
// rather than a non-deterministic sample.
|
|
let result = execute(&compiled, inputs, ExecutionOptions {
|
|
determinism: DeterminismMode::Strict,
|
|
seed: None,
|
|
});
|
|
assert!(result.is_err(), "strict mode must reject unseeded sampling");
|
|
if let Err(Error::Execution(e)) = result {
|
|
let msg = format!("{:?}", e);
|
|
assert!(msg.contains("seed") || msg.contains("Sample"),
|
|
"error should mention seed or Sample: {}", msg);
|
|
}
|
|
}
|