| """ |
| inference.py — load Aura weights and generate text. |
| |
| Usage as a library: |
| from inference import load_model, generate |
| model, tokenizer, config = load_model(".") |
| out = generate(model, tokenizer, "<s><|yor_Latn|>Kaabo...", max_new_tokens=128) |
| |
| Usage from the command line: see generate.py. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from pathlib import Path |
| from typing import Optional |
|
|
| import torch |
| import torch.nn.functional as F |
| from tokenizers import Tokenizer |
|
|
| from llama3 import LlamaTransformer, ModelArgs |
|
|
|
|
| def load_model(repo_dir: str | Path, device: Optional[str] = None): |
| """Load an Aura checkpoint from a repo directory. |
| |
| Looks for (in order): model.safetensors, model.pt. Reads config from the |
| checkpoint payload (model.pt) or from config.json (safetensors path). |
| |
| Returns (model, tokenizer, config). Model is in eval mode on `device`. |
| """ |
| repo_dir = Path(repo_dir) |
| if device is None: |
| device = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
| tokenizer_path = repo_dir / "tokenizer.json" |
| if not tokenizer_path.is_file(): |
| raise FileNotFoundError(f"tokenizer.json not found in {repo_dir}") |
| tokenizer = Tokenizer.from_file(str(tokenizer_path)) |
|
|
| st_path = repo_dir / "model.safetensors" |
| pt_path = repo_dir / "model.pt" |
|
|
| if st_path.is_file(): |
| from safetensors.torch import load_file |
| sd = load_file(str(st_path)) |
| cfg_path = repo_dir / "config.json" |
| if not cfg_path.is_file(): |
| raise FileNotFoundError( |
| f"model.safetensors present but config.json missing in {repo_dir}; " |
| f"cannot reconstruct ModelArgs." |
| ) |
| cfg_dict = json.loads(cfg_path.read_text()) |
| cfg_dict = {k: v for k, v in cfg_dict.items() if not k.startswith("_")} |
| config = ModelArgs(**cfg_dict) |
| elif pt_path.is_file(): |
| ckpt = torch.load(pt_path, map_location="cpu", weights_only=False) |
| sd = ckpt["model"] |
| config = ckpt["config"] |
| else: |
| raise FileNotFoundError( |
| f"Neither model.safetensors nor model.pt found in {repo_dir}" |
| ) |
|
|
| |
| sd = {k.replace("module.", "").replace("_orig_mod.", ""): v for k, v in sd.items()} |
|
|
| |
| sample = next(v for v in sd.values() if v.is_floating_point()) |
| dtype = sample.dtype |
|
|
| model = LlamaTransformer(config) |
| model.load_state_dict(sd) |
| model = model.to(device=device, dtype=dtype) |
| model.eval() |
| return model, tokenizer, config |
|
|
|
|
| @torch.no_grad() |
| def generate( |
| model, |
| tokenizer, |
| prompt: str, |
| max_new_tokens: int = 128, |
| temperature: float = 0.8, |
| top_p: float = 0.9, |
| num_sequences: int = 1, |
| seed: int = 42, |
| device: Optional[str] = None, |
| ) -> list[str]: |
| """Sample completions from `model` given `prompt`. Returns decoded strings.""" |
| if device is None: |
| device = next(model.parameters()).device.type |
| autocast_dtype = next(model.parameters()).dtype |
| if autocast_dtype not in (torch.float16, torch.bfloat16): |
| autocast_dtype = torch.bfloat16 |
|
|
| ids = tokenizer.encode(prompt).ids |
| x = torch.tensor(ids, dtype=torch.long, device=device).unsqueeze(0).repeat(num_sequences, 1) |
|
|
| eos_id = tokenizer.token_to_id("</s>") |
| rng = torch.Generator(device=device).manual_seed(seed) |
| finished = torch.zeros(num_sequences, dtype=torch.bool, device=device) |
| initial_len = x.size(1) |
|
|
| for _ in range(max_new_tokens): |
| with torch.autocast(device_type=str(device).split(":")[0], dtype=autocast_dtype): |
| logits = model(x) |
| next_logits = logits[:, -1, :] / max(temperature, 1e-5) |
|
|
| |
| probs = F.softmax(next_logits, dim=-1) |
| probs_sort, probs_idx = torch.sort(probs, descending=True, dim=-1) |
| cumprobs = torch.cumsum(probs_sort, dim=-1) |
| mask = cumprobs - probs_sort > top_p |
| probs_sort = probs_sort.masked_fill(mask, 0.0) |
| probs_sort = probs_sort / probs_sort.sum(dim=-1, keepdim=True) |
| ix_sorted = torch.multinomial(probs_sort, num_samples=1, generator=rng) |
| ix = torch.gather(probs_idx, -1, ix_sorted) |
|
|
| |
| if eos_id is not None: |
| ix[finished] = eos_id |
| x = torch.cat([x, ix], dim=1) |
| if eos_id is not None: |
| finished = finished | (ix.squeeze(-1) == eos_id) |
| if finished.all(): |
| break |
|
|
| out = [] |
| for i in range(num_sequences): |
| ids_i = x[i].tolist() |
| if eos_id is not None and eos_id in ids_i[initial_len:]: |
| cut = initial_len + ids_i[initial_len:].index(eos_id) |
| ids_i = ids_i[:cut] |
| out.append(tokenizer.decode(ids_i)) |
| return out |
|
|
|
|
| |
| import json |
|
|