tinydistill 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. tinydistill-0.2.0/.gitignore +8 -0
  2. tinydistill-0.2.0/LICENSE +21 -0
  3. tinydistill-0.2.0/PKG-INFO +294 -0
  4. tinydistill-0.2.0/README.md +264 -0
  5. tinydistill-0.2.0/examples/banking77/results-7b.json +84 -0
  6. tinydistill-0.2.0/examples/banking77/results-7b.md +9 -0
  7. tinydistill-0.2.0/examples/banking77/results-k8-7b.json +92 -0
  8. tinydistill-0.2.0/examples/banking77/results-k8-7b.md +10 -0
  9. tinydistill-0.2.0/examples/banking77/results-k8.json +103 -0
  10. tinydistill-0.2.0/examples/banking77/results-k8.md +11 -0
  11. tinydistill-0.2.0/examples/banking77/results.json +95 -0
  12. tinydistill-0.2.0/examples/banking77/results.md +10 -0
  13. tinydistill-0.2.0/examples/banking77/run.sh +22 -0
  14. tinydistill-0.2.0/examples/banking77/task.yaml +92 -0
  15. tinydistill-0.2.0/examples/financial-tweets/results.json +103 -0
  16. tinydistill-0.2.0/examples/financial-tweets/results.md +11 -0
  17. tinydistill-0.2.0/examples/financial-tweets/run.sh +17 -0
  18. tinydistill-0.2.0/examples/financial-tweets/task.yaml +21 -0
  19. tinydistill-0.2.0/examples/update_readme.py +20 -0
  20. tinydistill-0.2.0/pyproject.toml +50 -0
  21. tinydistill-0.2.0/src/tinydistill/__init__.py +6 -0
  22. tinydistill-0.2.0/src/tinydistill/cli.py +459 -0
  23. tinydistill-0.2.0/src/tinydistill/data.py +92 -0
  24. tinydistill-0.2.0/src/tinydistill/evaluate.py +140 -0
  25. tinydistill-0.2.0/src/tinydistill/export.py +103 -0
  26. tinydistill-0.2.0/src/tinydistill/student.py +140 -0
  27. tinydistill-0.2.0/src/tinydistill/task.py +108 -0
  28. tinydistill-0.2.0/src/tinydistill/teacher.py +272 -0
  29. tinydistill-0.2.0/tests/conftest.py +24 -0
  30. tinydistill-0.2.0/tests/test_cache.py +38 -0
  31. tinydistill-0.2.0/tests/test_cli.py +149 -0
  32. tinydistill-0.2.0/tests/test_export.py +34 -0
  33. tinydistill-0.2.0/tests/test_loss.py +53 -0
  34. tinydistill-0.2.0/tests/test_scoring.py +186 -0
  35. tinydistill-0.2.0/tests/test_task.py +78 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ .venv-gpu/
3
+ runs/
4
+ dist/
5
+ __pycache__/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andreas-Hum
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,294 @@
1
+ Metadata-Version: 2.5
2
+ Name: tinydistill
3
+ Version: 0.2.0
4
+ Summary: Distil an LLM classification prompt into a small, fast model that runs on a CPU.
5
+ Project-URL: Homepage, https://github.com/Andreas-Hum/tinydistill
6
+ Author: Andreas-Hum
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: inference,knowledge distillation,llm,onnx,text classification
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: onnx>=1.15
15
+ Requires-Dist: onnxruntime>=1.17
16
+ Requires-Dist: pyyaml>=6
17
+ Requires-Dist: torch>=2.2
18
+ Requires-Dist: transformers>=4.56
19
+ Provides-Extra: 4bit
20
+ Requires-Dist: accelerate>=0.30; extra == '4bit'
21
+ Requires-Dist: bitsandbytes>=0.43; extra == '4bit'
22
+ Provides-Extra: dev
23
+ Requires-Dist: build>=1.2; extra == 'dev'
24
+ Requires-Dist: datasets>=2.18; extra == 'dev'
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Requires-Dist: ruff>=0.5; extra == 'dev'
27
+ Provides-Extra: hf
28
+ Requires-Dist: datasets>=2.18; extra == 'hf'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # tinydistill
32
+
33
+ **Most of a large model's quality on one task, many times faster, on a CPU.**
34
+
35
+ `tinydistill` turns an LLM classification prompt into a small encoder classifier by knowledge distillation.
36
+ You describe the task in a short YAML file (labels, how to say each label, an instruction prompt) and give it
37
+ unlabelled texts. An instruction-tuned LLM labels them with soft targets, a small encoder learns to imitate
38
+ those targets, and the result is exported to ONNX (optionally int8) for fast CPU inference.
39
+
40
+ It continues my bachelor's project on inference optimisation by distilling LLMs.
41
+
42
+ ## Results
43
+
44
+ Two tasks with `Qwen/Qwen2.5-1.5B-Instruct` as the zero-shot teacher, and Banking77 also with
45
+ `Qwen/Qwen2.5-7B-Instruct`; the student is always `sentence-transformers/all-MiniLM-L6-v2`. Quality is measured on gold-labelled test texts that
46
+ neither model trains on. Every table is written by the task's `run.sh` from its saved `results*.json` file in
47
+ [`examples/`](https://github.com/Andreas-Hum/tinydistill/tree/main/examples); no number is typed by hand. The teacher's latency includes scoring every surface form
48
+ of every label, which costs most on Banking77 with its many labels, so there the GPU teacher row is the fairer
49
+ reference for speed.
50
+
51
+ ### Banking77: the hard case
52
+
53
+ Intent detection with 77 fine-grained classes ([Banking77](https://huggingface.co/datasets/PolyAI/banking77),
54
+ CC BY 4.0), reproduced by [`examples/banking77/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/banking77/run.sh).
55
+
56
+ Teacher only, no gold labels:
57
+
58
+
59
+ <!-- results: banking77/results.md -->
60
+ | Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
61
+ |---|---|---|---|---|---|---|---|---|---|
62
+ | teacher, zero-shot | cpu | 0.406 | 0.367 | n/a | 19955.9 | n/a | 0.1 | 6175 | 1.0x |
63
+ | teacher, zero-shot | cuda | 0.406 | 0.367 | n/a | 60.3 | n/a | 16.6 | 3087 | 331.1x |
64
+ | student, PyTorch | cpu | 0.386 | 0.315 | 0.772 | 3.9 | 78.3 | 408.9 | 91 | 5124.6x |
65
+ | student, ONNX fp32 | cpu | 0.386 | 0.315 | 0.772 | 2.6 | 82.4 | 388.4 | 91 | 7606.9x |
66
+ | student, ONNX int8 | cpu | 0.383 | 0.311 | 0.761 | 1.0 | 34.4 | 931.1 | 23 | 20502.9x |
67
+
68
+ Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 10003 unlabelled training texts.
69
+ CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
70
+ <!-- /results -->
71
+
72
+ With 8 gold labels per class, and the student trained on all gold labels as an upper bound:
73
+
74
+ <!-- results: banking77/results-k8.md -->
75
+ | Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
76
+ |---|---|---|---|---|---|---|---|---|---|
77
+ | teacher, zero-shot | cpu | 0.406 | 0.367 | n/a | 19699.4 | n/a | 0.1 | 6175 | 1.0x |
78
+ | teacher, zero-shot | cuda | 0.406 | 0.367 | n/a | 58.3 | n/a | 17.2 | 3087 | 338.1x |
79
+ | student, PyTorch | cpu | 0.404 | 0.324 | 0.699 | 3.9 | 76.7 | 417.4 | 91 | 5056.3x |
80
+ | upper bound: student on all gold labels | cpu | 0.911 | 0.909 | 0.412 | n/a | n/a | n/a | n/a | n/a |
81
+ | student, ONNX fp32 | cpu | 0.404 | 0.324 | 0.699 | 2.7 | 72.1 | 443.6 | 91 | 7306.9x |
82
+ | student, ONNX int8 | cpu | 0.398 | 0.318 | 0.697 | 1.0 | 36.7 | 872.1 | 23 | 18792.2x |
83
+
84
+ Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns from the teacher's soft labels on all 10003 training texts, plus cross-entropy on 8 gold labels per class (616 texts, weight 1.0). The upper bound is the same student trained with cross-entropy on all 10003 gold training labels and no teacher; its speed equals the student's.
85
+ CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
86
+ <!-- /results -->
87
+
88
+ With a stronger teacher, `Qwen/Qwen2.5-7B-Instruct` loaded in 4 bit (timed on the GPU only: 4-bit loading needs
89
+ CUDA), teacher only:
90
+
91
+ <!-- results: banking77/results-7b.md -->
92
+ | Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
93
+ |---|---|---|---|---|---|---|---|---|---|
94
+ | teacher, zero-shot, 4-bit | cuda | 0.630 | 0.607 | n/a | 272.2 | n/a | 3.7 | 5443 | 1.0x |
95
+ | student, PyTorch | cpu | 0.623 | 0.587 | 0.827 | 5.5 | 93.0 | 344.2 | 91 | 49.4x |
96
+ | student, ONNX fp32 | cpu | 0.623 | 0.587 | 0.827 | 3.4 | 86.8 | 368.7 | 91 | 80.8x |
97
+ | student, ONNX int8 | cpu | 0.622 | 0.586 | 0.822 | 1.6 | 43.8 | 730.0 | 23 | 171.2x |
98
+
99
+ Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-7B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 10003 unlabelled training texts.
100
+ CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot, 4-bit on cuda.
101
+ <!-- /results -->
102
+
103
+ The same teacher with 8 gold labels per class and the upper bound:
104
+
105
+ <!-- results: banking77/results-k8-7b.md -->
106
+ | Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
107
+ |---|---|---|---|---|---|---|---|---|---|
108
+ | teacher, zero-shot, 4-bit | cuda | 0.630 | 0.607 | n/a | 295.4 | n/a | 3.4 | 5443 | 1.0x |
109
+ | student, PyTorch | cpu | 0.627 | 0.583 | 0.765 | 4.5 | 93.6 | 341.9 | 91 | 65.5x |
110
+ | upper bound: student on all gold labels | cpu | 0.911 | 0.909 | 0.631 | n/a | n/a | n/a | n/a | n/a |
111
+ | student, ONNX fp32 | cpu | 0.627 | 0.583 | 0.765 | 3.2 | 80.9 | 395.7 | 91 | 92.5x |
112
+ | student, ONNX int8 | cpu | 0.622 | 0.576 | 0.762 | 1.1 | 40.7 | 786.4 | 23 | 262.6x |
113
+
114
+ Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-7B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns from the teacher's soft labels on all 10003 training texts, plus cross-entropy on 8 gold labels per class (616 texts, weight 1.0). The upper bound is the same student trained with cross-entropy on all 10003 gold training labels and no teacher; its speed equals the student's.
115
+ CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot, 4-bit on cuda.
116
+ <!-- /results -->
117
+
118
+ ### Financial tweets: an easier task
119
+
120
+ Sentiment of financial news tweets, 3 classes
121
+ ([Twitter Financial News Sentiment](https://huggingface.co/datasets/zeroshot/twitter-financial-news-sentiment),
122
+ MIT licence per the dataset card), reproduced by
123
+ [`examples/financial-tweets/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/financial-tweets/run.sh). The validation split is the test set.
124
+
125
+ <!-- results: financial-tweets/results.md -->
126
+ | Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
127
+ |---|---|---|---|---|---|---|---|---|---|
128
+ | teacher, zero-shot | cpu | 0.535 | 0.557 | n/a | 976.3 | n/a | 1.0 | 6175 | 1.0x |
129
+ | teacher, zero-shot | cuda | 0.535 | 0.557 | n/a | 33.7 | n/a | 29.6 | 3087 | 28.9x |
130
+ | student, PyTorch | cpu | 0.517 | 0.542 | 0.815 | 4.5 | 145.4 | 220.1 | 91 | 219.3x |
131
+ | upper bound: student on all gold labels | cpu | 0.867 | 0.824 | 0.531 | n/a | n/a | n/a | n/a | n/a |
132
+ | student, ONNX fp32 | cpu | 0.517 | 0.542 | 0.815 | 3.2 | 148.5 | 215.4 | 91 | 303.9x |
133
+ | student, ONNX int8 | cpu | 0.513 | 0.538 | 0.810 | 1.3 | 71.5 | 447.7 | 23 | 754.7x |
134
+
135
+ Task `financial-tweets`, 2388 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 9543 unlabelled training texts. The upper bound is the same student trained with cross-entropy on all 9543 gold training labels and no teacher; its speed equals the student's.
136
+ CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
137
+ <!-- /results -->
138
+
139
+ ## Quickstart
140
+
141
+ ```sh
142
+ pip install "tinydistill[hf]"
143
+ curl -LO https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/examples/banking77/task.yaml
144
+ tinydistill run task.yaml --data hf:PolyAI/banking77:train --test hf:PolyAI/banking77:test \
145
+ --out runs/banking77 --int8
146
+ tinydistill predict runs/banking77/onnx "I still haven't received my new card"
147
+ ```
148
+
149
+ Labelling the full dataset with the default teacher wants a GPU. For a quick look on a CPU, add
150
+ `--teacher Qwen/Qwen2.5-0.5B-Instruct --limit 200 --test-limit 200`. `--limit` takes the first rows, and the
151
+ Banking77 splits are sorted by intent, so a limited run only sees a few intents.
152
+
153
+ `run` does every step. Each step also has its own command, and they all read and write the run directory:
154
+
155
+ | Command | What it does |
156
+ |---|---|
157
+ | `tinydistill label TASK.yaml --data D [--test T] --out RUN` | teacher scores every label of every text; cached, resumable |
158
+ | `tinydistill train RUN` | distils the student from the teacher's soft targets |
159
+ | `tinydistill export RUN [--int8]` | writes `RUN/onnx` (ONNX, tokenizer, labels) |
160
+ | `tinydistill eval RUN` | writes `RUN/results.md` and `RUN/results.json` |
161
+ | `tinydistill predict RUN/onnx TEXT... [--data FILE]` | classifies texts, prints JSON lines |
162
+
163
+ Data can be `.csv`, `.jsonl` or `hf:<dataset>[:<config>]:<split>` (needs the `hf` extra). Useful options:
164
+ `--teacher` (any Hugging Face causal LM), `--load-4bit` (bitsandbytes, CUDA only: `pip install "tinydistill[4bit]"`),
165
+ `--student` (any Hugging Face encoder), `--epochs`, `--lr`, `--device`, `--threads`, and `--cache-dir` (runs that
166
+ share it label each text only once). See `tinydistill <command> --help`.
167
+
168
+ **Few labels.** With `--gold-per-class K`, the student also sees the gold labels of K randomly chosen training
169
+ examples per class, through a cross-entropy term (weight `--ce-weight`, default 1) added to the distillation
170
+ loss. `--upper-bound` additionally trains the same student on all gold training labels without the teacher,
171
+ and the results table gets an upper-bound row: how far the distilled student is from fully supervised training.
172
+
173
+ From Python:
174
+
175
+ ```python
176
+ from tinydistill import Predictor
177
+
178
+ clf = Predictor("runs/banking77/onnx") # uses model.int8.onnx when present
179
+ clf.predict(["How do I top up my card?"]) # list of label names
180
+ clf.predict_proba(["How do I top up my card?"]) # numpy array, one row per text
181
+ ```
182
+
183
+ ### The task file
184
+
185
+ ```yaml
186
+ name: banking77
187
+ text_field: text # column holding the input text
188
+ label_field: label # column holding gold labels (ids, or integers indexing `labels`)
189
+ prompt: |-
190
+ You are classifying messages that customers sent to their bank's support chat.
191
+ Choose the single intent from this list that best matches the message:
192
+ {labels}
193
+
194
+ Message: {text}
195
+
196
+ Reply with the intent exactly as written in the list.
197
+ labels:
198
+ - activate_my_card # a bare id: its name is derived, "activate my card"
199
+ - card_arrival
200
+ # ...
201
+ ```
202
+
203
+ `{labels}` (required) becomes a bulleted list of the label names, and `{text}` is the input. The name is what
204
+ the teacher scores. Each label can be a bare id as above, `id: name`, or carry a description that is shown in
205
+ the list (`- bullish: positive for the stock`) but not scored:
206
+
207
+ ```yaml
208
+ labels:
209
+ bearish:
210
+ description: negative for the stock, company or market
211
+ lost_card: lost or stolen card
212
+ ```
213
+
214
+ ## How the label scoring works
215
+
216
+ The teacher never generates free text. For an input x, the prompt is rendered with the model's chat template
217
+ (user message = the instruction with the label list, generation prompt for the assistant). A continuation is one
218
+ way of writing a label followed by the end-of-turn token, `c_1 ... c_m`, with log-probability
219
+
220
+ log p(c | prompt(x)) = sum_j log p(c_j | prompt(x), c_1 ... c_(j-1))
221
+
222
+ Multi-token labels are handled exactly, and the end-of-turn token keeps a label that is a prefix of another
223
+ (`card` versus `card arrival`) from winning by stopping early. Models do not always write a label the way it is
224
+ listed: Qwen2.5 tends to answer a sentiment question with "Bullish" even when the list says "bullish". So each
225
+ label is scored over its common surface forms, the name as written, lowercase, Capitalised and UPPERCASE, each
226
+ with and without a leading space (forms that tokenise identically count once):
227
+
228
+ score(label) = log sum over forms c of p(c | prompt(x))
229
+
230
+ This is verbaliser coverage: it only counts more ways of writing the same answer, and there is no calibration or
231
+ prior correction. All forms share the prefix tree below, but with many labels they still add up; a task file can
232
+ list fewer (`surface_forms: [lowercase, capitalised]` drops UPPERCASE, which tokenises into many pieces, and
233
+ `surface_forms: []` scores the name as written only). A softmax over the label scores gives the soft targets. The prompt lists each label in its
234
+ primary form, the name as written. The scores are cached as JSON lines keyed by a hash of the text, in a file
235
+ named after a fingerprint of the teacher, prompt and continuations, so an interrupted run resumes and a changed
236
+ prompt never reuses old scores.
237
+
238
+ To keep this fast, the part of the prompt before the text is run once and its key/value cache is kept. Each
239
+ text then needs a single forward pass that covers the text and all labels together: the tokens of all continuations
240
+ (every surface form of every label) form a prefix tree, so continuations that start alike, such as
241
+ `card arrival` and `card linking`, share nodes, and a 4D attention mask lets each tree node attend only to the
242
+ prompt, the text and its own ancestors. There is no padding and no per-label copy of the cache. Tests check the
243
+ scores against a plain forward pass per continuation.
244
+
245
+ The student minimises KL(teacher || student) over the training texts, plus `--ce-weight` times the
246
+ cross-entropy on the K gold labels per class chosen by `--gold-per-class`, if any (standard knowledge
247
+ distillation, Hinton et al., 2015).
248
+
249
+ ## Limitations
250
+
251
+ - **The teacher caps the student.** The student learns to imitate the teacher, mistakes included. A small LLM
252
+ choosing zero-shot among many fine-grained labels can be weak: Qwen2.5-0.5B-Instruct, for example, often
253
+ confuses related Banking77 intents. A bigger teacher (try `--load-4bit` with a 7B model) or a better prompt
254
+ usually helps more than anything on the student side.
255
+ - **Scores are sums, not averages.** Longer verbalisations get lower scores, so very uneven label lengths bias the
256
+ teacher. Keep verbalisations short and of similar length.
257
+ - **Only case and spacing variants are counted.** Plurals, synonyms or rephrasings of a label are not; keep label
258
+ names short and close to how the teacher would answer.
259
+ - **A too-small teacher can collapse.** Qwen2.5-0.5B-Instruct, for example, puts nearly every text into one or two
260
+ labels on the financial-tweets task, whatever the scoring; use 1.5B or larger.
261
+ - **Prompt and label order matter.** Zero-shot LLMs are sensitive to wording and to the order of the options.
262
+ - **Teacher cost grows with labels.** Every text is scored against every surface form of every label (shared
263
+ prefixes are computed once), one text at a time.
264
+ - **Single-label classification only.** Inputs are truncated to 128 tokens by default (`--max-length`).
265
+ - **Timings depend on the machine.** The results table states the threads and devices used; rerun `eval` on your
266
+ own hardware.
267
+ - ONNX export uses PyTorch's TorchScript-based exporter.
268
+
269
+ ## Development
270
+
271
+ ```sh
272
+ python -m venv .venv && . .venv/bin/activate
273
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
274
+ pip install -e ".[dev]"
275
+ ruff check . && ruff format --check . && pytest
276
+ ```
277
+
278
+ The tests run on a CPU in a few seconds with tiny random models from `hf-internal-testing`. They check the label
279
+ scores against a direct computation, the distillation loss and metrics against hand-computed values, cache
280
+ resumption, that the ONNX export reproduces the PyTorch logits, and the whole CLI end to end.
281
+
282
+ ## References
283
+
284
+ - G. Hinton, O. Vinyals, J. Dean. [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531). arXiv:1503.02531, 2015.
285
+ - T. Schick, H. Schütze. [Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference](https://arxiv.org/abs/2001.07676). EACL, 2021. (Verbalisers.)
286
+ - I. Casanueva, T. Temčinas, D. Gerz, M. Henderson, I. Vulić. [Efficient Intent Detection with Dual Sentence Encoders](https://arxiv.org/abs/2003.04807). NLP4ConvAI Workshop, 2020. (Banking77.)
287
+ - W. Wang, F. Wei, L. Dong, H. Bao, N. Yang, M. Zhou. [MiniLM: Deep Self-Attention Distillation for Task-Agnostic Compression of Pre-Trained Transformers](https://arxiv.org/abs/2002.10957). NeurIPS, 2020.
288
+ - N. Reimers, I. Gurevych. [Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks](https://arxiv.org/abs/1908.10084). EMNLP, 2019.
289
+ - Qwen Team. [Qwen2.5 Technical Report](https://arxiv.org/abs/2412.15115). arXiv:2412.15115, 2024.
290
+
291
+ ## Licence
292
+
293
+ [MIT](https://github.com/Andreas-Hum/tinydistill/blob/main/LICENSE). The Banking77 dataset is CC BY 4.0 (PolyAI); models are downloaded from the Hugging Face Hub under
294
+ their own licences.
@@ -0,0 +1,264 @@
1
+ # tinydistill
2
+
3
+ **Most of a large model's quality on one task, many times faster, on a CPU.**
4
+
5
+ `tinydistill` turns an LLM classification prompt into a small encoder classifier by knowledge distillation.
6
+ You describe the task in a short YAML file (labels, how to say each label, an instruction prompt) and give it
7
+ unlabelled texts. An instruction-tuned LLM labels them with soft targets, a small encoder learns to imitate
8
+ those targets, and the result is exported to ONNX (optionally int8) for fast CPU inference.
9
+
10
+ It continues my bachelor's project on inference optimisation by distilling LLMs.
11
+
12
+ ## Results
13
+
14
+ Two tasks with `Qwen/Qwen2.5-1.5B-Instruct` as the zero-shot teacher, and Banking77 also with
15
+ `Qwen/Qwen2.5-7B-Instruct`; the student is always `sentence-transformers/all-MiniLM-L6-v2`. Quality is measured on gold-labelled test texts that
16
+ neither model trains on. Every table is written by the task's `run.sh` from its saved `results*.json` file in
17
+ [`examples/`](https://github.com/Andreas-Hum/tinydistill/tree/main/examples); no number is typed by hand. The teacher's latency includes scoring every surface form
18
+ of every label, which costs most on Banking77 with its many labels, so there the GPU teacher row is the fairer
19
+ reference for speed.
20
+
21
+ ### Banking77: the hard case
22
+
23
+ Intent detection with 77 fine-grained classes ([Banking77](https://huggingface.co/datasets/PolyAI/banking77),
24
+ CC BY 4.0), reproduced by [`examples/banking77/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/banking77/run.sh).
25
+
26
+ Teacher only, no gold labels:
27
+
28
+
29
+ <!-- results: banking77/results.md -->
30
+ | Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
31
+ |---|---|---|---|---|---|---|---|---|---|
32
+ | teacher, zero-shot | cpu | 0.406 | 0.367 | n/a | 19955.9 | n/a | 0.1 | 6175 | 1.0x |
33
+ | teacher, zero-shot | cuda | 0.406 | 0.367 | n/a | 60.3 | n/a | 16.6 | 3087 | 331.1x |
34
+ | student, PyTorch | cpu | 0.386 | 0.315 | 0.772 | 3.9 | 78.3 | 408.9 | 91 | 5124.6x |
35
+ | student, ONNX fp32 | cpu | 0.386 | 0.315 | 0.772 | 2.6 | 82.4 | 388.4 | 91 | 7606.9x |
36
+ | student, ONNX int8 | cpu | 0.383 | 0.311 | 0.761 | 1.0 | 34.4 | 931.1 | 23 | 20502.9x |
37
+
38
+ Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 10003 unlabelled training texts.
39
+ CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
40
+ <!-- /results -->
41
+
42
+ With 8 gold labels per class, and the student trained on all gold labels as an upper bound:
43
+
44
+ <!-- results: banking77/results-k8.md -->
45
+ | Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
46
+ |---|---|---|---|---|---|---|---|---|---|
47
+ | teacher, zero-shot | cpu | 0.406 | 0.367 | n/a | 19699.4 | n/a | 0.1 | 6175 | 1.0x |
48
+ | teacher, zero-shot | cuda | 0.406 | 0.367 | n/a | 58.3 | n/a | 17.2 | 3087 | 338.1x |
49
+ | student, PyTorch | cpu | 0.404 | 0.324 | 0.699 | 3.9 | 76.7 | 417.4 | 91 | 5056.3x |
50
+ | upper bound: student on all gold labels | cpu | 0.911 | 0.909 | 0.412 | n/a | n/a | n/a | n/a | n/a |
51
+ | student, ONNX fp32 | cpu | 0.404 | 0.324 | 0.699 | 2.7 | 72.1 | 443.6 | 91 | 7306.9x |
52
+ | student, ONNX int8 | cpu | 0.398 | 0.318 | 0.697 | 1.0 | 36.7 | 872.1 | 23 | 18792.2x |
53
+
54
+ Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns from the teacher's soft labels on all 10003 training texts, plus cross-entropy on 8 gold labels per class (616 texts, weight 1.0). The upper bound is the same student trained with cross-entropy on all 10003 gold training labels and no teacher; its speed equals the student's.
55
+ CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
56
+ <!-- /results -->
57
+
58
+ With a stronger teacher, `Qwen/Qwen2.5-7B-Instruct` loaded in 4 bit (timed on the GPU only: 4-bit loading needs
59
+ CUDA), teacher only:
60
+
61
+ <!-- results: banking77/results-7b.md -->
62
+ | Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
63
+ |---|---|---|---|---|---|---|---|---|---|
64
+ | teacher, zero-shot, 4-bit | cuda | 0.630 | 0.607 | n/a | 272.2 | n/a | 3.7 | 5443 | 1.0x |
65
+ | student, PyTorch | cpu | 0.623 | 0.587 | 0.827 | 5.5 | 93.0 | 344.2 | 91 | 49.4x |
66
+ | student, ONNX fp32 | cpu | 0.623 | 0.587 | 0.827 | 3.4 | 86.8 | 368.7 | 91 | 80.8x |
67
+ | student, ONNX int8 | cpu | 0.622 | 0.586 | 0.822 | 1.6 | 43.8 | 730.0 | 23 | 171.2x |
68
+
69
+ Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-7B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 10003 unlabelled training texts.
70
+ CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot, 4-bit on cuda.
71
+ <!-- /results -->
72
+
73
+ The same teacher with 8 gold labels per class and the upper bound:
74
+
75
+ <!-- results: banking77/results-k8-7b.md -->
76
+ | Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
77
+ |---|---|---|---|---|---|---|---|---|---|
78
+ | teacher, zero-shot, 4-bit | cuda | 0.630 | 0.607 | n/a | 295.4 | n/a | 3.4 | 5443 | 1.0x |
79
+ | student, PyTorch | cpu | 0.627 | 0.583 | 0.765 | 4.5 | 93.6 | 341.9 | 91 | 65.5x |
80
+ | upper bound: student on all gold labels | cpu | 0.911 | 0.909 | 0.631 | n/a | n/a | n/a | n/a | n/a |
81
+ | student, ONNX fp32 | cpu | 0.627 | 0.583 | 0.765 | 3.2 | 80.9 | 395.7 | 91 | 92.5x |
82
+ | student, ONNX int8 | cpu | 0.622 | 0.576 | 0.762 | 1.1 | 40.7 | 786.4 | 23 | 262.6x |
83
+
84
+ Task `banking77`, 3080 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-7B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns from the teacher's soft labels on all 10003 training texts, plus cross-entropy on 8 gold labels per class (616 texts, weight 1.0). The upper bound is the same student trained with cross-entropy on all 10003 gold training labels and no teacher; its speed equals the student's.
85
+ CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot, 4-bit on cuda.
86
+ <!-- /results -->
87
+
88
+ ### Financial tweets: an easier task
89
+
90
+ Sentiment of financial news tweets, 3 classes
91
+ ([Twitter Financial News Sentiment](https://huggingface.co/datasets/zeroshot/twitter-financial-news-sentiment),
92
+ MIT licence per the dataset card), reproduced by
93
+ [`examples/financial-tweets/run.sh`](https://github.com/Andreas-Hum/tinydistill/blob/main/examples/financial-tweets/run.sh). The validation split is the test set.
94
+
95
+ <!-- results: financial-tweets/results.md -->
96
+ | Model | Device | Accuracy | Macro-F1 | Agreement with teacher | Latency, batch 1 (ms) | Latency, batch 32 (ms) | Throughput (texts/s) | Size (MB) | Speed-up, batch 1 |
97
+ |---|---|---|---|---|---|---|---|---|---|
98
+ | teacher, zero-shot | cpu | 0.535 | 0.557 | n/a | 976.3 | n/a | 1.0 | 6175 | 1.0x |
99
+ | teacher, zero-shot | cuda | 0.535 | 0.557 | n/a | 33.7 | n/a | 29.6 | 3087 | 28.9x |
100
+ | student, PyTorch | cpu | 0.517 | 0.542 | 0.815 | 4.5 | 145.4 | 220.1 | 91 | 219.3x |
101
+ | upper bound: student on all gold labels | cpu | 0.867 | 0.824 | 0.531 | n/a | n/a | n/a | n/a | n/a |
102
+ | student, ONNX fp32 | cpu | 0.517 | 0.542 | 0.815 | 3.2 | 148.5 | 215.4 | 91 | 303.9x |
103
+ | student, ONNX int8 | cpu | 0.513 | 0.538 | 0.810 | 1.3 | 71.5 | 447.7 | 23 | 754.7x |
104
+
105
+ Task `financial-tweets`, 2388 gold-labelled test texts. Teacher: `Qwen/Qwen2.5-1.5B-Instruct`. Student: `sentence-transformers/all-MiniLM-L6-v2`. The student learns only from the teacher's soft labels on 9543 unlabelled training texts. The upper bound is the same student trained with cross-entropy on all 9543 gold training labels and no teacher; its speed equals the student's.
106
+ CPU rows use 2 threads. Latency is the median over repeated runs. Size is the teacher's weights as loaded and the students' model files. The teacher scores one text at a time, so it is timed at batch 1 and its throughput is 1 / latency. Speed-up is relative to the teacher, zero-shot on cpu.
107
+ <!-- /results -->
108
+
109
+ ## Quickstart
110
+
111
+ ```sh
112
+ pip install "tinydistill[hf]"
113
+ curl -LO https://raw.githubusercontent.com/Andreas-Hum/tinydistill/main/examples/banking77/task.yaml
114
+ tinydistill run task.yaml --data hf:PolyAI/banking77:train --test hf:PolyAI/banking77:test \
115
+ --out runs/banking77 --int8
116
+ tinydistill predict runs/banking77/onnx "I still haven't received my new card"
117
+ ```
118
+
119
+ Labelling the full dataset with the default teacher wants a GPU. For a quick look on a CPU, add
120
+ `--teacher Qwen/Qwen2.5-0.5B-Instruct --limit 200 --test-limit 200`. `--limit` takes the first rows, and the
121
+ Banking77 splits are sorted by intent, so a limited run only sees a few intents.
122
+
123
+ `run` does every step. Each step also has its own command, and they all read and write the run directory:
124
+
125
+ | Command | What it does |
126
+ |---|---|
127
+ | `tinydistill label TASK.yaml --data D [--test T] --out RUN` | teacher scores every label of every text; cached, resumable |
128
+ | `tinydistill train RUN` | distils the student from the teacher's soft targets |
129
+ | `tinydistill export RUN [--int8]` | writes `RUN/onnx` (ONNX, tokenizer, labels) |
130
+ | `tinydistill eval RUN` | writes `RUN/results.md` and `RUN/results.json` |
131
+ | `tinydistill predict RUN/onnx TEXT... [--data FILE]` | classifies texts, prints JSON lines |
132
+
133
+ Data can be `.csv`, `.jsonl` or `hf:<dataset>[:<config>]:<split>` (needs the `hf` extra). Useful options:
134
+ `--teacher` (any Hugging Face causal LM), `--load-4bit` (bitsandbytes, CUDA only: `pip install "tinydistill[4bit]"`),
135
+ `--student` (any Hugging Face encoder), `--epochs`, `--lr`, `--device`, `--threads`, and `--cache-dir` (runs that
136
+ share it label each text only once). See `tinydistill <command> --help`.
137
+
138
+ **Few labels.** With `--gold-per-class K`, the student also sees the gold labels of K randomly chosen training
139
+ examples per class, through a cross-entropy term (weight `--ce-weight`, default 1) added to the distillation
140
+ loss. `--upper-bound` additionally trains the same student on all gold training labels without the teacher,
141
+ and the results table gets an upper-bound row: how far the distilled student is from fully supervised training.
142
+
143
+ From Python:
144
+
145
+ ```python
146
+ from tinydistill import Predictor
147
+
148
+ clf = Predictor("runs/banking77/onnx") # uses model.int8.onnx when present
149
+ clf.predict(["How do I top up my card?"]) # list of label names
150
+ clf.predict_proba(["How do I top up my card?"]) # numpy array, one row per text
151
+ ```
152
+
153
+ ### The task file
154
+
155
+ ```yaml
156
+ name: banking77
157
+ text_field: text # column holding the input text
158
+ label_field: label # column holding gold labels (ids, or integers indexing `labels`)
159
+ prompt: |-
160
+ You are classifying messages that customers sent to their bank's support chat.
161
+ Choose the single intent from this list that best matches the message:
162
+ {labels}
163
+
164
+ Message: {text}
165
+
166
+ Reply with the intent exactly as written in the list.
167
+ labels:
168
+ - activate_my_card # a bare id: its name is derived, "activate my card"
169
+ - card_arrival
170
+ # ...
171
+ ```
172
+
173
+ `{labels}` (required) becomes a bulleted list of the label names, and `{text}` is the input. The name is what
174
+ the teacher scores. Each label can be a bare id as above, `id: name`, or carry a description that is shown in
175
+ the list (`- bullish: positive for the stock`) but not scored:
176
+
177
+ ```yaml
178
+ labels:
179
+ bearish:
180
+ description: negative for the stock, company or market
181
+ lost_card: lost or stolen card
182
+ ```
183
+
184
+ ## How the label scoring works
185
+
186
+ The teacher never generates free text. For an input x, the prompt is rendered with the model's chat template
187
+ (user message = the instruction with the label list, generation prompt for the assistant). A continuation is one
188
+ way of writing a label followed by the end-of-turn token, `c_1 ... c_m`, with log-probability
189
+
190
+ log p(c | prompt(x)) = sum_j log p(c_j | prompt(x), c_1 ... c_(j-1))
191
+
192
+ Multi-token labels are handled exactly, and the end-of-turn token keeps a label that is a prefix of another
193
+ (`card` versus `card arrival`) from winning by stopping early. Models do not always write a label the way it is
194
+ listed: Qwen2.5 tends to answer a sentiment question with "Bullish" even when the list says "bullish". So each
195
+ label is scored over its common surface forms, the name as written, lowercase, Capitalised and UPPERCASE, each
196
+ with and without a leading space (forms that tokenise identically count once):
197
+
198
+ score(label) = log sum over forms c of p(c | prompt(x))
199
+
200
+ This is verbaliser coverage: it only counts more ways of writing the same answer, and there is no calibration or
201
+ prior correction. All forms share the prefix tree below, but with many labels they still add up; a task file can
202
+ list fewer (`surface_forms: [lowercase, capitalised]` drops UPPERCASE, which tokenises into many pieces, and
203
+ `surface_forms: []` scores the name as written only). A softmax over the label scores gives the soft targets. The prompt lists each label in its
204
+ primary form, the name as written. The scores are cached as JSON lines keyed by a hash of the text, in a file
205
+ named after a fingerprint of the teacher, prompt and continuations, so an interrupted run resumes and a changed
206
+ prompt never reuses old scores.
207
+
208
+ To keep this fast, the part of the prompt before the text is run once and its key/value cache is kept. Each
209
+ text then needs a single forward pass that covers the text and all labels together: the tokens of all continuations
210
+ (every surface form of every label) form a prefix tree, so continuations that start alike, such as
211
+ `card arrival` and `card linking`, share nodes, and a 4D attention mask lets each tree node attend only to the
212
+ prompt, the text and its own ancestors. There is no padding and no per-label copy of the cache. Tests check the
213
+ scores against a plain forward pass per continuation.
214
+
215
+ The student minimises KL(teacher || student) over the training texts, plus `--ce-weight` times the
216
+ cross-entropy on the K gold labels per class chosen by `--gold-per-class`, if any (standard knowledge
217
+ distillation, Hinton et al., 2015).
218
+
219
+ ## Limitations
220
+
221
+ - **The teacher caps the student.** The student learns to imitate the teacher, mistakes included. A small LLM
222
+ choosing zero-shot among many fine-grained labels can be weak: Qwen2.5-0.5B-Instruct, for example, often
223
+ confuses related Banking77 intents. A bigger teacher (try `--load-4bit` with a 7B model) or a better prompt
224
+ usually helps more than anything on the student side.
225
+ - **Scores are sums, not averages.** Longer verbalisations get lower scores, so very uneven label lengths bias the
226
+ teacher. Keep verbalisations short and of similar length.
227
+ - **Only case and spacing variants are counted.** Plurals, synonyms or rephrasings of a label are not; keep label
228
+ names short and close to how the teacher would answer.
229
+ - **A too-small teacher can collapse.** Qwen2.5-0.5B-Instruct, for example, puts nearly every text into one or two
230
+ labels on the financial-tweets task, whatever the scoring; use 1.5B or larger.
231
+ - **Prompt and label order matter.** Zero-shot LLMs are sensitive to wording and to the order of the options.
232
+ - **Teacher cost grows with labels.** Every text is scored against every surface form of every label (shared
233
+ prefixes are computed once), one text at a time.
234
+ - **Single-label classification only.** Inputs are truncated to 128 tokens by default (`--max-length`).
235
+ - **Timings depend on the machine.** The results table states the threads and devices used; rerun `eval` on your
236
+ own hardware.
237
+ - ONNX export uses PyTorch's TorchScript-based exporter.
238
+
239
+ ## Development
240
+
241
+ ```sh
242
+ python -m venv .venv && . .venv/bin/activate
243
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
244
+ pip install -e ".[dev]"
245
+ ruff check . && ruff format --check . && pytest
246
+ ```
247
+
248
+ The tests run on a CPU in a few seconds with tiny random models from `hf-internal-testing`. They check the label
249
+ scores against a direct computation, the distillation loss and metrics against hand-computed values, cache
250
+ resumption, that the ONNX export reproduces the PyTorch logits, and the whole CLI end to end.
251
+
252
+ ## References
253
+
254
+ - G. Hinton, O. Vinyals, J. Dean. [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531). arXiv:1503.02531, 2015.
255
+ - T. Schick, H. Schütze. [Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference](https://arxiv.org/abs/2001.07676). EACL, 2021. (Verbalisers.)
256
+ - I. Casanueva, T. Temčinas, D. Gerz, M. Henderson, I. Vulić. [Efficient Intent Detection with Dual Sentence Encoders](https://arxiv.org/abs/2003.04807). NLP4ConvAI Workshop, 2020. (Banking77.)
257
+ - W. Wang, F. Wei, L. Dong, H. Bao, N. Yang, M. Zhou. [MiniLM: Deep Self-Attention Distillation for Task-Agnostic Compression of Pre-Trained Transformers](https://arxiv.org/abs/2002.10957). NeurIPS, 2020.
258
+ - N. Reimers, I. Gurevych. [Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks](https://arxiv.org/abs/1908.10084). EMNLP, 2019.
259
+ - Qwen Team. [Qwen2.5 Technical Report](https://arxiv.org/abs/2412.15115). arXiv:2412.15115, 2024.
260
+
261
+ ## Licence
262
+
263
+ [MIT](https://github.com/Andreas-Hum/tinydistill/blob/main/LICENSE). The Banking77 dataset is CC BY 4.0 (PolyAI); models are downloaded from the Hugging Face Hub under
264
+ their own licences.