Skip to content

Commit 9db9a3a

Browse files
Praz314159CopilotForpee
authored
Fix BGE-small export compatibility (#263)
* Fix BGE-small export compatibility The documented BGE flow (scripts/download_bge_small_en_v1_5.py + the bge examples) is broken on a fresh download: current optimum exports produce a graph the tracer rejects at load with 'Unimplemented ONNX operator: Sqrt'. Root causes (export drift): - Fused LayerNormalization (opset >= 17) decomposes under tract into Div(x, Sqrt(var+eps)); tract only re-fuses Recip(Sqrt(.)) into Rsqrt (see declutter_recip), and the tracer has an Rsqrt handler but no Sqrt handler. - The export now has two graph outputs (token_embeddings, sentence_embedding); the prover seeds an opening only for outputs()[0] (prover.rs output_claim), so the second output is never opened (EvalReduction EmptyInput). - The export takes two inputs now (token_type_ids dropped), so the examples' three-input call panics with an out-of-bounds index at execution. Fix, in the download script's normalize_graph(): - Pin --opset 14 (keeps LayerNorm decomposed) and rewrite Div(x, Sqrt(v)) -> Mul(x, Reciprocal(Sqrt(v))) so tract fuses to Rsqrt. - Keep token_embeddings (the encoder's last hidden state) as the sole output and dead-node eliminate the pooling / L2-norm subgraph that only fed sentence_embedding. Pooling + normalization are a cheap public post-step. - Update bge.rs / bge_generate.rs to the two-input signature. Verified: scripts/download + 'cargo run -p jolt-atlas-core --example bge' loads, traces, proves, and verifies end to end. Note: keeping the full model's sentence_embedding output instead hits a separate degenerate-instance panic in the Shout lookup prover (scalar Rsqrt over the single pooled element -> RaPolynomial stuck in Round1 -> final_claim); filed separately. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Khalil Gibran Hassam <59589957+Forpee@users.noreply.github.com>
1 parent f521423 commit 9db9a3a

3 files changed

Lines changed: 85 additions & 8 deletions

File tree

atlas-onnx-tracer/examples/bge_generate.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
/// | Index | Name | Shape | Description |
1414
/// |-------|-----------------|------------------|---------------------------------------|
1515
/// | 0 | input_ids | [1, seq_len] | Token IDs (Gather indices) |
16-
/// | 1 | token_type_ids | [1, seq_len] | Segment IDs (all zeros for single) |
17-
/// | 2 | attention_mask | [1, seq_len] | Binary mask (1 = attend) |
16+
/// | 1 | attention_mask | [1, seq_len] | Binary mask (1 = attend) |
1817
use atlas_onnx_tracer::model::{Model, RunArgs};
1918

2019
fn main() {

jolt-atlas-core/examples/bge.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,7 @@ fn main() {
4646
let input_ids_data: Vec<i32> = (0..seq_len).map(|_| rng.gen_range(0..vocab_size)).collect();
4747
let input_ids = Tensor::new(Some(&input_ids_data), &[1, seq_len]).unwrap();
4848

49-
// Input 1: token_type_ids — segment IDs in {0, 1}. Use all zeros for single-segment input.
50-
let token_type_ids_data: Vec<i32> = vec![0; seq_len];
51-
let token_type_ids = Tensor::new(Some(&token_type_ids_data), &[1, seq_len]).unwrap();
52-
53-
// Input 2: attention_mask — all 1s (attend everywhere), in BERT-style binary form.
49+
// Input 1: attention_mask — all 1s (attend everywhere), in BERT-style binary form.
5450
let attention_mask_data: Vec<i32> = vec![1; seq_len];
5551
let attention_mask = Tensor::new(Some(&attention_mask_data), &[1, seq_len]).unwrap();
5652

@@ -61,7 +57,7 @@ fn main() {
6157
let timing = std::time::Instant::now();
6258
let (proof, io, _debug_info) = ONNXProof::<Fr, Blake2bTranscript, HyperKZG<Bn254>>::prove(
6359
&prover_preprocessing,
64-
&[input_ids, token_type_ids, attention_mask],
60+
&[input_ids, attention_mask],
6561
);
6662
println!("Proof generation took {:.2?}", timing.elapsed());
6763

scripts/download_bge_small_en_v1_5.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ def export_model():
4343
"optimum.exporters.onnx",
4444
"--model",
4545
MODEL_ID,
46+
"--opset",
47+
"14",
4648
str(MODEL_DIR),
4749
],
4850
)
@@ -53,6 +55,85 @@ def export_model():
5355
print("Export complete.")
5456

5557

58+
def normalize_graph():
59+
"""Rewrite export patterns the Atlas tracer/prover does not support.
60+
61+
The stock optimum feature-extraction export is incompatible with the Atlas
62+
tracer in three ways; all are handled here so the documented flow works
63+
end to end (load, trace, prove, verify).
64+
65+
1. Fused ``LayerNormalization`` (opset >= 17) decomposes under tract into
66+
``Div(x, Sqrt(var + eps))``, which tract cannot re-fuse: it only rewrites
67+
``Recip(Sqrt(.))`` to ``Rsqrt`` (see tract's ``declutter_recip``), and the
68+
tracer has an ``Rsqrt`` handler but no ``Sqrt`` handler. We pin
69+
``--opset 14`` at export (keeps LayerNorm decomposed) and rewrite
70+
``Div(x, Sqrt(v))`` into ``Mul(x, Reciprocal(Sqrt(v)))`` so tract fuses it.
71+
72+
2. The export has two graph outputs: ``token_embeddings`` (the encoder's
73+
last hidden state) and ``sentence_embedding`` (mean/CLS pooled and
74+
L2-normalized). The Atlas prover seeds an opening only for
75+
``outputs()[0]`` (see prover.rs ``output_claim``), so a second output is
76+
never opened. We keep ``token_embeddings`` as the sole output -- it is
77+
the full 12-layer encoder result, and pooling + normalization are a
78+
cheap public post-step -- then dead-node eliminate the pooling / L2-norm
79+
subgraph that only fed ``sentence_embedding``.
80+
81+
Dropping that subgraph also removes its CLS-pooling ``Gather(axis=1)`` (the
82+
tracer's Gather is axis-0 only) and its final scalar ``Rsqrt`` over a
83+
single pooled element, which triggers a degenerate-instance panic in the
84+
Shout lookup prover (see the referenced issue).
85+
"""
86+
import onnx
87+
from onnx import helper
88+
89+
path = MODEL_DIR / "model.onnx"
90+
model = onnx.load(str(path))
91+
g = model.graph
92+
93+
# -- 1. Div(x, Sqrt(v)) -> Mul(x, Reciprocal(Sqrt(v))) ------------------
94+
sqrt_outs = {n.output[0] for n in g.node if n.op_type == "Sqrt"}
95+
nodes, patched_div = [], 0
96+
for n in g.node:
97+
if n.op_type == "Div" and len(n.input) == 2 and n.input[1] in sqrt_outs:
98+
recip_out = n.output[0] + "_recip"
99+
nodes.append(helper.make_node(
100+
"Reciprocal", [n.input[1]], [recip_out], name=n.name + "_recip"))
101+
nodes.append(helper.make_node(
102+
"Mul", [n.input[0], recip_out], [n.output[0]], name=n.name + "_mul"))
103+
patched_div += 1
104+
else:
105+
nodes.append(n)
106+
107+
# -- 2. Single output = token_embeddings, then dead-node elimination ----
108+
keep_output = "token_embeddings"
109+
pruned_outputs = 0
110+
if any(o.name == keep_output for o in g.output) and len(g.output) > 1:
111+
for o in [o for o in g.output if o.name != keep_output]:
112+
g.output.remove(o)
113+
pruned_outputs += 1
114+
115+
producer = {out: n for n in nodes for out in n.output}
116+
live = set()
117+
frontier = [o.name for o in g.output]
118+
while frontier:
119+
name = frontier.pop()
120+
n = producer.get(name)
121+
if n is None or id(n) in live:
122+
continue
123+
live.add(id(n))
124+
frontier.extend(n.input)
125+
kept = [n for n in nodes if id(n) in live]
126+
removed_nodes = len(nodes) - len(kept)
127+
128+
del g.node[:]
129+
g.node.extend(kept)
130+
onnx.save(model, str(path))
131+
print(
132+
f"Graph normalized: {patched_div} layernorm div(s) rewritten, "
133+
f"{pruned_outputs} output(s) pruned, {removed_nodes} dead node(s) removed."
134+
)
135+
136+
56137
def rename_network():
57138
"""Rename model.onnx -> network.onnx for compatibility."""
58139
src = MODEL_DIR / "model.onnx"
@@ -70,6 +151,7 @@ def rename_network():
70151
def main():
71152
ensure_packages()
72153
export_model()
154+
normalize_graph()
73155
rename_network()
74156
print(f"\nOK: {MODEL_ID} ONNX model is ready at {MODEL_DIR}")
75157

0 commit comments

Comments
 (0)