Skip to content

Commit 0f0a336

Browse files
committed
perf(joint): infer on burn-flex (burn 0.21) — ~5.6x faster cold inference
Upgrade burn 0.19.1 -> 0.21.0 and switch the joint tagger's runtime inference backend from burn-ndarray to burn-flex, a fast pure-Rust CPU backend (gemm + SIMD). flex is now the only inference backend, on every target including wasm32 (no rayon + critical-section atomics shim there). - cold inference ~5.6x faster (0.53ms vs 2.97ms/sentence), byte-identical output; real-prose parse_essay 5.9x faster, lint_essay_uncached 3.4x. - retrain the embedded model on 0.21 (UPOS 0.9463 / NP F1 0.8872); the 0.19 model.mpk can't load on 0.21 (version guard + new LSTM gate_activation field). - burn 0.21 moved Backend::Device to a BackendTypes supertrait; name the concrete device type in runtime.rs instead. - regenerate_english_joint: AutoGraphicsApi instead of hardcoded Metal so training runs on any wgpu GPU (Metal/Vulkan/DX12). - save_then_load_preserves_forward: rewrite as an fp16->fp16 round-trip equality check (the old fp32-vs-fp16 threshold failed on both backends under 0.21's RNG, not a flex issue). - char/suffix vocab JSON now pretty-printed with ordered keys for diff-friendly artifacts; key order is cosmetic (ids are the values). burn-ndarray remains only for the legacy BurnChunker and tests.
1 parent 36caedc commit 0f0a336

11 files changed

Lines changed: 3930 additions & 1129 deletions

File tree

Cargo.lock

Lines changed: 1589 additions & 1053 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

harper-brill/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ harper-pos-utils = { path = "../harper-pos-utils/", version = "2.0.0" }
1111
serde_json = "1.0.150"
1212
# Only pulled in by the `training` feature (GPU regeneration tooling). Inference
1313
# and tests never need it — the trained model is embedded as bytes and run on CPU.
14-
burn = { version = "0.19.1", default-features = false, optional = true }
14+
burn = { version = "0.21.0", default-features = false, optional = true }
1515

1616
[build-dependencies]
1717
serde_json = "1.0.150"

harper-brill/examples/regenerate_english_joint.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use std::process::Command;
2525
use std::rc::Rc;
2626
use std::time::Instant;
2727

28-
use burn::backend::wgpu::{RuntimeOptions, WgpuDevice, graphics::Metal, init_setup};
28+
use burn::backend::wgpu::{RuntimeOptions, WgpuDevice, graphics::AutoGraphicsApi, init_setup};
2929
use burn::backend::{Autodiff, wgpu::Wgpu};
3030

3131
use harper_pos_utils::conllu_utils::extract_records_from_files;
@@ -176,9 +176,13 @@ fn main() {
176176
suffix_vocab.len()
177177
);
178178

179-
// Explicit Metal init must precede any tensor operations on macOS.
179+
// Explicit wgpu init must precede any tensor operations on macOS.
180+
// `AutoGraphicsApi` selects the platform's native graphics API at runtime —
181+
// Metal on macOS, Vulkan on Linux (NVIDIA/AMD/Intel), DX12 on Windows — so the
182+
// same example runs on any GPU wgpu supports, not just Apple Metal. Override
183+
// in tests via the `AUTO_GRAPHICS_BACKEND` env var.
180184
let device = WgpuDevice::DefaultDevice;
181-
init_setup::<Metal>(&device, RuntimeOptions::default());
185+
init_setup::<AutoGraphicsApi>(&device, RuntimeOptions::default());
182186

183187
let cfg = JointTrainConfig {
184188
char_dim: ARCH.char_dim,
Lines changed: 218 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,218 @@
1-
{"ي":132,"V":59,">":97,"²":211,"é":81,"T":32,"±":112,"ι":155,"è":169,"ę":198,"U":44,"³":90,"…":106,"况":182,"0":53,"í":110,"澹":188,"S":19,";":68,"?":73,"×":190,"樋":137,"<":96,"R":58,"Q":28,"=":93,"k":17,"3":38,"ə":149,"ë":215,"½":119,"ɛ":115,"•":172,"l":3,"2":40,"1":50,"m":7,"6":52,"h":20,"ف":193,"W":49,"̃":123,"奈":141,"j":63,"4":67,"5":69,"w":27,"°":202,"i":12,"ح":130,"v":43,"#":78,"ε":164,"ʻ":171,"u":22,"!":42,"æ":173,"ن":194,"\"":74,"t":24,"一":139,"ʃ":121,"c":13,"n":8,"ç":105,"/":76,"ه":128,"津":142,"β":154,"o":15,"b":21,"Ἱ":163,"远":186,"ʁ":116,"a":6,".":30,"`":88,"f":14,"8":62,"ú":147,"ο":156,"Σ":158,"谿":179,"'":57,"¢":175,"§":189,"e":10,"à":126,":":9,"d":18,"9":51,"£":109,"ˈ":117,"%":79,"y":29,"Ḥ":134,"á":103,"夏":143,"μ":167,"ū":146,"z":56,"$":82,"g":33,"ʊ":118,"x":71,"&":80,"ī":135,"ν":162,"â":170,"和":183,"î":216,"O":64,"Ã":89,"{":98,"–":108,"(":65,"N":60,"@":94,"ا":129,")":66,"}":102,"­":107,"ό":161,"*":87,"|":99,"清":185,"ω":159,"+":91,"空":177,"å":205,"~":100,"—":84,"口":138,"ź":197,"A":2,"C":70,"Ε":150,"p":25,"B":39,"-":4,"q":26,"J":46,"ρ":120,"€":191,"I":37,"r":11,",":23,"ô":125,"ς":157,"ɔ":168,"古":187,"s":16,"σ":152,"✉":208,"ö":217,"琴":181,"M":41,"د":131,"L":75,"H":48,"ó":111,"子":144,"έ":153,"ώ":165,"K":61,"ñ":113,"ɑ":122,"司":176,"ò":114,"山":180,"ż":199,"ś":201,"ὐ":151,"图":178,"ä":213,"[":31,"‘":92,"ō":136,"’":83,"7":72,"Ō":145,"φ":160,"ز":127,"É":124,"υ":166,"Ā":195,"ø":206,"☏":207,"º":210,"ß":212,"ā":133,"葉":140,"☎":203,"ص":192,"P":36,"E":47,"”":86,"·":101,"ł":200,"D":35,"_":77,"“":85,"ʒ":148,"Ł":196,"^":104,"F":45,"Ø":204,"œ":214,"]":34,"G":55,"静":184,"X":95,"Æ":209,"Y":54,"č":174,"Z":5}
1+
{
2+
"!": 42,
3+
"\"": 74,
4+
"#": 78,
5+
"$": 82,
6+
"%": 79,
7+
"&": 80,
8+
"'": 57,
9+
"(": 65,
10+
")": 66,
11+
"*": 87,
12+
"+": 91,
13+
",": 23,
14+
"-": 4,
15+
".": 30,
16+
"/": 76,
17+
"0": 53,
18+
"1": 50,
19+
"2": 40,
20+
"3": 38,
21+
"4": 67,
22+
"5": 69,
23+
"6": 52,
24+
"7": 72,
25+
"8": 62,
26+
"9": 51,
27+
":": 9,
28+
";": 68,
29+
"<": 96,
30+
"=": 93,
31+
">": 97,
32+
"?": 73,
33+
"@": 94,
34+
"A": 2,
35+
"B": 39,
36+
"C": 70,
37+
"D": 35,
38+
"E": 47,
39+
"F": 45,
40+
"G": 55,
41+
"H": 48,
42+
"I": 37,
43+
"J": 46,
44+
"K": 61,
45+
"L": 75,
46+
"M": 41,
47+
"N": 60,
48+
"O": 64,
49+
"P": 36,
50+
"Q": 28,
51+
"R": 58,
52+
"S": 19,
53+
"T": 32,
54+
"U": 44,
55+
"V": 59,
56+
"W": 49,
57+
"X": 95,
58+
"Y": 54,
59+
"Z": 5,
60+
"[": 31,
61+
"]": 34,
62+
"^": 104,
63+
"_": 77,
64+
"`": 88,
65+
"a": 6,
66+
"b": 21,
67+
"c": 13,
68+
"d": 18,
69+
"e": 10,
70+
"f": 14,
71+
"g": 33,
72+
"h": 20,
73+
"i": 12,
74+
"j": 63,
75+
"k": 17,
76+
"l": 3,
77+
"m": 7,
78+
"n": 8,
79+
"o": 15,
80+
"p": 25,
81+
"q": 26,
82+
"r": 11,
83+
"s": 16,
84+
"t": 24,
85+
"u": 22,
86+
"v": 43,
87+
"w": 27,
88+
"x": 71,
89+
"y": 29,
90+
"z": 56,
91+
"{": 98,
92+
"|": 99,
93+
"}": 102,
94+
"~": 100,
95+
"¢": 175,
96+
"£": 109,
97+
"§": 189,
98+
"­": 107,
99+
"°": 202,
100+
"±": 112,
101+
"²": 211,
102+
"³": 90,
103+
"·": 101,
104+
"º": 210,
105+
"½": 119,
106+
"Ã": 89,
107+
"Æ": 209,
108+
"É": 124,
109+
"×": 190,
110+
"Ø": 204,
111+
"ß": 212,
112+
"à": 126,
113+
"á": 103,
114+
"â": 170,
115+
"ä": 213,
116+
"å": 205,
117+
"æ": 173,
118+
"ç": 105,
119+
"è": 169,
120+
"é": 81,
121+
"ë": 215,
122+
"í": 110,
123+
"î": 216,
124+
"ñ": 113,
125+
"ò": 114,
126+
"ó": 111,
127+
"ô": 125,
128+
"ö": 217,
129+
"ø": 206,
130+
"ú": 147,
131+
"Ā": 195,
132+
"ā": 133,
133+
"č": 174,
134+
"ę": 198,
135+
"ī": 135,
136+
"Ł": 196,
137+
"ł": 200,
138+
"Ō": 145,
139+
"ō": 136,
140+
"œ": 214,
141+
"ś": 201,
142+
"ū": 146,
143+
"ź": 197,
144+
"ż": 199,
145+
"ɑ": 122,
146+
"ɔ": 168,
147+
"ə": 149,
148+
"ɛ": 115,
149+
"ʁ": 116,
150+
"ʃ": 121,
151+
"ʊ": 118,
152+
"ʒ": 148,
153+
"ʻ": 171,
154+
"ˈ": 117,
155+
"̃": 123,
156+
"Ε": 150,
157+
"Σ": 158,
158+
"έ": 153,
159+
"β": 154,
160+
"ε": 164,
161+
"ι": 155,
162+
"μ": 167,
163+
"ν": 162,
164+
"ο": 156,
165+
"ρ": 120,
166+
"ς": 157,
167+
"σ": 152,
168+
"υ": 166,
169+
"φ": 160,
170+
"ω": 159,
171+
"ό": 161,
172+
"ώ": 165,
173+
"ا": 129,
174+
"ح": 130,
175+
"د": 131,
176+
"ز": 127,
177+
"ص": 192,
178+
"ف": 193,
179+
"ن": 194,
180+
"ه": 128,
181+
"ي": 132,
182+
"Ḥ": 134,
183+
"Ἱ": 163,
184+
"ὐ": 151,
185+
"–": 108,
186+
"—": 84,
187+
"‘": 92,
188+
"’": 83,
189+
"“": 85,
190+
"”": 86,
191+
"•": 172,
192+
"…": 106,
193+
"€": 191,
194+
"☎": 203,
195+
"☏": 207,
196+
"✉": 208,
197+
"一": 139,
198+
"况": 182,
199+
"口": 138,
200+
"古": 187,
201+
"司": 176,
202+
"和": 183,
203+
"图": 178,
204+
"夏": 143,
205+
"奈": 141,
206+
"子": 144,
207+
"山": 180,
208+
"樋": 137,
209+
"津": 142,
210+
"清": 185,
211+
"澹": 188,
212+
"琴": 181,
213+
"空": 177,
214+
"葉": 140,
215+
"谿": 179,
216+
"远": 186,
217+
"静": 184
218+
}
245 Bytes
Binary file not shown.

harper-brill/finished_joint/suffix_vocab.json

Lines changed: 2005 additions & 1 deletion
Large diffs are not rendered by default.

harper-brill/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ const JOINT_ARCH: JointArch = JointArch {
2727
};
2828

2929
thread_local! {
30-
// burn's NdArray model is `!Sync`, so the runtime is a per-thread `Rc`
31-
// rather than a process-wide `Arc`. Inference is memoized inside the runtime.
30+
// The runtime is a per-thread `Rc` (not a process-wide `Arc`): burn CPU
31+
// models are not guaranteed `Sync`, and a per-thread instance also keeps the
32+
// memoization cache lock-free across threads. Inference is memoized inside.
3233
static JOINT: Rc<JointRuntime> = Rc::new(build_joint());
3334
}
3435

harper-pos-utils/Cargo.toml

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,31 @@ strum_macros = "0.28.0"
1414
serde = { version = "1.0.228", features = ["derive"] }
1515
is-macro = "0.3.7"
1616
rand = { version = "0.10.1", optional = true }
17-
burn = { version = "0.19.1", default-features = false, features = ["std"] }
18-
burn-ndarray = { version = "0.19.0", default-features = false }
17+
burn = { version = "0.21.0", default-features = false, features = ["std"] }
18+
burn-ndarray = { version = "0.21.0", default-features = false }
1919
serde_json = "1.0.150"
2020
smallvec = "1.15.1"
2121
lru = "0.18.0"
2222
rayon = { version = "1.12.0", optional = true }
2323

24-
# burn-ndarray's explicit SIMD kernels (~1.8x faster cold inference) require the
25-
# `simd` feature, which isn't no_std-clean in 0.19.1 and so also pulls in `std`.
26-
# Gate both to native targets; the wasm32 build keeps the no_std scalar path.
24+
# The joint tagger infers on burn-flex (0.21+) on every target: a fast pure-Rust
25+
# CPU backend (gemm + SIMD) — ~5.6x faster cold inference than burn-ndarray, at
26+
# byte-identical output. Native uses the default features (incl. rayon).
27+
# burn-ndarray remains only for the legacy `BurnChunker` (burn_chunker.rs) and
28+
# tests; its explicit SIMD kernels need the `simd` feature (not no_std-clean, so
29+
# it also pulls `std`), hence the native-only entry below.
2730
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
28-
burn-ndarray = { version = "0.19.0", default-features = false, features = ["simd", "std"] }
31+
burn-ndarray = { version = "0.21.0", default-features = false, features = ["simd", "std"] }
32+
burn-flex = { version = "0.21.0" }
33+
34+
# wasm32 has no threads (drop `rayon`) and no atomic CAS (flex requires the
35+
# `critical-section` shim there). SIMD uses wasm's simd128.
36+
[target.'cfg(target_arch = "wasm32")'.dependencies]
37+
burn-flex = { version = "0.21.0", default-features = false, features = ["std", "simd", "critical-section"] }
38+
# burn-flex's RNG init pulls getrandom 0.4, which needs its `wasm_js` backend
39+
# feature enabled to build for wasm32-unknown-unknown (mirrors harper-wasm's own
40+
# getrandom 0.3 wasm_js setup; the two major versions coexist independently).
41+
getrandom = { version = "0.4", default-features = false, features = ["wasm_js"] }
2942

3043
[features]
3144
default = []

harper-pos-utils/src/joint/char_vocab.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use crate::joint::{CHAR_PAD, CHAR_UNK};
44
use hashbrown::HashMap;
55
use serde::{Deserialize, Serialize};
6+
use std::collections::BTreeMap;
67

78
#[derive(Debug, Clone, Serialize, Deserialize)]
89
pub struct CharVocab {
@@ -53,7 +54,11 @@ impl CharVocab {
5354
}
5455

5556
pub fn to_json(&self) -> String {
56-
serde_json::to_string(&self.map).expect("serialize char vocab")
57+
// Ordered (BTreeMap by char) + pretty-printed so the committed artifact
58+
// has a stable, diff-friendly layout. The ids are the values; key order
59+
// is cosmetic and does not affect decoding.
60+
let ordered: BTreeMap<char, usize> = self.map.iter().map(|(&k, &v)| (k, v)).collect();
61+
serde_json::to_string_pretty(&ordered).expect("serialize char vocab")
5762
}
5863

5964
pub fn from_json(s: &str) -> Self {

0 commit comments

Comments
 (0)