open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-deberta/scripts/hf_reference_embedding.py
Transaction Science d8ba701449 jouleclaw Phase 6: Edge-First v6 ports + 4 net-new standard-defining crates
12 crates landed (8 ports + 4 fresh designs). All 866 tests pass
across Phase 1-6; 0 failures; 52 ignored (39 inherited from
efficient-genai as hardware-gated, 12 in deberta as model-file-gated,
1 in sana_wm pre-existing efficient-genai input-size bug).

Phase 6a — Edge-First v6 surface (ported from pattern-lang)

  jouleclaw-schema       ~2137 LOC. QueryPlan, RetrievedItem,
                         KnowledgeAxes, EpistemicMode, AtomicClaim,
                         EntailmentResult, VerificationReport, Answer,
                         RAP, SystemCapabilities, AnytimeResult,
                         Invariants — the type vocabulary the rest of
                         the v6 surface is written against.

  jouleclaw-plan         ~1484 LOC. Query planner — picks the cascade
                         walk for a given input.

  jouleclaw-execute      ~2780 LOC. Plan executor with retry, anytime,
                         and budget primitives.

  jouleclaw-compose      ~1467 LOC. Verified-answer composer.

  jouleclaw-deberta      ~4801 LOC. DeBERTa-v3 forward pass for NLI
                         (used by jouleclaw-diagnose). Apple Accelerate
                         cblas_sgemm FFI gained `unsafe extern "C"` for
                         Rust 2024. Tests that require a real model on
                         disk now skip gracefully (`eprintln!("[skip]")`
                         + early return) instead of panicking — they
                         remain in-tree as integration tests and run
                         automatically when the model is present at
                         `models/deberta-v3-large-mnli/`.

  jouleclaw-diagnose     ~2353 LOC. Verification stage — runs NLI
                         against retrieved claims to certify the
                         answer.

  jouleclaw-edge-cli     ~2876 LOC. CLI binary that wires
                         schema/plan/execute/diagnose/compose/deberta
                         into a single edge agent surface.

  jouleclaw-edge-client  ~224 LOC. Thin stdin/stdout RPC client.

  Deferred: joule-edge (server) — its workspace depends on joule-l1 +
  lean-bridge + synth, which are OpenIE IP that do not port into the
  public open standard. Will land when the thin LawfulTier trait
  surface in jouleclaw-cascade ships in a follow-on commit.

Phase 6b-e — net-new (the standard-defining moat)

  jouleclaw-prov   Receipt-per-resolution emitter. CascadeTier (L0–L4
                   with stable wire tags + named tags),
                   ClaimProvenance (PROV-O compatible Entity shape),
                   ToolTouch (per-tool joule accounting),
                   ReceiptBuilder with worst-counter-floor provenance
                   semantics. Smart-Byte-envelope shaped so receipts
                   seal cleanly into the sibling standard's signed
                   transport. 6 tests passing.

  jouleclaw-fresh  L3.5 cascade stage — fresh retrieval with
                   provenance. SearchProvider trait (Brave / Tavily /
                   Exa / Serper plug in here), Transport trait (keeps
                   reqwest out of the standard), TrustTable
                   (bootstrap from Wikipedia perennial-sources;
                   normalises `www.` prefix), FreshFetch orchestrator,
                   `provenance_envelope()` that wraps a RetrievedClaim
                   into a jouleclaw-prov ClaimProvenance ready to
                   embed in a cascade receipt. 6 tests passing.

  jouleclaw-mcp    MCP tool surface with joule metering. MeteredTool
                   trait + `dispatch_metered()` brackets every tool
                   call with energy counter reads and emits a
                   ToolTouch into the running receipt. WireEncoding
                   (JsonRpc / Cbor) + `negotiate()` defines the
                   `joule-mcp` capability profile —
                   `x-jouleclaw/joule-mcp@1` is the canonical
                   negotiation tag; both endpoints advertise it to
                   opt into CBOR, otherwise stays JSON-RPC for
                   compat. 5 tests passing.

  jouleclaw-cli    `jclaw` binary — the Pi-class minimal harness.
                   v0.1 ships `version`, `tier-walk`, `receipt`,
                   `meter` subcommands plus a heuristic router that
                   picks a tier from input shape. Receipt builder is
                   live; energy counter binding is a deployment
                   choice (feature-gated per platform). Hot-reload +
                   tree-forked sessions + full MCP client land
                   later — v0.1 proves the type vocabulary and the
                   end-to-end receipt emission. 4 tests passing.

Phase 6 cumulative

  17 → 29 crates in the workspace.
  ~155k → ~170k LOC.
  440 → 866 tests passing.
  Net-new code surface: ~1500 LOC of standard-defining moat
  (jouleclaw-prov + fresh + mcp + cli).

Next: SPEC.md + CHARTER + conformance/v1 wire vectors + the
memory entry. Then promote to a public release tag.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 18:44:46 -04:00

117 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Dump the DeBERTa-v3 embedding-layer output for one canonical NLI
pair so the Rust implementation can be verified numerically.
For v3, the embedding layer is:
embeddings = LayerNorm(word_embeddings[input_ids])
embeddings *= attention_mask.unsqueeze(-1) # mask out PAD positions
(No absolute position embeddings — position_biased_input=False; no
token-type embeddings — type_vocab_size=0.)
We run the HF model up to *just after* the embedding LayerNorm and
mask multiply, then save:
- input_ids
- attention_mask
- embedding_output (the tensor the encoder will consume)
Computed in fp32 (model is upcast on load) so the Rust comparison
isn't fighting fp16 quantization.
Usage:
python3 crates/joule-deberta/scripts/hf_reference_embedding.py
Writes:
crates/joule-deberta/fixtures/embedding_simple_entail.json
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer # type: ignore
MODEL_DIR = Path("models/deberta-v3-large-mnli")
OUT_PATH = Path("crates/joule-deberta/fixtures/embedding_simple_entail.json")
# Same first pair from the tokenizer fixture — keeps the verification
# chain coherent.
PREMISE = "Paris is the capital of France."
HYPOTHESIS = "France's capital is Paris."
def main() -> int:
if not MODEL_DIR.exists():
print(f"ERR: model dir {MODEL_DIR} not found", file=sys.stderr)
return 1
tok = AutoTokenizer.from_pretrained(str(MODEL_DIR))
model = AutoModelForSequenceClassification.from_pretrained(
str(MODEL_DIR), torch_dtype=torch.float32
)
model.eval()
enc = tok(
PREMISE, HYPOTHESIS,
add_special_tokens=True, truncation=False, padding=False,
return_attention_mask=True, return_tensors="pt",
)
input_ids = enc["input_ids"]
attention_mask = enc["attention_mask"]
# Walk the embedding sub-module by hand so we capture the exact
# tensor the encoder receives — not the raw word embeddings, but
# after LayerNorm + mask multiply.
emb_layer = model.deberta.embeddings
with torch.no_grad():
word_embeds = emb_layer.word_embeddings(input_ids)
# v3: position_biased_input=False, type_vocab_size=0, so we
# skip both branches and go straight to LayerNorm.
ln_out = emb_layer.LayerNorm(word_embeds)
# Mask broadcast: [B, S] -> [B, S, 1] times [B, S, H].
mask_b = attention_mask.unsqueeze(-1).to(ln_out.dtype)
embedding_output = ln_out * mask_b
# No dropout at eval time.
# Strip the batch dim — Rust side computes over a single sequence.
embedding_output = embedding_output.squeeze(0)
input_ids_list = input_ids.squeeze(0).tolist()
attention_mask_list = attention_mask.squeeze(0).tolist()
# Flatten [seq_len, hidden_size] to row-major flat list.
seq_len, hidden = embedding_output.shape
flat = embedding_output.flatten().tolist()
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
with OUT_PATH.open("w") as f:
json.dump({
"model_source": str(MODEL_DIR),
"case_name": "simple_entail",
"premise": PREMISE,
"hypothesis": HYPOTHESIS,
"input_ids": input_ids_list,
"attention_mask": attention_mask_list,
"seq_len": seq_len,
"hidden_size": hidden,
"dtype": "float32",
"embedding_output_flat": flat,
# Sanity stats so the Rust side can see what it's
# comparing against without inspecting the full tensor.
"stats": {
"min": float(embedding_output.min()),
"max": float(embedding_output.max()),
"mean": float(embedding_output.mean()),
"abs_mean": float(embedding_output.abs().mean()),
},
}, f)
print(
f"wrote {OUT_PATH}: seq_len={seq_len} hidden={hidden} "
f"abs_mean={embedding_output.abs().mean():.4f}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())