@@ -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
146286if __name__ == "__main__" :
0 commit comments