Skip to content

Commit 0af5387

Browse files
Copilotxadupre
andauthored
Refactor test_trained_tiny_llm.py to match test_random_tiny_llm.py pattern and add CI job (#87)
* Initial plan * Replicate refactoring from test_random_tiny_llm.py into test_trained_tiny_llm.py Agent-Logs-Url: https://github.com/xadupre/mbext/sessions/2d48c936-414f-4d71-a1c1-8c6ac81c053e Co-authored-by: xadupre <22452781+xadupre@users.noreply.github.com> * Add CI job for trained tiny LLM tests Agent-Logs-Url: https://github.com/xadupre/mbext/sessions/074a6487-04d5-41b1-92f9-8bd78cacc5bf Co-authored-by: xadupre <22452781+xadupre@users.noreply.github.com> * fix * fix * Pass HF_TOKEN secret to trained tiny LLM test step Agent-Logs-Url: https://github.com/xadupre/mbext/sessions/ac629472-feba-4e00-9e8b-21d0c8e8d1a2 Co-authored-by: xadupre <22452781+xadupre@users.noreply.github.com> * Rename HF_TOKEN to HUGGING_FACE_HUB_TOKEN and add continue-on-error for connectivity tolerance Agent-Logs-Url: https://github.com/xadupre/mbext/sessions/f159bfd1-b592-44be-9bac-3e275644e1b2 Co-authored-by: xadupre <22452781+xadupre@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: xadupre <22452781+xadupre@users.noreply.github.com> Co-authored-by: Xavier Dupré <xadupre@microsoft.com>
1 parent 21dcc7f commit 0af5387

2 files changed

Lines changed: 117 additions & 116 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Trained Tiny LLM Tests
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
tests:
14+
name: ci (${{ matrix.os }}, py${{ matrix.python-version }}, torch-${{ matrix.torch-version || 'stable' }}, transformers-${{ matrix.transformers-version || 'latest' }})
15+
runs-on: ${{ matrix.os }}
16+
permissions:
17+
contents: read
18+
id-token: write
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
os: ["ubuntu-latest"]
23+
python-version: ["3.13"]
24+
transformers-version: ["5.5"]
25+
torch-version: ["2.10"]
26+
steps:
27+
- uses: actions/checkout@v4
28+
- uses: actions/setup-python@v5
29+
with:
30+
python-version: "3.13"
31+
32+
# we install torch first to avoid downloading any CUDA dependency
33+
34+
- name: Install nightly pytorch
35+
if: matrix.torch-version == 'nightly'
36+
run: pip install --pre --upgrade torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cpu
37+
38+
- name: Install pytorch 2.10 (Linux/Windows)
39+
if: matrix.torch-version == '2.10' && matrix.os != 'macos-latest'
40+
run: pip install torch==2.10.0+cpu torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
41+
42+
- name: Install dependencies
43+
run: pip install -e ".[dev]" torch transformers tokenizers
44+
45+
- name: Run trained tiny LLM tests
46+
continue-on-error: true
47+
env:
48+
HUGGING_FACE_HUB_TOKEN: ${{ secrets.HUGGING_FACE_HUB_TOKEN }}
49+
run: LONGTEST=1 pytest tests/trained/test_trained_tiny_llm.py -v --cov=modelbuilder --cov-report=xml --cov-report=term-missing
50+
51+
- name: Upload coverage to Codecov
52+
uses: codecov/codecov-action@v5
53+
with:
54+
files: coverage.xml
55+
flags: trained-tiny-llm-tests
56+
fail_ci_if_error: false

tests/trained/test_trained_tiny_llm.py

Lines changed: 61 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,21 @@
66
import os
77
import unittest
88

9-
import numpy as np
10-
11-
from modelbuilder.ext_test_case import ExtTestCase
9+
from modelbuilder.ext_test_case import ExtTestCase, hide_stdout, long_test
1210

1311
MODEL_NAME = "arnir0/Tiny-LLM"
1412

1513

1614
class TestTrainedTinyLLM(ExtTestCase):
17-
def _common_part(self, precision, dtype, int4=False):
15+
def _common_part(self, precision, dtype, provider="cpu", int4=False):
1816
from transformers import AutoModelForCausalLM
17+
1918
from modelbuilder.builder import create_model
2019

2120
# Use 4 layers so that both rope (layers 0-2) and no-rope (layer 3)
2221
# code paths are exercised with the default no_rope_layer_interval=4.
2322
output_dir, cache_dir = self.get_dirs(
24-
f"test_trained_tiny_llm_{'int4' if int4 else precision}_cpu", clean=False
23+
f"test_trained_tiny_llm_{'int4' if int4 else precision}_{provider}", clean=False
2524
)
2625
onnx_path = os.path.join(output_dir, "model.onnx")
2726
if not os.path.exists(onnx_path):
@@ -30,7 +29,7 @@ def _common_part(self, precision, dtype, int4=False):
3029
model_name=MODEL_NAME,
3130
input_path="",
3231
precision="int4" if int4 else precision,
33-
execution_provider="cpu",
32+
execution_provider=provider,
3433
output_dir=output_dir,
3534
cache_dir=cache_dir,
3635
)
@@ -44,22 +43,20 @@ def _common_part(self, precision, dtype, int4=False):
4443
model.eval()
4544
return onnx_path, model
4645

47-
def test_trained_tiny_llm_fp32_discrepancies_cpu(self):
48-
"""
49-
Convert arnir0/Tiny-LLM to an fp32 ONNX model targeting the CPU execution
50-
provider. Only one hidden layer is materialised (via the
51-
``num_hidden_layers`` extra option) to keep the test fast.
52-
The test verifies that:
53-
* ``create_model`` completes without error.
54-
* The expected ``model.onnx`` file is written to the output directory.
55-
* The produced ONNX file passes ``onnx.checker.check_model``.
56-
"""
46+
def _dtype_from_precision(self, precision):
5747
import torch
5848

59-
precision, dtype, np_dtype = "fp32", torch.float32, np.float32
49+
dtype_map = {"fp32": torch.float32, "fp16": torch.float16, "bf16": torch.bfloat16}
50+
return dtype_map.get(precision, torch.float32)
6051

61-
onnx_path, model = self._common_part(precision, dtype)
62-
sess = self._check_with_ort(onnx_path, cpu=True)
52+
def common_discrepancies(self, precision, provider):
53+
import torch
54+
55+
dtype = self._dtype_from_precision(precision)
56+
np_dtype = self.get_input_np_dtype(precision)
57+
58+
onnx_path, model = self._common_part(precision, dtype, provider=provider)
59+
sess = self._check_with_ort(onnx_path, cpu=provider == "cpu")
6360
config = model.config
6461

6562
batch_size = 1
@@ -70,7 +67,7 @@ def test_trained_tiny_llm_fp32_discrepancies_cpu(self):
7067
batch_size=batch_size,
7168
seq_len=seq_len,
7269
np_dtype=np_dtype,
73-
provider="cpu",
70+
provider=provider,
7471
head_size=head_size,
7572
num_hidden_layers=config.num_hidden_layers,
7673
num_key_value_heads=config.num_key_value_heads,
@@ -89,31 +86,16 @@ def test_trained_tiny_llm_fp32_discrepancies_cpu(self):
8986
precision=precision,
9087
model_id=MODEL_NAME,
9188
experiment="forward",
92-
provider="cpu",
93-
test="test_trained_tiny_llm_fp32_discrepancies_cpu",
89+
provider=provider,
90+
test=f"test_trained_tiny_llm_{precision}_discrepancies_{provider}",
9491
input_type="text",
92+
kind="trained",
9593
)
9694
)
9795
self.log_results(disc)
9896
self.assertLess(disc["max_abs_err"], 0.05)
9997

100-
def test_trained_tiny_llm_genai_generate_cpu(self):
101-
"""
102-
Compare ``transformers.generate`` with ``onnxruntime-genai`` generate
103-
on the ``arnir0/Tiny-LLM`` trained model.
104-
105-
The test converts the model to fp32 ONNX (CPU), then:
106-
107-
* Runs ``transformers.generate(do_sample=False)`` on a short prompt.
108-
* Runs ``onnxruntime_genai`` greedy generation on the same prompt and
109-
ONNX model.
110-
111-
Both backends use greedy (argmax) decoding, so the generated token
112-
sequences must be bit-for-bit identical.
113-
114-
The test is skipped automatically when ``onnxruntime-genai`` is not
115-
installed.
116-
"""
98+
def common_genai_generate(self, precision, provider, int4=False):
11799
try:
118100
import onnxruntime_genai as og
119101
except ImportError:
@@ -124,9 +106,9 @@ def test_trained_tiny_llm_genai_generate_cpu(self):
124106
import torch
125107
from transformers import AutoTokenizer
126108

127-
precision, dtype = "fp16", torch.float16
109+
dtype = self._dtype_from_precision(precision)
128110

129-
onnx_path, model = self._common_part(precision, dtype)
111+
onnx_path, model = self._common_part(precision, dtype, provider=provider, int4=int4)
130112

131113
genai_config_path = os.path.join(os.path.dirname(onnx_path), "genai_config.json")
132114
self.assertExists(genai_config_path)
@@ -174,102 +156,65 @@ def test_trained_tiny_llm_genai_generate_cpu(self):
174156

175157
# Greedy decoding is deterministic: both backends must produce the
176158
# exact same token sequence (prompt + all generated tokens).
159+
test_precision = "int4" if int4 else precision
177160
disc = self.first_token_diff(pt_tokens[start_sequence:], og_tokens)
178161
disc.update(
179162
dict(
180-
precision=precision,
163+
precision=test_precision,
181164
model_id=MODEL_NAME,
182165
experiment="generate",
183-
provider="cpu",
184-
test="test_trained_tiny_llm_genai_generate_cpu",
166+
provider=provider,
167+
test=f"test_trained_tiny_llm_genai_generate_{test_precision}_{provider}",
185168
expected_text=tokenizer.decode(
186169
pt_tokens[start_sequence:], skip_special_tokens=False
187170
),
188171
genai_text=tokenizer.decode(og_tokens, skip_special_tokens=False),
189172
input_type="text",
173+
kind="trained",
190174
)
191175
)
192176
self.log_results(disc)
193177
self.assertEqual(pt_tokens[start_sequence:], og_tokens)
194178

195-
def test_trained_tiny_llm_genai_generate_int4_cpu(self):
196-
try:
197-
import onnxruntime_genai as og
198-
except ImportError:
199-
raise unittest.SkipTest(
200-
"onnxruntime-genai is not installed; skipping genai comparison test."
201-
)
202-
203-
import torch
204-
from transformers import AutoTokenizer
205-
206-
precision, dtype = "fp32", torch.float32
207-
208-
onnx_path, model = self._common_part(precision, dtype, int4=True)
209-
210-
genai_config_path = os.path.join(os.path.dirname(onnx_path), "genai_config.json")
211-
self.assertExists(genai_config_path)
212-
213-
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
214-
prompt = "Once upon a time"
215-
max_new_tokens = 20
179+
@long_test()
180+
@hide_stdout()
181+
def test_trained_tiny_llm_fp32_discrepancies_cpu(self):
182+
"""
183+
Convert arnir0/Tiny-LLM to an fp32 ONNX model targeting the CPU execution
184+
provider. Only one hidden layer is materialised (via the
185+
``num_hidden_layers`` extra option) to keep the test fast.
186+
The test verifies that:
187+
* ``create_model`` completes without error.
188+
* The expected ``model.onnx`` file is written to the output directory.
189+
* The produced ONNX file passes ``onnx.checker.check_model``.
190+
"""
191+
self.common_discrepancies("fp32", "cpu")
216192

217-
# ------------------------------------------------------------------
218-
# transformers greedy generation (reference)
219-
# ------------------------------------------------------------------
220-
inputs = tokenizer(prompt, return_tensors="pt")
221-
start_sequence = inputs["input_ids"].shape[1]
222-
inputs = inputs.to("cpu")
223-
with torch.no_grad():
224-
pt_output = model.generate(
225-
**inputs,
226-
max_new_tokens=max_new_tokens,
227-
do_sample=False,
228-
pad_token_id=tokenizer.eos_token_id,
229-
)
230-
pt_tokens = pt_output[0].tolist()
193+
@long_test()
194+
@hide_stdout()
195+
def test_trained_tiny_llm_genai_generate_cpu(self):
196+
"""
197+
Compare ``transformers.generate`` with ``onnxruntime-genai`` generate
198+
on the ``arnir0/Tiny-LLM`` trained model.
231199
232-
# ------------------------------------------------------------------
233-
# onnxruntime-genai greedy generation
234-
# ------------------------------------------------------------------
235-
og_model = og.Model(os.path.dirname(onnx_path))
200+
The test converts the model to fp16 ONNX (CPU), then:
236201
237-
params = og.GeneratorParams(og_model)
238-
params.set_search_options(
239-
do_sample=False,
240-
max_length=inputs["input_ids"].shape[1] + max_new_tokens,
241-
temperature=1.0,
242-
top_k=1,
243-
)
202+
* Runs ``transformers.generate(do_sample=False)`` on a short prompt.
203+
* Runs ``onnxruntime_genai`` greedy generation on the same prompt and
204+
ONNX model.
244205
245-
generator = og.Generator(og_model, params)
246-
generator.append_tokens(inputs["input_ids"])
206+
Both backends use greedy (argmax) decoding, so the generated token
207+
sequences must be bit-for-bit identical.
247208
248-
og_tokens = []
249-
while not generator.is_done():
250-
generator.generate_next_token()
251-
og_token = generator.get_next_tokens()[0]
252-
og_tokens.append(int(og_token))
209+
The test is skipped automatically when ``onnxruntime-genai`` is not
210+
installed.
211+
"""
212+
self.common_genai_generate("fp16", "cpu")
253213

254-
# Greedy decoding is deterministic: both backends must produce the
255-
# exact same token sequence (prompt + all generated tokens).
256-
disc = self.first_token_diff(pt_tokens[start_sequence:], og_tokens)
257-
disc.update(
258-
dict(
259-
precision="int4",
260-
model_id=MODEL_NAME,
261-
experiment="generate",
262-
provider="cpu",
263-
test="test_trained_tiny_llm_genai_generate_int4_cpu",
264-
expected_text=tokenizer.decode(
265-
pt_tokens[start_sequence:], skip_special_tokens=False
266-
),
267-
genai_text=tokenizer.decode(og_tokens, skip_special_tokens=False),
268-
input_type="text",
269-
)
270-
)
271-
self.log_results(disc)
272-
self.assertEqual(pt_tokens[start_sequence:], og_tokens)
214+
@long_test()
215+
@hide_stdout()
216+
def test_trained_tiny_llm_genai_generate_int4_cpu(self):
217+
self.common_genai_generate("fp32", "cpu", int4=True)
273218

274219

275220
if __name__ == "__main__":

0 commit comments

Comments
 (0)