open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-runtime/tests/repeat.rs
Transaction Science 29dff259e4 jouleclaw Phase 3: backends (reference + apple) + runtime
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]>
2026-05-26 13:09:59 -04:00

116 lines
4.1 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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);
}