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]>
175 lines
6.9 KiB
Rust
175 lines
6.9 KiB
Rust
//! Tests for in-place Scatter via storage aliasing.
|
|
//!
|
|
//! `GraphBuilder::scatter_inplace` sets `Node::aliases_input = Some(0)`,
|
|
//! hinting that the dst input's storage may be reused as the output's
|
|
//! storage. The executor honors this when the input's Arc<TensorStorage>
|
|
//! is uniquely owned at execution time.
|
|
//!
|
|
//! These tests verify:
|
|
//! (1) the alias path produces identical results to plain Scatter
|
|
//! (correctness)
|
|
//! (2) the alias is actually taken — the output's storage Arc is the
|
|
//! same one that the input's was (the proof the dst→output copy
|
|
//! was elided)
|
|
//! (3) when the input is shared (held by another consumer), the
|
|
//! executor safely falls back to copying
|
|
|
|
use jouleclaw_core::graph::GraphBuilder;
|
|
use jouleclaw_core::tensor::{Dtype, Tensor, TensorMeta};
|
|
use jouleclaw_runtime::{compile, execute, ExecutionOptions, Runtime};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
fn build_runtime() -> Runtime { Runtime::boot() }
|
|
|
|
/// Aliased scatter produces the same result as plain scatter.
|
|
#[test]
|
|
fn aliased_scatter_matches_plain_scatter() {
|
|
let dst_meta = TensorMeta::new(Dtype::F32, &[8]);
|
|
let src_meta = TensorMeta::new(Dtype::F32, &[3]);
|
|
let dst_data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
|
|
let src_data = vec![100.0, 200.0, 300.0];
|
|
let offset = 2;
|
|
|
|
// Plain scatter.
|
|
let mut g1 = GraphBuilder::new();
|
|
let d1 = g1.input("dst", dst_meta.clone());
|
|
let s1 = g1.input("src", src_meta.clone());
|
|
let o1 = g1.scatter(d1, s1, 0, offset);
|
|
g1.output("out", o1);
|
|
let graph1 = g1.build();
|
|
let rt = build_runtime();
|
|
let c1 = compile(graph1, &rt.kernels).unwrap();
|
|
let mut inputs = HashMap::new();
|
|
inputs.insert("dst".into(), Tensor::from_f32(dst_meta.clone(), &dst_data));
|
|
inputs.insert("src".into(), Tensor::from_f32(src_meta.clone(), &src_data));
|
|
let r1 = execute(&c1, inputs.clone(), ExecutionOptions::default()).unwrap();
|
|
let plain = r1.outputs.get("out").unwrap().as_f32_vec();
|
|
|
|
// Aliased scatter (scatter_inplace).
|
|
let mut g2 = GraphBuilder::new();
|
|
let d2 = g2.input("dst", dst_meta.clone());
|
|
let s2 = g2.input("src", src_meta.clone());
|
|
let o2 = g2.scatter_inplace(d2, s2, 0, offset);
|
|
g2.output("out", o2);
|
|
let graph2 = g2.build();
|
|
let c2 = compile(graph2, &rt.kernels).unwrap();
|
|
let r2 = execute(&c2, inputs, ExecutionOptions::default()).unwrap();
|
|
let aliased = r2.outputs.get("out").unwrap().as_f32_vec();
|
|
|
|
assert_eq!(aliased, plain,
|
|
"aliased scatter must produce the same bytes as plain scatter");
|
|
assert_eq!(aliased, vec![1.0, 2.0, 100.0, 200.0, 300.0, 6.0, 7.0, 8.0]);
|
|
}
|
|
|
|
/// Proof the alias actually fires: the output tensor's storage Arc points
|
|
/// to the SAME allocation as the input's storage Arc did at execution time.
|
|
///
|
|
/// We construct dst and src as input Tensors with known storage Arcs.
|
|
/// After execute, the output's storage should be the dst input's storage
|
|
/// (the same Arc we put in, since we held the only other ref and that
|
|
/// got moved into the executor's bound map, then stolen).
|
|
#[test]
|
|
fn aliased_scatter_steals_input_storage() {
|
|
let dst_meta = TensorMeta::new(Dtype::F32, &[8]);
|
|
let src_meta = TensorMeta::new(Dtype::F32, &[3]);
|
|
|
|
let mut g = GraphBuilder::new();
|
|
let d = g.input("dst", dst_meta.clone());
|
|
let s = g.input("src", src_meta.clone());
|
|
let o = g.scatter_inplace(d, s, 0, 2);
|
|
g.output("out", o);
|
|
let graph = g.build();
|
|
|
|
let rt = build_runtime();
|
|
let c = compile(graph, &rt.kernels).unwrap();
|
|
|
|
let dst_tensor = Tensor::from_f32(dst_meta, &[0f32; 8]);
|
|
let dst_storage_addr = Arc::as_ptr(&dst_tensor.storage) as usize;
|
|
|
|
let mut inputs = HashMap::new();
|
|
inputs.insert("dst".into(), dst_tensor);
|
|
inputs.insert("src".into(), Tensor::from_f32(src_meta, &[7.0, 8.0, 9.0]));
|
|
let r = execute(&c, inputs, ExecutionOptions::default()).unwrap();
|
|
let out = r.outputs.get("out").unwrap();
|
|
let out_storage_addr = Arc::as_ptr(&out.storage) as usize;
|
|
|
|
assert_eq!(out_storage_addr, dst_storage_addr,
|
|
"in-place scatter should steal dst's storage Arc \
|
|
(out_addr=0x{:x}, dst_addr=0x{:x})",
|
|
out_storage_addr, dst_storage_addr);
|
|
}
|
|
|
|
/// Plain scatter (no alias hint) does NOT steal storage — the output is
|
|
/// a fresh allocation distinct from the dst input.
|
|
#[test]
|
|
fn plain_scatter_does_not_steal_storage() {
|
|
let dst_meta = TensorMeta::new(Dtype::F32, &[8]);
|
|
let src_meta = TensorMeta::new(Dtype::F32, &[3]);
|
|
|
|
let mut g = GraphBuilder::new();
|
|
let d = g.input("dst", dst_meta.clone());
|
|
let s = g.input("src", src_meta.clone());
|
|
let o = g.scatter(d, s, 0, 2);
|
|
g.output("out", o);
|
|
let graph = g.build();
|
|
|
|
let rt = build_runtime();
|
|
let c = compile(graph, &rt.kernels).unwrap();
|
|
|
|
let dst_tensor = Tensor::from_f32(dst_meta, &[0f32; 8]);
|
|
let dst_addr = Arc::as_ptr(&dst_tensor.storage) as usize;
|
|
|
|
let mut inputs = HashMap::new();
|
|
inputs.insert("dst".into(), dst_tensor);
|
|
inputs.insert("src".into(), Tensor::from_f32(src_meta, &[7.0, 8.0, 9.0]));
|
|
let r = execute(&c, inputs, ExecutionOptions::default()).unwrap();
|
|
let out = r.outputs.get("out").unwrap();
|
|
let out_addr = Arc::as_ptr(&out.storage) as usize;
|
|
|
|
assert_ne!(out_addr, dst_addr,
|
|
"plain scatter (no in-place hint) should allocate fresh output");
|
|
}
|
|
|
|
/// Demo: print the storage addresses for visible confirmation.
|
|
#[test]
|
|
fn alias_demo() {
|
|
println!("\n=== In-place Scatter aliasing demo ===");
|
|
|
|
let dst_meta = TensorMeta::new(Dtype::F32, &[16]);
|
|
let src_meta = TensorMeta::new(Dtype::F32, &[4]);
|
|
|
|
for in_place in &[false, true] {
|
|
let mut g = GraphBuilder::new();
|
|
let d = g.input("dst", dst_meta.clone());
|
|
let s = g.input("src", src_meta.clone());
|
|
let o = if *in_place {
|
|
g.scatter_inplace(d, s, 0, 5)
|
|
} else {
|
|
g.scatter(d, s, 0, 5)
|
|
};
|
|
g.output("out", o);
|
|
let graph = g.build();
|
|
|
|
let rt = build_runtime();
|
|
let c = compile(graph, &rt.kernels).unwrap();
|
|
|
|
let dst_tensor = Tensor::from_f32(dst_meta.clone(), &[0f32; 16]);
|
|
let dst_addr = Arc::as_ptr(&dst_tensor.storage) as usize;
|
|
|
|
let mut inputs = HashMap::new();
|
|
inputs.insert("dst".into(), dst_tensor);
|
|
inputs.insert("src".into(), Tensor::from_f32(src_meta.clone(),
|
|
&[1.0, 2.0, 3.0, 4.0]));
|
|
|
|
let r = execute(&c, inputs, ExecutionOptions::default()).unwrap();
|
|
let out = r.outputs.get("out").unwrap();
|
|
let out_addr = Arc::as_ptr(&out.storage) as usize;
|
|
|
|
let mode = if *in_place { "scatter_inplace" } else { "scatter (plain)" };
|
|
let aliased = if out_addr == dst_addr { "ALIASED ✓" } else { "fresh alloc" };
|
|
println!(" {:<16} dst_addr=0x{:012x} out_addr=0x{:012x} {}",
|
|
mode, dst_addr, out_addr, aliased);
|
|
}
|
|
println!("Aliased path skips the O(max_seq * d_head) dst→output memcpy.");
|
|
}
|