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]>
116 lines
4.1 KiB
Rust
116 lines
4.1 KiB
Rust
//! Tests for the Repeat primitive.
|
||
//!
|
||
//! Repeat semantics: `output[..., i*N + j, ...] = input[..., j, ...]`
|
||
//! for `i in 0..repeats`, where N is the input's size on the chosen axis.
|
||
//! This is a "tile" pattern (repeats whole-axis blocks contiguously);
|
||
//! it is NOT a "repeat-interleave" pattern (which the GQA block builds
|
||
//! via reshape+repeat+reshape).
|
||
|
||
use jouleclaw_core::graph::GraphBuilder;
|
||
use jouleclaw_core::tensor::{Dtype, Tensor, TensorMeta};
|
||
use jouleclaw_runtime::{compile, execute, ExecutionOptions, Runtime};
|
||
use std::collections::HashMap;
|
||
|
||
fn run_repeat(data: Vec<f32>, shape: &[usize], axis: i32, repeats: usize) -> Tensor {
|
||
let mut g = GraphBuilder::new();
|
||
let meta = TensorMeta::new(Dtype::F32, shape);
|
||
let x = g.input("x", meta.clone());
|
||
let y = g.repeat(x, axis, repeats);
|
||
g.output("y", y);
|
||
let graph = g.build();
|
||
|
||
let runtime = Runtime::boot();
|
||
let compiled = compile(graph, &runtime.kernels).unwrap();
|
||
let mut inputs = HashMap::new();
|
||
inputs.insert("x".into(), Tensor::from_f32(meta, &data));
|
||
let res = execute(&compiled, inputs, ExecutionOptions::default()).unwrap();
|
||
res.outputs.get("y").unwrap().clone()
|
||
}
|
||
|
||
/// 1D repeat: tile the whole sequence twice.
|
||
#[test]
|
||
fn repeat_1d_tiles_axis() {
|
||
let out = run_repeat(vec![1.0, 2.0, 3.0], &[3], 0, 2);
|
||
assert_eq!(out.meta.shape, vec![6]);
|
||
assert_eq!(out.as_f32_vec(), vec![1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
|
||
}
|
||
|
||
/// repeats=1 is a no-op (same shape, copy of data).
|
||
#[test]
|
||
fn repeat_one_is_identity() {
|
||
let out = run_repeat(vec![1.0, 2.0, 3.0, 4.0], &[2, 2], 0, 1);
|
||
assert_eq!(out.meta.shape, vec![2, 2]);
|
||
assert_eq!(out.as_f32_vec(), vec![1.0, 2.0, 3.0, 4.0]);
|
||
}
|
||
|
||
/// 2D repeat on axis 0: tile rows.
|
||
#[test]
|
||
fn repeat_2d_axis_0() {
|
||
// 2x3 input → repeat 3 times along axis 0 → 6x3.
|
||
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
|
||
let out = run_repeat(data, &[2, 3], 0, 3);
|
||
assert_eq!(out.meta.shape, vec![6, 3]);
|
||
let expected = vec![
|
||
1.0, 2.0, 3.0, 4.0, 5.0, 6.0,
|
||
1.0, 2.0, 3.0, 4.0, 5.0, 6.0,
|
||
1.0, 2.0, 3.0, 4.0, 5.0, 6.0,
|
||
];
|
||
assert_eq!(out.as_f32_vec(), expected);
|
||
}
|
||
|
||
/// 2D repeat on axis 1: tile within each row.
|
||
#[test]
|
||
fn repeat_2d_axis_1() {
|
||
let data = vec![1.0, 2.0, 3.0, 4.0];
|
||
let out = run_repeat(data, &[2, 2], 1, 3);
|
||
assert_eq!(out.meta.shape, vec![2, 6]);
|
||
// Each row: [1,2,1,2,1,2] / [3,4,3,4,3,4]
|
||
let expected = vec![
|
||
1.0, 2.0, 1.0, 2.0, 1.0, 2.0,
|
||
3.0, 4.0, 3.0, 4.0, 3.0, 4.0,
|
||
];
|
||
assert_eq!(out.as_f32_vec(), expected);
|
||
}
|
||
|
||
/// Negative axis indexing.
|
||
#[test]
|
||
fn repeat_negative_axis() {
|
||
let data = vec![1.0, 2.0, 3.0, 4.0];
|
||
let out = run_repeat(data, &[2, 2], -1, 2);
|
||
assert_eq!(out.meta.shape, vec![2, 4]);
|
||
assert_eq!(out.as_f32_vec(), vec![1.0, 2.0, 1.0, 2.0, 3.0, 4.0, 3.0, 4.0]);
|
||
}
|
||
|
||
/// 3D repeat on middle axis (the GQA-broadcast pattern):
|
||
/// `[n_kv, 1, seq*d_head] → [n_kv, group_size, seq*d_head]` then reshape
|
||
/// produces the repeat-interleaved K/V. Just test the raw repeat here.
|
||
#[test]
|
||
fn repeat_3d_middle_axis_for_gqa_broadcast() {
|
||
// Input [2, 1, 3]: two kv-heads, singleton group axis, 3 features.
|
||
// Repeat axis=1 with repeats=2: result [2, 2, 3] with each kv-head's
|
||
// content duplicated along the new group axis.
|
||
let data = vec![
|
||
1.0, 2.0, 3.0, // kv-head 0
|
||
4.0, 5.0, 6.0, // kv-head 1
|
||
];
|
||
let out = run_repeat(data, &[2, 1, 3], 1, 2);
|
||
assert_eq!(out.meta.shape, vec![2, 2, 3]);
|
||
// kv-head 0 row 0: [1,2,3]; row 1: [1,2,3]
|
||
// kv-head 1 row 0: [4,5,6]; row 1: [4,5,6]
|
||
// After reshape to [4, 3] this gives [1,2,3, 1,2,3, 4,5,6, 4,5,6]
|
||
// — the repeat-interleave pattern needed for GQA.
|
||
let expected = vec![
|
||
1.0, 2.0, 3.0, 1.0, 2.0, 3.0, // kv-head 0 ×2
|
||
4.0, 5.0, 6.0, 4.0, 5.0, 6.0, // kv-head 1 ×2
|
||
];
|
||
assert_eq!(out.as_f32_vec(), expected);
|
||
}
|
||
|
||
/// Repeat is deterministic.
|
||
#[test]
|
||
fn repeat_is_deterministic() {
|
||
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||
let out1 = run_repeat(data.clone(), &[5], 0, 4);
|
||
let out2 = run_repeat(data, &[5], 0, 4);
|
||
assert_eq!(out1.storage.bytes, out2.storage.bytes);
|
||
}
|