Skip to content

Commit 3b1a2c6

Browse files
Copilotxadupre
andauthored
Add end-to-end unit test for 2-layer Tiny-LLM with discrepancy check
Agent-Logs-Url: https://github.com/xadupre/mbext/sessions/de71068e-9001-4798-9ef7-964b487e52f5 Co-authored-by: xadupre <22452781+xadupre@users.noreply.github.com>
1 parent 5dfdbd6 commit 3b1a2c6

1 file changed

Lines changed: 142 additions & 2 deletions

File tree

tests/test_tiny_llm.py

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ def test_tiny_llm_fp32_cpu_random_weights(self):
7575
"""
7676
from tokenizers import Tokenizer
7777
from tokenizers.models import WordLevel
78-
from transformers import AutoModelForCausalLM, LlamaConfig, PreTrainedTokenizerFast
78+
from transformers import (
79+
AutoModelForCausalLM,
80+
LlamaConfig,
81+
PreTrainedTokenizerFast,
82+
)
7983

8084
from modelbuilder.builder import create_model
8185

@@ -138,9 +142,145 @@ def test_tiny_llm_fp32_cpu_random_weights(self):
138142
# Validate that the ONNX model can be loaded by the runtime.
139143
import onnxruntime
140144

141-
onnxruntime.InferenceSession(
145+
onnxruntime.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
146+
147+
def test_tiny_llm_fp32_cpu_e2e_2layers(self):
148+
"""
149+
End-to-end test: build a 2-layer arnir0/Tiny-LLM with randomly
150+
initialised weights, convert it to an fp32 ONNX model targeting the
151+
CPU execution provider, run inference through both the original
152+
PyTorch model and the produced ONNX model on the same input, and
153+
verify that the two sets of logits agree within a small tolerance.
154+
155+
The test verifies that:
156+
* ``create_model`` completes without error when given a local model
157+
directory with exactly 2 hidden layers.
158+
* The expected ``model.onnx`` file is written to the output directory.
159+
* ONNX logits match PyTorch logits (no significant numerical
160+
discrepancies) across the full vocabulary dimension.
161+
"""
162+
import numpy as np
163+
import onnxruntime
164+
import torch
165+
from tokenizers import Tokenizer
166+
from tokenizers.models import WordLevel
167+
from transformers import (
168+
AutoModelForCausalLM,
169+
LlamaConfig,
170+
PreTrainedTokenizerFast,
171+
)
172+
173+
from modelbuilder.builder import create_model
174+
175+
# Fix the seed so the random weights and the random input are
176+
# reproducible across runs.
177+
torch.manual_seed(42)
178+
179+
# Config matching the arnir0/Tiny-LLM architecture but with exactly
180+
# 2 hidden layers as required by the issue.
181+
config = LlamaConfig(
182+
architectures=["LlamaForCausalLM"],
183+
bos_token_id=1,
184+
eos_token_id=2,
185+
hidden_act="silu",
186+
hidden_size=512,
187+
intermediate_size=1376,
188+
max_position_embeddings=2048,
189+
model_type="llama",
190+
num_attention_heads=8,
191+
num_hidden_layers=2,
192+
num_key_value_heads=4,
193+
rms_norm_eps=1e-05,
194+
rope_theta=10000.0,
195+
vocab_size=32000,
196+
)
197+
198+
with tempfile.TemporaryDirectory() as model_dir:
199+
# Create a model with random weights from the config and save it.
200+
model = AutoModelForCausalLM.from_config(config)
201+
model.eval()
202+
model.save_pretrained(model_dir)
203+
204+
# Create and save a minimal tokenizer so that save_processing()
205+
# inside create_model() can load and copy it to the output folder.
206+
vocab = {"<unk>": 0, "<s>": 1, "</s>": 2}
207+
tokenizer = PreTrainedTokenizerFast(
208+
tokenizer_object=Tokenizer(WordLevel(vocab=vocab, unk_token="<unk>")),
209+
bos_token="<s>",
210+
eos_token="</s>",
211+
unk_token="<unk>",
212+
)
213+
tokenizer.save_pretrained(model_dir)
214+
215+
output_dir = os.path.join(model_dir, "onnx_output")
216+
cache_dir = os.path.join(model_dir, "cache")
217+
os.makedirs(output_dir, exist_ok=True)
218+
os.makedirs(cache_dir, exist_ok=True)
219+
220+
# Convert to ONNX, materialising both hidden layers.
221+
create_model(
222+
model_name=MODEL_NAME,
223+
input_path=model_dir,
224+
output_dir=output_dir,
225+
precision="fp32",
226+
execution_provider="cpu",
227+
cache_dir=cache_dir,
228+
num_hidden_layers=2,
229+
)
230+
231+
onnx_path = os.path.join(output_dir, "model.onnx")
232+
assert os.path.exists(
233+
onnx_path
234+
), f"Expected ONNX model not found at {onnx_path}"
235+
236+
# --- PyTorch inference ---
237+
batch_size = 1
238+
seq_len = 5
239+
input_ids = torch.randint(0, config.vocab_size, (batch_size, seq_len))
240+
with torch.no_grad():
241+
pt_logits = model(input_ids).logits.numpy()
242+
243+
# --- ONNX Runtime inference ---
244+
sess = onnxruntime.InferenceSession(
142245
onnx_path, providers=["CPUExecutionProvider"]
143246
)
247+
# Determine which inputs the ONNX model actually expects so that
248+
# optional inputs (e.g. position_ids for GQA+RotEmb) are handled
249+
# correctly.
250+
onnx_input_names = {inp.name for inp in sess.get_inputs()}
251+
252+
head_size = config.hidden_size // config.num_attention_heads
253+
onnx_feed = {
254+
"input_ids": input_ids.numpy().astype(np.int64),
255+
"attention_mask": np.ones((batch_size, seq_len), dtype=np.int64),
256+
"position_ids": np.arange(seq_len, dtype=np.int64).reshape(
257+
batch_size, seq_len
258+
),
259+
}
260+
# Provide empty past KV-cache tensors for every materialised layer.
261+
for i in range(config.num_hidden_layers):
262+
onnx_feed[f"past_key_values.{i}.key"] = np.zeros(
263+
(batch_size, config.num_key_value_heads, 0, head_size),
264+
dtype=np.float32,
265+
)
266+
onnx_feed[f"past_key_values.{i}.value"] = np.zeros(
267+
(batch_size, config.num_key_value_heads, 0, head_size),
268+
dtype=np.float32,
269+
)
270+
# Keep only what the session expects.
271+
onnx_feed = {k: v for k, v in onnx_feed.items() if k in onnx_input_names}
272+
273+
onnx_outputs = sess.run(None, onnx_feed)
274+
onnx_logits = onnx_outputs[0]
275+
276+
# --- Check discrepancies ---
277+
np.testing.assert_allclose(
278+
pt_logits,
279+
onnx_logits,
280+
atol=1e-3,
281+
rtol=1e-3,
282+
err_msg="Discrepancy detected between PyTorch and ONNX logits",
283+
)
144284

145285

146286
if __name__ == "__main__":

0 commit comments

Comments
 (0)