File size: 5,815 Bytes
4694a7a 3837ef0 4694a7a 3837ef0 4694a7a 3837ef0 4694a7a 3837ef0 4694a7a 3837ef0 4694a7a 3837ef0 4694a7a 3837ef0 4694a7a 3837ef0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | """Single-turn chat with TinyLlama-1.1B-Chat INT4 via ONNX Runtime GenAI.
Writes ``predictions.json`` next to this script.
"""
from __future__ import annotations
import json
from pathlib import Path
import onnxruntime_genai as og
from jinja2 import Environment
from tokenizers import Tokenizer
# -- Configuration -------------------------------------------------------------
# HellaSwag-style commonsense continuation prompt. Small chat models
# (~1B) generate cleanest output on well-scoped everyday scenarios (see
# TinyLlama's 59% HellaSwag acc_norm) vs open-ended factual prose which
# tends to hallucinate and loop.
DEFAULT_PROMPT = (
"A woman is in the kitchen making pancakes. She pours the batter onto "
"a hot pan and waits for bubbles to appear on the surface. Once the "
"bubbles pop, she"
)
MAX_LENGTH = 256 # absolute token budget (prompt + decoded)
DO_SAMPLE = False # greedy decode for reproducibility
TEMPERATURE = 0.0 # ignored unless do_sample=True
def _raise_exception(msg: str) -> None:
"""Bridge for the chat template's ``raise_exception`` helper."""
raise RuntimeError(msg)
def render_chat_template(
template_path: Path, bundle_dir: Path, user_prompt: str
) -> str:
"""Render ``chat_template.jinja`` with a single user turn.
Reads BOS/EOS tokens from ``tokenizer_config.json`` so the rendered string
matches the actual tokenizer's special tokens. ``add_generation_prompt=True``
appends the assistant header so the model continues from there.
"""
template_src = template_path.read_text(encoding="utf-8")
with (bundle_dir / "tokenizer_config.json").open() as f:
tok_cfg = json.load(f)
env = Environment(trim_blocks=True, lstrip_blocks=True, autoescape=False)
env.globals["raise_exception"] = _raise_exception
template = env.from_string(template_src)
return template.render(
messages=[{"role": "user", "content": user_prompt}],
bos_token=tok_cfg.get("bos_token", "<s>"),
eos_token=tok_cfg.get("eos_token", "</s>"),
add_generation_prompt=True,
)
def load_model(bundle_dir: Path) -> tuple[og.Model, Tokenizer]:
"""Load the ONNX Runtime GenAI model and the bundled HF tokenizer."""
model = og.Model(str(bundle_dir))
tokenizer = Tokenizer.from_file(str(bundle_dir / "tokenizer.json"))
return model, tokenizer
def encode_prompt(tokenizer: Tokenizer, bundle_dir: Path, prompt: str) -> list[int]:
"""Tokenise so the model sees exactly one BOS token.
Whether the prompt already carries BOS is a property of the chat template,
not of the model: Llama-3.x templates emit ``bos_token`` themselves,
TinyLlama's does not, and a base model has no template at all. Derive the
``add_special_tokens`` value from the prompt rather than hardcoding it, then
assert the invariant so a template or tokenizer change fails loudly instead
of silently degrading generation quality.
"""
bos = json.loads((bundle_dir / "tokenizer_config.json").read_text())["bos_token"]
ids = tokenizer.encode(prompt, add_special_tokens=not prompt.startswith(bos)).ids
bos_id = tokenizer.token_to_id(bos)
if not ids or ids[0] != bos_id or ids.count(bos_id) != 1:
raise RuntimeError(
f"prompt must carry exactly one leading {bos!r}; got {ids.count(bos_id)}"
)
return ids
def generate(
model: og.Model,
tokenizer: Tokenizer,
bundle_dir: Path,
chat_prompt: str,
) -> tuple[str, int, int]:
"""Run greedy decode for one chat turn.
Returns ``(decoded_response, prompt_token_count, generated_token_count)``.
"""
input_ids = encode_prompt(tokenizer, bundle_dir, chat_prompt)
params = og.GeneratorParams(model)
params.set_search_options(
max_length=MAX_LENGTH,
do_sample=DO_SAMPLE,
temperature=TEMPERATURE,
)
generator = og.Generator(model, params)
generator.append_tokens(input_ids)
while not generator.is_done():
generator.generate_next_token()
full_ids = list(generator.get_sequence(0))
response_ids = full_ids[len(input_ids) :]
decoded = tokenizer.decode(response_ids, skip_special_tokens=True)
return decoded, len(input_ids), len(response_ids)
def save_results(
bundle_dir: Path,
user_prompt: str,
response: str,
prompt_tokens: int,
) -> Path:
"""Persist the prompt/response pair as ``predictions.json``."""
output_path = bundle_dir / "predictions.json"
payload = {
"task": "TinyLlama-1.1B-Chat — single-turn chat",
"chat_template": "chat_template.jinja",
"results": [
{
"user": user_prompt,
"assistant": response,
"prompt_tokens": prompt_tokens,
}
],
"max_length": MAX_LENGTH,
"do_sample": DO_SAMPLE,
"temperature": TEMPERATURE,
}
output_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False))
return output_path
def main() -> None:
bundle_dir = Path(__file__).resolve().parent
template_path = bundle_dir / "chat_template.jinja"
print(f"Loading model from: {bundle_dir}")
model, tokenizer = load_model(bundle_dir)
chat_prompt = render_chat_template(template_path, bundle_dir, DEFAULT_PROMPT)
print(f"\nPrompt: {DEFAULT_PROMPT}\n")
print("Generating...")
response, prompt_tokens, generated_tokens = generate(model, tokenizer, bundle_dir, chat_prompt)
print("\n--- Response ---")
print(response)
print("--- /Response ---")
print(
f"\nPrompt tokens: {prompt_tokens} | " f"Generated tokens: {generated_tokens}"
)
saved = save_results(bundle_dir, DEFAULT_PROMPT, response, prompt_tokens)
print(f"Saved: {saved}")
if __name__ == "__main__":
main()
|