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]>
117 lines
4.1 KiB
Python
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())
|