Skip to content

Commit 4552f7d

Browse files
Copilotxadupre
andauthored
add fast tests for OlmoForCausalLM
Agent-Logs-Url: https://github.com/xadupre/mbext/sessions/b9a0fb80-6c05-45df-95b8-76730f121da9 Co-authored-by: xadupre <22452781+xadupre@users.noreply.github.com>
1 parent a9e7f4f commit 4552f7d

1 file changed

Lines changed: 333 additions & 0 deletions

File tree

tests/fast/test_random_olmo.py

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
import os
7+
import unittest
8+
9+
import numpy as np
10+
11+
from modelbuilder.ext_test_case import ExtTestCase, hide_stdout, requires_cuda
12+
13+
MODEL_NAME = "OlmoForCausalLM"
14+
15+
16+
class TestOLMo(ExtTestCase):
17+
def common_fast_olmo_random_weights(self, precision, provider):
18+
import torch
19+
from tokenizers import Tokenizer
20+
from tokenizers.models import WordLevel
21+
from transformers import AutoModelForCausalLM, OlmoConfig, PreTrainedTokenizerFast
22+
23+
from modelbuilder.builder import create_model
24+
25+
num_hidden_layers = 1
26+
27+
config = OlmoConfig(
28+
architectures=["OlmoForCausalLM"],
29+
hidden_act="silu",
30+
hidden_size=512,
31+
intermediate_size=1376,
32+
max_position_embeddings=2048,
33+
model_type="olmo",
34+
num_attention_heads=8,
35+
num_hidden_layers=num_hidden_layers,
36+
num_key_value_heads=4,
37+
rope_theta=10000.0,
38+
vocab_size=50304,
39+
)
40+
41+
basename = f"test_discrepancies_olmo_{precision}_{provider}"
42+
model_dir = self.get_model_dir(basename)
43+
output_dir, cache_dir = self.get_dirs(basename)
44+
45+
model = AutoModelForCausalLM.from_config(config)
46+
model.eval().to(provider)
47+
model.save_pretrained(model_dir)
48+
49+
vocab = {"<unk>": 0, "<s>": 1, "</s>": 2}
50+
tokenizer = PreTrainedTokenizerFast(
51+
tokenizer_object=Tokenizer(WordLevel(vocab=vocab, unk_token="<unk>")),
52+
bos_token="<s>",
53+
eos_token="</s>",
54+
unk_token="<unk>",
55+
)
56+
tokenizer.save_pretrained(model_dir)
57+
58+
create_model(
59+
model_name=MODEL_NAME,
60+
input_path=model_dir,
61+
output_dir=output_dir,
62+
precision=precision,
63+
execution_provider=provider,
64+
cache_dir=cache_dir,
65+
)
66+
67+
log_data = dict(
68+
precision=precision,
69+
model_id=MODEL_NAME,
70+
experiment="forward",
71+
provider=provider,
72+
test=basename,
73+
input_type="text",
74+
kind="random",
75+
)
76+
77+
onnx_path = os.path.join(output_dir, "model.onnx")
78+
self.assertExists(onnx_path)
79+
sess = self.check_ort(onnx_path)
80+
81+
batch_size = 1
82+
seq_len = 5
83+
head_size = config.hidden_size // config.num_attention_heads
84+
85+
torch.manual_seed(0)
86+
input_ids = torch.randint(0, config.vocab_size, (batch_size, seq_len)).to(provider)
87+
onnx_input_names = [i.name for i in sess.get_inputs()]
88+
onnx_output_names = [i.name for i in sess.get_outputs()]
89+
90+
with self.subTest(step="prefill"):
91+
prefill_feed = {
92+
"input_ids": input_ids.cpu().numpy().astype(np.int64),
93+
"attention_mask": np.ones((batch_size, seq_len), dtype=np.int64),
94+
"position_ids": np.arange(seq_len, dtype=np.int64).reshape(batch_size, seq_len),
95+
}
96+
for i in range(num_hidden_layers):
97+
prefill_feed[f"past_key_values.{i}.key"] = np.zeros(
98+
(batch_size, config.num_key_value_heads, 0, head_size),
99+
dtype=self.get_input_np_dtype(precision),
100+
)
101+
prefill_feed[f"past_key_values.{i}.value"] = np.zeros(
102+
(batch_size, config.num_key_value_heads, 0, head_size),
103+
dtype=self.get_input_np_dtype(precision),
104+
)
105+
prefill_feed = {k: v for k, v in prefill_feed.items() if k in onnx_input_names}
106+
prefill_outputs = sess.run(None, prefill_feed)
107+
prefill_results = dict(zip(onnx_output_names, prefill_outputs))
108+
109+
with torch.no_grad():
110+
pt_prefill = model(input_ids)
111+
112+
np_prefill = pt_prefill.logits.detach().cpu().numpy()
113+
disc = self.get_numpy_discrepancy(np_prefill, prefill_outputs[0])
114+
self.log_results({"step": "prefill", **disc, **log_data})
115+
atol = {"fp16": 1e-2, "bf16": 1e-2, "fp32": 1e-3, "int4": 0.5}
116+
np.testing.assert_allclose(
117+
np_prefill, prefill_outputs[0], atol=atol[precision], rtol=1e-3
118+
)
119+
120+
with self.subTest(step="decode"):
121+
next_token = int(np.argmax(prefill_results["logits"][0, -1, :]))
122+
123+
decode_feed = {
124+
"input_ids": np.array([[next_token]], dtype=np.int64),
125+
"attention_mask": np.ones((batch_size, seq_len + 1), dtype=np.int64),
126+
"position_ids": np.array([[seq_len]], dtype=np.int64),
127+
}
128+
for i in range(num_hidden_layers):
129+
decode_feed[f"past_key_values.{i}.key"] = prefill_results[f"present.{i}.key"]
130+
decode_feed[f"past_key_values.{i}.value"] = prefill_results[f"present.{i}.value"]
131+
decode_feed = {k: v for k, v in decode_feed.items() if k in onnx_input_names}
132+
133+
decode_outputs = sess.run(None, decode_feed)
134+
onnx_decode_logits = decode_outputs[0]
135+
136+
with torch.no_grad():
137+
pt_past_kv = pt_prefill.past_key_values
138+
next_token_tensor = torch.tensor([[next_token]], dtype=torch.long).to(provider)
139+
pt_decode = model(next_token_tensor, past_key_values=pt_past_kv)
140+
pt_decode_logits = pt_decode.logits.detach().cpu().numpy()
141+
142+
disc = self.get_numpy_discrepancy(pt_decode_logits, decode_outputs[0])
143+
self.log_results({"step": "decode", **disc, **log_data})
144+
atol = {"fp16": 1e-2, "bf16": 1e-2, "fp32": 1e-3, "int4": 0.5}
145+
rtol = {"fp16": 10, "bf16": 1e-2, "fp32": 1e-3, "int4": 10000}
146+
np.testing.assert_allclose(
147+
pt_decode_logits, onnx_decode_logits, atol=atol[precision], rtol=rtol[precision]
148+
)
149+
150+
def common_olmo_greedy_generation(self, precision, provider):
151+
import torch
152+
from tokenizers import Tokenizer
153+
from tokenizers.models import WordLevel
154+
from transformers import AutoModelForCausalLM, OlmoConfig, PreTrainedTokenizerFast
155+
156+
from modelbuilder.builder import create_model
157+
158+
num_hidden_layers = 1
159+
160+
config = OlmoConfig(
161+
architectures=["OlmoForCausalLM"],
162+
bos_token_id=1,
163+
eos_token_id=2,
164+
hidden_act="silu",
165+
hidden_size=512,
166+
intermediate_size=1376,
167+
max_position_embeddings=2048,
168+
model_type="olmo",
169+
num_attention_heads=8,
170+
num_hidden_layers=num_hidden_layers,
171+
num_key_value_heads=4,
172+
rope_theta=10000.0,
173+
vocab_size=50304,
174+
)
175+
176+
basename = f"test_generation_olmo_{precision}_{provider}"
177+
model_dir = self.get_model_dir(basename)
178+
output_dir, cache_dir = self.get_dirs(basename)
179+
180+
torch.manual_seed(42)
181+
model = AutoModelForCausalLM.from_config(config)
182+
model.eval().to(provider)
183+
model.save_pretrained(model_dir)
184+
185+
vocab = {"<unk>": 0, "<s>": 1, "</s>": 2}
186+
tokenizer = PreTrainedTokenizerFast(
187+
tokenizer_object=Tokenizer(WordLevel(vocab=vocab, unk_token="<unk>")),
188+
bos_token="<s>",
189+
eos_token="</s>",
190+
unk_token="<unk>",
191+
)
192+
tokenizer.save_pretrained(model_dir)
193+
194+
create_model(
195+
model_name=MODEL_NAME,
196+
input_path=model_dir,
197+
output_dir=output_dir,
198+
precision=precision,
199+
execution_provider=provider,
200+
cache_dir=cache_dir,
201+
)
202+
203+
onnx_path = os.path.join(output_dir, "model.onnx")
204+
self.assertExists(onnx_path)
205+
sess = self._check_with_ort(onnx_path, cpu=provider == "cpu")
206+
207+
input_names = {inp.name for inp in sess.get_inputs()}
208+
output_names = [out.name for out in sess.get_outputs()]
209+
210+
batch_size = 1
211+
head_size = config.hidden_size // config.num_attention_heads
212+
max_new_tokens = 10
213+
214+
torch.manual_seed(0)
215+
prompt_ids = torch.randint(3, config.vocab_size, (batch_size, 5)).to(provider)
216+
217+
with torch.no_grad():
218+
pt_output = model.generate(
219+
prompt_ids,
220+
max_new_tokens=max_new_tokens,
221+
do_sample=False,
222+
pad_token_id=config.eos_token_id,
223+
)
224+
pt_tokens = pt_output[0].tolist()
225+
226+
current_ids = prompt_ids.detach().cpu().numpy().astype(np.int64)
227+
228+
past_kv = {}
229+
for i in range(num_hidden_layers):
230+
past_kv[f"past_key_values.{i}.key"] = np.zeros(
231+
(batch_size, config.num_key_value_heads, 0, head_size),
232+
dtype=self.get_input_np_dtype(precision),
233+
)
234+
past_kv[f"past_key_values.{i}.value"] = np.zeros(
235+
(batch_size, config.num_key_value_heads, 0, head_size),
236+
dtype=self.get_input_np_dtype(precision),
237+
)
238+
239+
onnx_tokens = current_ids[0].tolist()
240+
for _ in range(max_new_tokens):
241+
past_len = past_kv["past_key_values.0.key"].shape[2]
242+
cur_len = current_ids.shape[1]
243+
244+
feed = {
245+
"input_ids": current_ids,
246+
"attention_mask": np.ones((batch_size, past_len + cur_len), dtype=np.int64),
247+
"position_ids": np.arange(past_len, past_len + cur_len, dtype=np.int64).reshape(
248+
batch_size, cur_len
249+
),
250+
}
251+
for i in range(num_hidden_layers):
252+
feed[f"past_key_values.{i}.key"] = past_kv[f"past_key_values.{i}.key"]
253+
feed[f"past_key_values.{i}.value"] = past_kv[f"past_key_values.{i}.value"]
254+
feed = {k: v for k, v in feed.items() if k in input_names}
255+
256+
outputs = sess.run(None, feed)
257+
results = dict(zip(output_names, outputs))
258+
259+
next_token = int(np.argmax(results["logits"][0, -1, :]))
260+
onnx_tokens.append(next_token)
261+
262+
for i in range(num_hidden_layers):
263+
past_kv[f"past_key_values.{i}.key"] = results[f"present.{i}.key"]
264+
past_kv[f"past_key_values.{i}.value"] = results[f"present.{i}.value"]
265+
266+
current_ids = np.array([[next_token]], dtype=np.int64)
267+
268+
if next_token == config.eos_token_id:
269+
break
270+
271+
diff = self.first_token_diff(pt_tokens, onnx_tokens)
272+
diff.update(
273+
dict(
274+
precision=precision,
275+
model_id=MODEL_NAME,
276+
experiment="generate",
277+
provider=provider,
278+
test=basename,
279+
input_type="text",
280+
kind="fast",
281+
)
282+
)
283+
self.log_results(diff)
284+
if precision == "fp16":
285+
pt_tokens = pt_tokens[:-5]
286+
onnx_tokens = onnx_tokens[:-5]
287+
self.assertEqual(pt_tokens, onnx_tokens)
288+
289+
@hide_stdout()
290+
def test_olmo_fp32_cpu_greedy_generation(self):
291+
self.common_olmo_greedy_generation("fp32", "cpu")
292+
293+
@hide_stdout()
294+
def test_olmo_fp16_cpu_greedy_generation(self):
295+
self.common_olmo_greedy_generation("fp16", "cpu")
296+
297+
@unittest.skip("fails due to incorrect model")
298+
@hide_stdout()
299+
def test_olmo_fp32_cuda_greedy_generation(self):
300+
self.common_olmo_greedy_generation("fp32", "cuda")
301+
302+
@hide_stdout()
303+
@requires_cuda()
304+
def test_olmo_fp16_cuda_greedy_generation(self):
305+
self.common_olmo_greedy_generation("fp16", "cuda")
306+
307+
@unittest.skip("onnxruntime python binding does not support bf16 easily")
308+
@hide_stdout()
309+
@requires_cuda()
310+
def test_olmo_bf16_cuda_greedy_generation(self):
311+
self.common_olmo_greedy_generation("bf16", "cuda")
312+
313+
@hide_stdout()
314+
def test_fast_discrepancy_olmo_fp32_cpu(self):
315+
self.common_fast_olmo_random_weights("fp32", "cpu")
316+
317+
@hide_stdout()
318+
def test_fast_discrepancy_olmo_fp16_cpu(self):
319+
self.common_fast_olmo_random_weights("fp16", "cpu")
320+
321+
@unittest.skip("issue")
322+
@hide_stdout()
323+
def test_fast_discrepancy_olmo_int4_cpu(self):
324+
self.common_fast_olmo_random_weights("int4", "cpu")
325+
326+
@hide_stdout()
327+
@requires_cuda()
328+
def test_fast_discrepancy_olmo_fp16_cuda(self):
329+
self.common_fast_olmo_random_weights("fp16", "cuda")
330+
331+
332+
if __name__ == "__main__":
333+
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)