Full review of the JouleClaw Rust workspace (120 crates) to deliver "no errors, no warnings, no stubs, no code islands" as of 2026-06-03. Mechanical state, post-audit: - cargo build --workspace --all-targets: 0 errors, 0 warnings - cargo test --workspace --no-fail-fast: 3096 pass, 0 fail, 57 ignored - 0 stubs / unimplemented! in production code paths (the 2 hits are in test helpers, both explicitly scoped) - 0 genuine code islands (40 leaf crates are all legit: binaries, standalone end-user surfaces, or recent waves) Files touched in the cleanup: - jouleclaw-omni (Cargo.toml + src/lib.rs): vendored efficient-genai port — added #![allow(missing_docs)] at crate root with documented reason (we did not author 100k+ LOC of ML internals; the PUBLIC surface is documented at module level). Also added [lints.rust] check-cfg for the dropped `jouledb` feature flag so the gate doesn't surface as `unexpected_cfgs`. - jouleclaw-edge: restored `use std::sync::Arc` in the test module (cargo-fix regression — Arc is used in a #[cfg(test)] block). - jouleclaw-attest, -prov, -mcp, -federation, -sketch, -gate, -loader-gguf, -agents-md, -workspace: added rustdoc comments on every previously-undocumented public item (struct fields, methods, enum variants, associated functions, trait methods). 291 tests across those crates verified green. - jouleclaw-loader-gguf: removed unnecessary parentheses around two assigned bit-mask values. - jouleclaw-agents-md: registered `cfg(kani)` via [lints.rust] check-cfg so the kani-proof markers don't surface as warnings. - jouleclaw-workspace: marked EntryKind::wire as #[allow(dead_code)] (used by tests at lines 782–784). - Workspace-wide cargo fix + clippy --fix sweep: knocked out is_multiple_of, div_ceil, collapsible-if, unused imports, unused variables, unused mut, and similar lints across many crates. The jouleclaw-omni vendored port carries internal TODOs from the upstream `efficient-genai` repo (HAL/CUDA/Cranelift/RoPE/flash-attn scaffolding) — those are documented incomplete paths, not JouleClaw's own stubs. The PUBLIC JouleClaw surface (cascade, receipts, agents, contract, mission, verify, policy, workspace, hydra, …) has zero stubs and zero TODOs. Co-Authored-By: Claude Opus 4.7 <[email protected]>
246 lines
8.9 KiB
Rust
246 lines
8.9 KiB
Rust
//! Tests for the embedding API.
|
|
|
|
use jouleclaw_loader_gguf::read_gguf;
|
|
use jouleclaw_loader_gguf::synthetic::{synthesize_llama_gguf, SyntheticConfig};
|
|
use jouleclaw_loader_gguf::tokenizer::Vocab;
|
|
use jouleclaw_runtime::{embed, cosine_similarity, EmbedConfig, Pooling};
|
|
use std::io::Cursor;
|
|
|
|
fn build_test_vocab() -> Vec<(String, f32)> {
|
|
let mut v = vec![
|
|
("<unk>".to_string(), 0.0),
|
|
("<s>".to_string(), 0.0),
|
|
("</s>".to_string(), 0.0),
|
|
("\u{2581}".to_string(), 0.0),
|
|
];
|
|
for c in 'a'..='z' { v.push((c.to_string(), -1.0)); }
|
|
for c in 'a'..='z' { v.push((format!("\u{2581}{}", c), -0.5)); }
|
|
for b in 0..256u32 { v.push((format!("<0x{:02X}>", b), -10.0)); }
|
|
v
|
|
}
|
|
|
|
fn build_test_model(head_count: usize, head_count_kv: usize) -> jouleclaw_loader_gguf::GgufModel {
|
|
let vocab = build_test_vocab();
|
|
let cfg = SyntheticConfig {
|
|
vocab_size: vocab.len(),
|
|
embedding_length: 16,
|
|
block_count: 2,
|
|
feed_forward_length: 32,
|
|
head_count, head_count_kv,
|
|
rms_eps: 1e-6,
|
|
seed: 200,
|
|
vocab: Some(vocab),
|
|
merges: None,
|
|
bos_id: Some(1), eos_id: Some(2), unk_id: Some(0), chat_template: None,
|
|
};
|
|
let bytes = synthesize_llama_gguf(&cfg);
|
|
read_gguf(Cursor::new(bytes)).unwrap()
|
|
}
|
|
|
|
/// Mean pooling produces a [d_model] vector that is L2-normalized.
|
|
#[test]
|
|
fn mean_pooling_produces_d_model_vector() {
|
|
let model = build_test_model(4, 4);
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
let cfg = EmbedConfig::default();
|
|
let result = embed(&model, &vocab, "hello world", &cfg).unwrap();
|
|
assert_eq!(result.vector.len(), 16);
|
|
assert_eq!(result.d_model, 16);
|
|
|
|
// L2 norm should be 1.0 (within FP tolerance).
|
|
let norm_sq: f32 = result.vector.iter().map(|x| x * x).sum();
|
|
assert!((norm_sq - 1.0).abs() < 1e-5,
|
|
"L2-normalized vector should have norm 1; got norm_sq = {}", norm_sq);
|
|
}
|
|
|
|
/// Last-token pooling produces a different vector than mean pooling.
|
|
#[test]
|
|
fn last_token_pooling_differs_from_mean() {
|
|
let model = build_test_model(4, 4);
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
|
|
let mut cfg = EmbedConfig::default();
|
|
let mean = embed(&model, &vocab, "abc def ghi", &cfg).unwrap();
|
|
cfg.pooling = Pooling::LastToken;
|
|
let last = embed(&model, &vocab, "abc def ghi", &cfg).unwrap();
|
|
|
|
assert_eq!(mean.vector.len(), last.vector.len());
|
|
let cos = cosine_similarity(&mean.vector, &last.vector);
|
|
assert!(cos < 0.999,
|
|
"mean and last-token pooling should produce different vectors, \
|
|
got cos similarity = {}", cos);
|
|
}
|
|
|
|
/// No-pooling returns the raw per-token hidden states.
|
|
#[test]
|
|
fn no_pooling_returns_per_token_states() {
|
|
let model = build_test_model(4, 4);
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
let cfg = EmbedConfig {
|
|
pooling: Pooling::None,
|
|
l2_normalize: false,
|
|
..EmbedConfig::default()
|
|
};
|
|
let result = embed(&model, &vocab, "abc", &cfg).unwrap();
|
|
// Vector should be seq * d_model elements.
|
|
assert_eq!(result.vector.len(), result.token_count * result.d_model);
|
|
assert!(result.token_count > 0);
|
|
}
|
|
|
|
/// Embeddings are deterministic given identical input.
|
|
#[test]
|
|
fn embedding_is_deterministic() {
|
|
let model = build_test_model(4, 4);
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
let cfg = EmbedConfig::default();
|
|
let e1 = embed(&model, &vocab, "deterministic test", &cfg).unwrap();
|
|
let e2 = embed(&model, &vocab, "deterministic test", &cfg).unwrap();
|
|
let e3 = embed(&model, &vocab, "deterministic test", &cfg).unwrap();
|
|
assert_eq!(e1.vector, e2.vector);
|
|
assert_eq!(e2.vector, e3.vector);
|
|
}
|
|
|
|
/// Self-similarity is exactly 1.0 for L2-normalized vectors.
|
|
#[test]
|
|
fn self_similarity_is_one() {
|
|
let model = build_test_model(4, 4);
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
let cfg = EmbedConfig::default();
|
|
let result = embed(&model, &vocab, "test", &cfg).unwrap();
|
|
let cos = cosine_similarity(&result.vector, &result.vector);
|
|
assert!((cos - 1.0).abs() < 1e-5,
|
|
"self-similarity should be 1.0, got {}", cos);
|
|
}
|
|
|
|
/// Different texts produce different embeddings; cosine similarity should
|
|
/// be in [-1, 1] and not stuck at 1.
|
|
#[test]
|
|
fn different_texts_produce_different_embeddings() {
|
|
let model = build_test_model(4, 4);
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
let cfg = EmbedConfig::default();
|
|
let e1 = embed(&model, &vocab, "abc", &cfg).unwrap();
|
|
let e2 = embed(&model, &vocab, "xyz", &cfg).unwrap();
|
|
let cos = cosine_similarity(&e1.vector, &e2.vector);
|
|
assert!(cos >= -1.0 && cos <= 1.0);
|
|
assert!(cos < 0.999,
|
|
"different texts should produce distinguishable embeddings, \
|
|
got cos = {}", cos);
|
|
// Embedding values themselves should differ.
|
|
assert_ne!(e1.vector, e2.vector);
|
|
}
|
|
|
|
/// Embeddings work with GQA models.
|
|
#[test]
|
|
fn embed_works_with_gqa() {
|
|
let model = build_test_model(4, 2); // GQA
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
let cfg = EmbedConfig::default();
|
|
let result = embed(&model, &vocab, "hello", &cfg).unwrap();
|
|
assert_eq!(result.vector.len(), 16);
|
|
let norm_sq: f32 = result.vector.iter().map(|x| x * x).sum();
|
|
assert!((norm_sq - 1.0).abs() < 1e-5);
|
|
}
|
|
|
|
/// L2 normalization can be disabled.
|
|
#[test]
|
|
fn l2_normalize_can_be_disabled() {
|
|
let model = build_test_model(4, 4);
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
let mut cfg = EmbedConfig::default();
|
|
cfg.l2_normalize = false;
|
|
let result = embed(&model, &vocab, "abc", &cfg).unwrap();
|
|
let norm_sq: f32 = result.vector.iter().map(|x| x * x).sum();
|
|
// Unnormalized norm should generally NOT be 1.0.
|
|
assert!((norm_sq - 1.0).abs() > 1e-5 || norm_sq < 1e-10,
|
|
"unnormalized embedding norm shouldn't accidentally be 1.0; got norm_sq = {}",
|
|
norm_sq);
|
|
}
|
|
|
|
/// Empty prompt with add_bos handled gracefully (either errors or returns
|
|
/// something sensible; we just don't crash).
|
|
#[test]
|
|
fn empty_prompt_handled_gracefully() {
|
|
let model = build_test_model(4, 4);
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
let cfg = EmbedConfig {
|
|
add_bos: false,
|
|
..EmbedConfig::default()
|
|
};
|
|
// Don't crash; either error or return is acceptable.
|
|
let _ = embed(&model, &vocab, "", &cfg);
|
|
}
|
|
|
|
/// Cosine similarity properties: symmetric, in [-1, 1], 1.0 on identical.
|
|
#[test]
|
|
fn cosine_similarity_properties() {
|
|
let a = vec![0.6f32, 0.8];
|
|
let b = vec![0.8f32, 0.6];
|
|
let c = vec![-0.6f32, -0.8]; // anti-parallel to a
|
|
|
|
assert!((cosine_similarity(&a, &a) - 1.0).abs() < 1e-6);
|
|
assert!((cosine_similarity(&a, &b) - 0.96).abs() < 1e-3);
|
|
assert!((cosine_similarity(&a, &c) - (-1.0)).abs() < 1e-6);
|
|
|
|
// Symmetric.
|
|
assert_eq!(cosine_similarity(&a, &b), cosine_similarity(&b, &a));
|
|
}
|
|
|
|
/// Embed encoder graph is cheaper than full generate (no lm_head matmul).
|
|
/// We verify this indirectly: the encoder graph runs successfully and
|
|
/// returns the post-norm hidden states, demonstrating that the lm_head
|
|
/// is skipped.
|
|
#[test]
|
|
fn encoder_graph_skips_lm_head() {
|
|
let model = build_test_model(4, 4);
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
|
|
let cfg = EmbedConfig {
|
|
pooling: Pooling::None,
|
|
l2_normalize: false,
|
|
..EmbedConfig::default()
|
|
};
|
|
let result = embed(&model, &vocab, "test prompt here", &cfg).unwrap();
|
|
|
|
// The hidden state size is d_model, not vocab_size. If the lm_head
|
|
// had been applied, the size would be vocab_size = 312, not d_model = 16.
|
|
assert_eq!(result.d_model, 16);
|
|
assert_eq!(result.vector.len(), result.token_count * 16);
|
|
}
|
|
|
|
/// Demo: side-by-side similarity comparison.
|
|
#[test]
|
|
fn embedding_similarity_demo() {
|
|
let model = build_test_model(4, 4);
|
|
let vocab = Vocab::from_gguf(&model).unwrap();
|
|
let cfg = EmbedConfig::default();
|
|
|
|
let texts = [
|
|
"the cat sat on the mat",
|
|
"a cat is sitting on a mat",
|
|
"quantum mechanics describes nature",
|
|
];
|
|
let embeddings: Vec<_> = texts.iter().map(|t| {
|
|
embed(&model, &vocab, t, &cfg).unwrap().vector
|
|
}).collect();
|
|
|
|
println!("\n=== Embedding similarity demo ===");
|
|
println!("Model: 2-layer synthetic Llama, d_model=16, random weights.");
|
|
println!("(Real models trained on text would show meaningful similarity ");
|
|
println!(" patterns; with random weights the similarities are arbitrary.)\n");
|
|
println!("Pooling: mean, L2-normalized");
|
|
println!("Texts:");
|
|
for (i, t) in texts.iter().enumerate() {
|
|
println!(" [{}] {:?}", i, t);
|
|
}
|
|
println!("\nPairwise cosine similarities:");
|
|
for i in 0..texts.len() {
|
|
for j in 0..texts.len() {
|
|
let cos = cosine_similarity(&embeddings[i], &embeddings[j]);
|
|
print!(" cos([{}], [{}]) = {:+.4}", i, j, cos);
|
|
}
|
|
println!();
|
|
}
|
|
println!("\nEmbedding dim: {} (vs vocab_size: {} — encoder graph skips lm_head)",
|
|
embeddings[0].len(), vocab.len());
|
|
}
|