coolblaze03 commited on
Commit
17f5a29
·
verified ·
1 Parent(s): 4b46b8f

Upload README.md with huggingface_hub

Browse files
Files changed (1) hide show
  1. README.md +81 -314
README.md CHANGED
@@ -17,97 +17,16 @@ tags:
17
 
18
  # PyBytecode v3 — 1.5B
19
 
20
- **A 1.5B open model that decompiles Python 3.12 bytecode back to source, and ships with a sound
21
- verifier that proves when an output is correct.**
22
 
23
- Hand it a disassembled code object, get Python back. Then run the verifier: it recompiles the
24
- prediction and compares the resulting code object against the target, byte for byte. If they
25
- match, the decompilation is **proven** correct — not scored, not ranked, proven.
26
-
27
- That verifier is the distinguishing property of this release. Most generative models ask you to
28
- trust an aggregate accuracy. This one lets you check the single file in front of you.
29
-
30
- ```
31
- verified = recompile(prediction) yields a code object byte-identical to the target's
32
- = PROVABLY correct. Sound: there is no such thing as a false "verified".
33
- unverified = UNKNOWN, not wrong. A correct decompilation that compiles differently
34
- (a `while` where the original had a `for`) does not certify.
35
- ```
36
-
37
- Three consequences worth being explicit about:
38
-
39
- - **Reported accuracy is a lower bound on correctness.** Everything counted here is proven; some
40
- of what is not counted is right too.
41
- - **Best-of-N buys real accuracy, not a better guess.** Sample until something certifies, then
42
- stop. The verifier decides when to stop, so extra budget cannot mislead you.
43
- - **The check is exact.** Comparison runs over the real code object, recursively, including
44
- docstrings and `co_exceptiontable` — not over a textual disassembly. That is not decoration: an
45
- earlier representation omitted the exception table's `end`, which made a `try:` body and a
46
- `try/else:` body indistinguishable, and it once certified a wrong prediction.
47
 
48
  Weights are Apache-2.0. A GGUF build ships alongside for llama.cpp / LM Studio / Ollama.
49
 
50
- ---
51
-
52
- ## How well it works
53
-
54
- Strict L1 oracle throughout. Every figure carries a 95% interval and names the set it was measured
55
- on.
56
-
57
- ### On the published benchmark — `csn-3.12-licensed`, n=600 across 117 repositories
58
-
59
- | | certified | 95% CI |
60
- |---|---|---|
61
- | **PyBytecode v3, greedy** | **506 / 600 = 84.33%** | [80.48, 87.94] |
62
- | **PyBytecode v3, verified best-of-32** | **562 / 600 = 93.67%** | [90.86, 96.08] |
63
- | Untuned `Qwen2.5-Coder-1.5B-Instruct`, greedy | **4 / 600 = 0.67%** | [0.16, 1.35] |
64
-
65
- Intervals are **repo-clustered** (bootstrap over repositories, 10,000 resamples), because rows
66
- from one repository share an author and a house style and are not independent. Design effects
67
- 1.65 / 1.79 / 0.84.
68
-
69
- The benchmark ships with the model, licence-resolved per row at the exact commit, so these numbers
70
- are recomputable from files in this repository rather than taken on faith.
71
-
72
- ### The untuned base scores ~0, which is what makes the benchmark trustworthy
73
-
74
- The same base model, given the same prompt, the same decoding budget and the same oracle, certifies
75
- **4 of 600** here and **0 of 400** on our earlier benchmark. Under looser oracles it is clearer
76
- still: the base produces **syntactically valid Python on 52% of rows** and reaches AST-identity on
77
- **0%**. It paraphrases the disassembly into plausible-looking code with invented helpers rather
78
- than inverting it.
79
-
80
- A strong general code model cannot guess its way to a single point on this task. The benchmark is
81
- not solvable by pattern-matching, and everything v3 scores was learned from the fine-tune.
82
-
83
- ### The benchmark rebuild reproduced the result
84
-
85
- The published 600-row set was built from scratch: different rows (only 85 of 600 appear in the
86
- earlier 400-row set), 117 repositories instead of 24, and a 1% per-repo cap replacing a 15%
87
- concentration. It lands in the same place — certified@32 **93.67%** against **93.25%**, greedy
88
- **84.33%** against **83.75%**. The difference of **+0.42pp** carries a 95% interval of
89
- [−4.38, +5.22], so it is well inside noise, and the two size profiles are near-identical (median
90
- 59 vs 62 rep lines; 93.3% vs 94.0% of rows under 200), which is the composition factor that would
91
- otherwise move the number. Standardised to the older set's size mix the new set reads 94.48%.
92
- A result that survives a rebuild of the set it was measured on is worth more than the same result
93
- quoted twice.
94
-
95
- ### Which numbers came from where
96
-
97
- | Set | Published? | Why |
98
- |---|---|---|
99
- | `csn-3.12-licensed` (600) | **yes** | every row licence-resolved at its commit |
100
- | `pybytecode-mbpp-3.12` (383) | **yes** | MBPP, CC-BY-4.0 |
101
- | CSN-3.12 (400) | no | 45% of its rows are not redistributable |
102
- | Held-out (279) | no | per-row attribution was not retained |
103
-
104
- Our own results are measured on both the published set and the earlier pair. The figures in
105
- *Comparison with other systems* exist only on the earlier pair, because that is where those runs
106
- were done; they are labelled there because you cannot re-run them from what we shipped.
107
-
108
- ---
109
-
110
- ## How to use it well
111
 
112
  ```python
113
  from transformers import AutoModelForCausalLM, AutoTokenizer
@@ -128,7 +47,13 @@ prediction = tok.decode(model.generate(**batch, max_new_tokens=2048,
128
  skip_special_tokens=True)
129
  ```
130
 
131
- **Then certify it** — this is the step that matters:
 
 
 
 
 
 
132
 
133
  ```python
134
  from harness.pybytecode_core.verify import code_fingerprint
@@ -138,233 +63,82 @@ def certified(prediction: str, reference_code_object) -> bool:
138
  return code_fingerprint(got) == code_fingerprint(reference_code_object)
139
  ```
140
 
141
- Greedy decoding (temperature 0) for single-shot; temperature ~0.8 when sampling for best-of-N.
142
-
143
- ### Measure your input size first
 
144
 
145
- **This model works on function-sized units and degrades sharply past ~200 representation lines.**
146
- That is the single most useful thing to know before you start, and it costs one line to check:
147
 
148
- ```python
149
- from harness.pybytecode_core.rep import disassemble_v2
150
- rep_lines = disassemble_v2(code_object).count("\n") # the model's actual input length
151
- ```
152
 
153
- | your input | what to expect |
154
  |---|---|
155
- | **under 100 rep lines** | the model's home ground — ~89–96% greedy, ~97–99% at best-of-32 |
156
- | **100–200** | still strong, but sampling starts to earn its cost |
157
- | **200–300** | roughly a coin flip greedy; best-of-32 recovers much of it |
158
- | **over ~300** | greedy rarely certifies; best-of-32 recovers some. Expect misses |
159
- | **over ~400** | we certified nothing here, greedy **or** at 32 samples |
160
-
161
- Above the knee a symbolic decompiler is the better tool, and the measurements are in *Comparison
162
- with other systems* below.
163
-
164
- The full curve, pooled over both earlier benchmarks (n=679) so the thin upper buckets carry as
165
- many rows as we can give them:
166
-
167
- | rep lines | rows | v3 greedy | v3 best-of-32 | untuned base | PyLingual k=32 |
168
- |---|---|---|---|---|---|
169
- | 0–49 | 311 | 94.21% | 98.71% | 0.96% | 99.04% |
170
- | 50–99 | 229 | 88.65% | 96.94% | 0.00% | 95.63% |
171
- | 100–199 | 105 | 78.10% | 88.57% | 0.00% | 84.76% |
172
- | 200–299 | 19 | 47.37% | 84.21% | 0.00% | 84.21% |
173
- | 300–399 | 11 | 18.18% | 54.55% | 0.00% | 81.82% |
174
- | 400–599 | 2 | 0.00% | 50.00% | 0.00% | 100.00% |
175
- | 600+ | 2 | 0.00% | 0.00% | 0.00% | 50.00% |
176
-
177
- - **The greedy knee is at ~200 rep lines**: 78.1% → 47.4% → 18.2% → 0% across four consecutive
178
- buckets.
179
- - **Best-of-32 postpones the knee to ~300; it does not remove it.** Sampling 32 times buys roughly
180
- one bucket of headroom.
181
- - Almost every point we score comes from small units. On the earlier CSN-3.12 set (n=400),
182
- **97.61%** of greedy certifications and **95.98%** of best-of-32 certifications are units under
183
- 200 rep lines; on the published 600-row set it is **96.64%** of greedy certifications. The
184
- headline accuracy is a statement about small units.
185
- - The top two buckets are n=2 each and carry nothing on their own. What carries the conclusion is
186
- the monotone decline through n=311/229/105/19/11 below them.
187
-
188
- The same stratification on the **published** 600-row benchmark — recomputable from the files in
189
- this repo with `harness/size_curve.py` — reproduces the shape:
190
-
191
- | rep lines | rows | v3 greedy | 95% CI | v3 best-of-32 | 95% CI |
192
- |---|---|---|---|---|---|
193
- | 0–49 | 224 | 95.98% | [93.01, 98.51] | 98.21% | [96.31, 99.58] |
194
- | 50–99 | 224 | 89.73% | [85.17, 93.93] | 98.21% | [96.26, 99.57] |
195
- | 100–199 | 112 | 65.18% | [55.36, 74.14] | 85.71% | [77.57, 92.98] |
196
- | 200–299 | 27 | 51.85% | — | 77.78% | — |
197
- | 300–399 | 5 | 60.00% | — | 100.00% | — |
198
- | 400–599 | 5 | 0.00% | — | 0.00% | — |
199
- | 600+ | 3 | 0.00% | — | 0.00% | — |
200
-
201
- Intervals are repo-clustered, and omitted below 30 rows / 10 repositories rather than printed at a
202
- width that would imply precision we do not have. The 300–399 bucket reading above the one below it
203
- is n=5 noise, not a recovery. Median input in this benchmark is **59** rep lines; p90 is 167, max
204
- 1,622.
205
-
206
- On this set the top two buckets stay at **0% even with 32 samples** — above ~400 rep lines,
207
- sampling did not rescue a single one of the 8 rows. **96.64%** of greedy certifications and
208
- **95.37%** of best-of-32 certifications come from units under 200 rep lines.
209
-
210
- ### Decide how much budget to spend
211
-
212
- The best-of-32 budget is **1 greedy decode + 31 sampled candidates at temperature 0.8**, with
213
- verified early stop — sampling halts on the first candidate that certifies, so the mean cost is
214
- far below 32. Across the 94 greedy failures it took a mean of 15.16 samples and recovered 56:
215
-
216
- | budget | certified | |
217
- |---|---|---|
218
- | @1 (greedy alone) | 506 / 600 | 84.33% |
219
- | @2 | 525 / 600 | 87.50% |
220
- | @4 | 546 / 600 | 91.00% |
221
- | @8 | 555 / 600 | 92.50% |
222
- | @16 | 558 / 600 | 93.00% |
223
- | @32 | 562 / 600 | 93.67% |
224
-
225
- Most of the recovery arrives in the first few samples: @4 already captures two thirds of what @32
226
- gets. Because early stop depends only on the index of the first passing sample, this curve is
227
- exact rather than an estimate — the same cached generations give every point.
228
-
229
- ---
230
-
231
- ## What this model does not do
232
-
233
- - **Large units — this is the measured limit.** Certification falls from 94% under 50
234
- representation lines to 18% at 300–399 and 0% above 400. See *Measure your input size*: the
235
- curve is the honest specification of what this model does. It was trained on functions; a whole
236
- module is a different regime in both size and shape, and we have not evaluated one.
237
- - **Python 3.12 only.** A scope statement, not the explanation for the size behaviour above:
238
- trained and measured on 3.12, and the oracle refuses cross-minor input by design. Every number
239
- on this card is in-version, so nothing here is contaminated by a version mismatch.
240
- - **It did not decompile real malware.** On the one wild PyInstaller-packed sample in scope, the
241
- entry-point module produced **nothing certifiable** — the prediction did not even compile. 3 of
242
- 9 units in that sample certified, and those three were PyInstaller's own bootstrap plus a
243
- bundled stdlib module. Extraction and representation work (1,510/1,510 code objects recovered);
244
- end-to-end decompilation of real malware logic is **not demonstrated**.
245
-
246
- That entry point was both 3.13 *and* 491 representation lines, so **size and version are
247
- confounded in it and it is evidence for neither** on its own. It is reported because you should
248
- know the result, not because it diagnoses anything. The benign in-version curve above is the
249
- instrument, and it already shows the model failing at that size on clean 3.12 input.
250
- - **Annotated functions** are handicapped: the training corpus was compiled with PEP-563
251
- stringised annotations inherited, real `.pyc` files are not, and the benchmarks deliberately do
252
- not hand the model its training-time distribution back.
253
-
254
- ## The oracle's limits, where you will meet them
255
-
256
- Full detail in [`ORACLE-LIMITS.md`](ORACLE-LIMITS.md).
257
-
258
- - **0.33% false-reject floor on foreign `.pyc`.** On 600 wild install-time `.pyc`, 598 certify;
259
- 2 do not, because CPython 3.12.3 and 3.12.13 generate different code for the same source. No
260
- normalisation fixes that without abandoning the byte-identical guarantee. It fails to a **false
261
- reject** — you are told "unknown" about a correct answer, never "verified" about a wrong one.
262
- - **Optimization level must match the producer's.** A `.pyc` built with `-O` is a different code
263
- object. The wrong level does not degrade gracefully, it collapses to ~24%. Try all three; it
264
- costs three compiles. But note that at `optimize>=1` docstrings are absent from the `.pyc`, so
265
- **docstring recovery becomes unprovable** against such a file — a limit of the artifact, not of
266
- the oracle.
267
- - **The 100% pre-flight the harness prints proves nothing about soundness.** It grades each
268
- reference against itself, i.e. compares `compile(x)` with `compile(x)`, so any deterministic
269
- function of the source scores 100% — including a stub that ignores the bytecode entirely. It
270
- detects a broken harness and nothing more. The soundness evidence is the mutation test (0 true
271
- survivors in 1,239 mutants) and 18 targeted blind-spot probes (18/18).
272
- - **Untested:** cross-minor 3.13, Nuitka, non-CPython builds, obfuscated bytecode.
273
-
274
- ## Reproduce every number on this card
275
-
276
- No model, no GPU, no network, no third-party decompiler — CPython 3.12 standard library only:
277
-
278
- ```bash
279
- cd harness
280
- python3 grade.py --bench ../benchmarks/csn-3.12-licensed/bench.jsonl --self-test-only --out /tmp/st.json
281
- python3 analyze_scores.py --bench ../benchmarks/csn-3.12-licensed/bench.jsonl \
282
- --greedy ../generations/gen_v3_csn600.jsonl \
283
- --base ../generations/gen_base_csn600.jsonl \
284
- --out /tmp/scores.json --rows-out /tmp/rows.jsonl
285
- python3 size_curve.py --bench ../benchmarks/csn-3.12-licensed/bench.jsonl \
286
- --greedy ../generations/gen_v3_csn600.jsonl \
287
- --base ../generations/gen_base_csn600.jsonl \
288
- --out /tmp/size_curve.json
289
- ```
290
-
291
- Per-row verdicts for all 600 rows — repo, function, commit SHA, SPDX, tuned verdict, base verdict
292
- — land in `rows.jsonl`. Every aggregate above is recomputable from that file.
293
-
294
- ## Comparison with other systems
295
 
296
- [PyLingual](https://github.com/syssec-utd/pylingual) is another system that performs this task, by
297
- symbolic reconstruction rather than generation. We ran it on the same sets, under the same oracle,
298
- at a matched budget.
299
 
300
- | CSN-3.12, n=400, 24 repos | certified | 95% CI (repo-clustered) |
301
- |---|---|---|
302
- | untuned base, greedy | 0 / 400 = 0.00% | — |
303
- | v3 greedy | 335 / 400 = 83.75% | [77.84, 88.89] |
304
- | v3 certified@32 | 373 / 400 = 93.25% | [88.82, 96.89] |
305
- | PyLingual k=32 | 377 / 400 = 94.25% | [91.06, 96.80] |
306
- | **union** | **393 / 400 = 98.25%** | [96.81, 99.51] |
307
 
308
- | Held-out, n=279 | certified | 95% CI (Wilson) |
309
- |---|---|---|
310
- | untuned base, greedy | 3 / 279 = 1.08% | [0.37, 3.11] |
311
- | v3 greedy | 254 / 279 = 91.04% | [87.11, 93.86] |
312
- | v3 certified@32 | 272 / 279 = 97.49% | [94.91, 98.78] |
313
- | PyLingual k=32 | 267 / 279 = 95.70% | [92.63, 97.52] |
314
- | **union** | **275 / 279 = 98.57%** | [96.37, 99.44] |
315
 
316
- **Neither difference is statistically significant** (exact paired McNemar, p = 0.6177 on CSN and
317
- p = 0.2266 on held-out). We claim no accuracy advantage, and none is claimed against us. On cost
318
- the comparison runs against us: ~1.5B parameters against their ~320M, and slower wall-clock.
319
 
320
- Docstring recovery on the 115 held-out rows carrying a real docstring: **115 / 115 = 100%**
321
- (PyLingual: 113/115 = 98.26%, under an oracle that does not score docstrings at all).
322
 
323
- The held-out interval is **Wilson, not clustered**: every row of that set carries the same
324
- placeholder repository value, so its clustering cannot be assessed. The CSN design effects of
325
- 2.34–2.69 show why that matters — on a set where one repository supplied 15% of rows, a naive
326
- interval would have been far too narrow.
327
-
328
- These figures were measured on our earlier benchmark pair, which is **not** published. PyLingual
329
- has not been run on the 600-row licensed benchmark released alongside this model.
330
-
331
- ### The two approaches fail on different inputs
332
-
333
- The more interesting measured result is not which number is larger, but that the two systems lose
334
- on different inputs, along an axis that turns out to be **unit size**:
335
-
336
- | rep lines | rows | PyBytecode greedy | PyBytecode @32 | PyLingual k=32 |
337
- |---|---|---|---|---|
338
- | 100–199 | 105 | 78.10% | 88.57% | 84.76% |
339
- | 200–299 | 19 | 47.37% | 84.21% | 84.21% |
340
- | 300–399 | 11 | **18.18%** | 54.55% | **81.82%** |
341
- | 400–599 | 2 | **0.00%** | 50.00% | **100.0%** |
342
-
343
- A generative model degrades as its input grows; a symbolic reconstruction does not. Two things
344
- follow, both useful:
345
-
346
- - **Large units are not intrinsically unsolvable.** A symbolic system handles them at 81.82% where
347
- we score 18.18%. What our curve measures is a capability limit of a 1.5B model, not a property
348
- of the task — which is a statement about where to spend the next round of scale.
349
- - **The union exceeds either system**, 98.25% and 98.57% above. Because verification is sound, the
350
- union costs nothing to take: run either, keep the answer that certifies, fall through to the
351
- other. Neither system can certify a wrong answer, so combining them cannot mislead you.
352
-
353
- ## Training
354
 
355
- | | |
356
- |---|---|
357
- | Base | [`Qwen/Qwen2.5-Coder-1.5B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct) (Apache-2.0) |
358
- | Method | LoRA r=16, α=32, all attention + MLP projections, 1 epoch, lr 2e-4, merged into the base |
359
- | Rows | 48,196 pairs of Python 3.12 disassembly → source |
360
- | Corpus | `codeparrot/github-code-clean`, per-row filtered to 7 permissive licences; all GPL/LGPL/AGPL/MPL/EPL dropped before extraction |
361
- | Decontamination | shard-disjoint, repo-disjoint, and identifier-blind fingerprint-disjoint |
362
-
363
- **The training corpus is not distributed.** Per-row attribution was not retained during
364
- extraction, so the corpus cannot be redistributed without dropping required MIT/BSD/Apache
365
- notices. That is a property of the artifact, not of the licences — nothing in it is unlicensed or
366
- reciprocally licensed. Full lineage, including what was dropped and why:
367
- [`DATA-CARD-training-corpus.md`](DATA-CARD-training-corpus.md).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
  ## Licence
370
 
@@ -372,18 +146,11 @@ reciprocally licensed. Full lineage, including what was dropped and why:
372
 
373
  Derived from `Qwen/Qwen2.5-Coder-1.5B-Instruct`, which is Apache-2.0. Under Apache-2.0 §4 we ship
374
  the licence, retain attribution, and state our changes (LoRA fine-tune, adapter merged; no
375
- architecture, vocabulary or tokenizer change). The upstream repository ships a `LICENSE` and no
376
- `NOTICE`, so there is no upstream notice text to carry forward. If you redistribute these weights
377
- or build derivatives, the same four obligations pass to you.
378
-
379
- The corpus constraint above binds the *corpus*, not these weights: trained parameters are not a
380
- copy of any source text, and the corpus was filtered to permissive licences before training, so no
381
- reciprocal terms exist upstream to propagate.
382
 
383
  Decompilation has obvious dual use. Apache-2.0 imposes no field-of-use restriction and we have not
384
- added one: a restrictive weights licence would not prevent misuse and would block the
385
- interoperability, incident-response and recovery work this is for. Complying with the law where
386
- you operate is your responsibility.
387
 
388
  ## Citation
389
 
 
17
 
18
  # PyBytecode v3 — 1.5B
19
 
20
+ Turns Python 3.12 bytecode back into Python source. Hand it a disassembled code object, get source
21
+ code back.
22
 
23
+ The unusual part: **you can check every answer.** Recompile what the model wrote and compare it
24
+ against the bytecode you started with — if they match, that file is exactly right, and you know it
25
+ without trusting an accuracy number.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  Weights are Apache-2.0. A GGUF build ships alongside for llama.cpp / LM Studio / Ollama.
28
 
29
+ ## Quickstart
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  ```python
32
  from transformers import AutoModelForCausalLM, AutoTokenizer
 
47
  skip_special_tokens=True)
48
  ```
49
 
50
+ Greedy decoding (temperature 0) for a single shot; temperature ~0.8 when you sample several
51
+ candidates.
52
+
53
+ ## Checking the answer
54
+
55
+ Compile the model's output and compare the resulting code object to the one you were decompiling.
56
+ Same code object means same behaviour, so a match tells you *this* answer is correct:
57
 
58
  ```python
59
  from harness.pybytecode_core.verify import code_fingerprint
 
63
  return code_fingerprint(got) == code_fingerprint(reference_code_object)
64
  ```
65
 
66
+ Two things follow. A failed check means "not confirmed", not "wrong" — a correct rewrite that
67
+ compiles differently (a `while` where the original had a `for`) won't match, so the accuracy
68
+ figures below are a floor, not an estimate. And because the check is cheap and reliable, sampling
69
+ several answers and keeping the first one that passes is a real gain rather than a nicer guess.
70
 
71
+ ## Results
 
72
 
73
+ On the benchmark published with this model:
 
 
 
74
 
75
+ | | certified |
76
  |---|---|
77
+ | **PyBytecode v3, one attempt** | **506 / 600 = 84.33%** |
78
+ | **PyBytecode v3, up to 32 tries** | **562 / 600 = 93.67%** |
79
+ | `Qwen2.5-Coder-1.5B-Instruct` before fine-tuning, one attempt | 4 / 600 = 0.67% |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ The benchmark is `csn-3.12-licensed`: 600 real functions from 117 GitHub repositories, compiled to
82
+ 3.12 bytecode, shipped with the model. The base model before fine-tuning gets essentially none of
83
+ them, so this is not something a general code model can guess its way through.
84
 
85
+ Full numbers, confidence intervals, method, per-budget curve and the comparison with other
86
+ systems: [`EVAL.md`](EVAL.md).
 
 
 
 
 
87
 
88
+ [PyLingual](https://github.com/syssec-utd/pylingual) is another system that does this task, by
89
+ symbolic reconstruction rather than generation. On our earlier benchmarks it scores about the same
90
+ as we do, and the two miss on different inputs — so running both and keeping whichever answer
91
+ passes the check gets you more than either alone. Numbers in [`EVAL.md`](EVAL.md).
 
 
 
92
 
93
+ ## When it works well, and when it doesn't
 
 
94
 
95
+ **It is good on individual functions and gets much worse on long ones.** Size is measured in
96
+ *disassembly lines* — how long the input you hand the model is. One line tells you:
97
 
98
+ ```python
99
+ from harness.pybytecode_core.rep import disassemble_v2
100
+ rep_lines = disassemble_v2(code_object).count("\n")
101
+ ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ | disassembly lines | rows | one attempt | up to 32 tries |
104
+ |---|---|---|---|
105
+ | under 100 | 448 | 92.86% | 98.21% |
106
+ | 100–199 | 112 | 65.18% | 85.71% |
107
+ | 200–399 | 32 | 53.12% | 81.25% |
108
+ | 400+ | 8 | 0.00% | 0.00% |
109
+
110
+ Below ~100 lines it is on home ground. Accuracy starts dropping around 200, and above ~400 lines
111
+ nothing certified at all, even with 32 tries. Sampling more buys roughly one bucket of headroom;
112
+ it does not remove the limit. For big units, a symbolic decompiler is the better tool. The full
113
+ seven-bucket curve is in [`EVAL.md`](EVAL.md).
114
+
115
+ ## Limits
116
+
117
+ - **Long inputs.** The table above is the honest specification: trained on functions, not modules,
118
+ and it fails above ~400 disassembly lines.
119
+ - **Python 3.12 only.** Trained and measured on 3.12; the checker refuses other minor versions by
120
+ design.
121
+ - **If the `.pyc` was built with `-O`, compile at the same level or the check will not match.**
122
+ Wrong level collapses to ~24%, so try all three — it costs three compiles. At `-O` and above,
123
+ docstrings aren't in the `.pyc` at all, so docstring recovery can't be confirmed against one.
124
+ - **A `.pyc` built by someone else can fail the check even when the answer is right** — about
125
+ 0.33% of the time, because CPython patch releases compile the same source differently. It always
126
+ fails in the safe direction: "unknown" about a correct answer, never "confirmed" about a wrong
127
+ one.
128
+ - **It has not been shown to work on real malware.** On the one packed sample we tried, the
129
+ entry-point module produced nothing certifiable. Extraction worked; decompiling the actual
130
+ malware logic did not.
131
+ - Untested: Python 3.13, Nuitka, non-CPython builds, obfuscated bytecode.
132
+
133
+ Details on all of these in [`EVAL.md`](EVAL.md) and [`ORACLE-LIMITS.md`](ORACLE-LIMITS.md).
134
+
135
+ ## Model details
136
+
137
+ Fine-tuned from [`Qwen/Qwen2.5-Coder-1.5B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct)
138
+ (Apache-2.0) with LoRA on 48,196 pairs of Python 3.12 disassembly → source, adapter merged. The
139
+ corpus was filtered to permissive licences before training and is not redistributed — per-row
140
+ attribution was not retained, so shipping it would strip required notices. Lineage:
141
+ [`DATA-CARD-training-corpus.md`](DATA-CARD-training-corpus.md). Training settings: [`EVAL.md`](EVAL.md).
142
 
143
  ## Licence
144
 
 
146
 
147
  Derived from `Qwen/Qwen2.5-Coder-1.5B-Instruct`, which is Apache-2.0. Under Apache-2.0 §4 we ship
148
  the licence, retain attribution, and state our changes (LoRA fine-tune, adapter merged; no
149
+ architecture, vocabulary or tokenizer change). The same obligations pass to you if you
150
+ redistribute these weights or build derivatives.
 
 
 
 
 
151
 
152
  Decompilation has obvious dual use. Apache-2.0 imposes no field-of-use restriction and we have not
153
+ added one. Complying with the law where you operate is your responsibility.
 
 
154
 
155
  ## Citation
156