arthu1 commited on
Commit
78ec42e
·
verified ·
1 Parent(s): 3202cb1

Add ready-to-run ChatML inference CLI

Browse files
Files changed (2) hide show
  1. README.md +7 -1
  2. inference.py +121 -0
README.md CHANGED
@@ -64,7 +64,13 @@ This remains a small research model. It is unreliable for multi-step arithmetic,
64
 
65
  ```bash
66
  pip install -r requirements.txt
67
- python infer.py --checkpoint model.safetensors --prompt "<|im_start|>user\nWhat is Python?<|im_end|>\n<|im_start|>assistant\n"
 
 
 
 
 
 
68
  ```
69
 
70
  ## Distribution
 
64
 
65
  ```bash
66
  pip install -r requirements.txt
67
+ python inference.py --prompt "What is Python?"
68
+ ```
69
+
70
+ Omit `--prompt` to start an interactive chat:
71
+
72
+ ```bash
73
+ python inference.py
74
  ```
75
 
76
  ## Distribution
inference.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Run Aurora Proelia ChatML locally or start an interactive chat."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+
9
+ import torch
10
+ from safetensors.torch import load_file
11
+ from tokenizers import Tokenizer
12
+
13
+ from aurora.config import load_model_config
14
+ from aurora.model import AuroraForCausalLM
15
+
16
+
17
+ DEFAULT_SYSTEM = (
18
+ "You are Ember Proelia, a proprietary language model created by North ML. "
19
+ "Answer directly and concisely. Do not claim web access or certainty you do not have."
20
+ )
21
+
22
+
23
+ def render_chat(messages: list[dict[str, str]], add_generation_prompt: bool = True) -> str:
24
+ text = "".join(
25
+ f"<|im_start|>{item['role']}\n{item['content'].strip()}<|im_end|>\n"
26
+ for item in messages
27
+ )
28
+ if add_generation_prompt:
29
+ text += "<|im_start|>assistant\n"
30
+ return text
31
+
32
+
33
+ def choose_device(value: str) -> torch.device:
34
+ if value != "auto":
35
+ return torch.device(value)
36
+ if torch.cuda.is_available():
37
+ return torch.device("cuda")
38
+ if torch.backends.mps.is_available():
39
+ return torch.device("mps")
40
+ return torch.device("cpu")
41
+
42
+
43
+ def load_model(root: Path, device: torch.device):
44
+ config = load_model_config(root / "model_ember_proelia_207m_16k.yaml")
45
+ tokenizer = Tokenizer.from_file(str(root / "tokenizer.json"))
46
+ dtype = torch.float16 if device.type in {"cuda", "mps"} else torch.float32
47
+ model = AuroraForCausalLM(config).to(device=device, dtype=dtype).eval()
48
+ state = load_file(str(root / "model.safetensors"), device=str(device))
49
+ missing, unexpected = model.load_state_dict(state, strict=False)
50
+ missing = [name for name in missing if not name.endswith("._extra_state")]
51
+ if missing or unexpected:
52
+ raise RuntimeError(f"checkpoint mismatch: missing={missing}, unexpected={unexpected}")
53
+ return model, tokenizer, config
54
+
55
+
56
+ def generate(model, tokenizer: Tokenizer, config, prompt: str, device: torch.device, max_new_tokens: int) -> str:
57
+ bos = tokenizer.token_to_id("<bos>")
58
+ eos = tokenizer.token_to_id("<eos>")
59
+ ids = [bos, *tokenizer.encode(prompt, add_special_tokens=False).ids]
60
+ generated: list[int] = []
61
+ with torch.inference_mode():
62
+ for _ in range(max_new_tokens):
63
+ inputs = torch.tensor([ids[-int(config.context_length):]], dtype=torch.long, device=device)
64
+ logits, _ = model(inputs)
65
+ token = int(torch.argmax(logits[0, -1]).item())
66
+ if token == eos:
67
+ break
68
+ generated.append(token)
69
+ ids.append(token)
70
+ text = tokenizer.decode(generated, skip_special_tokens=True)
71
+ if "<|im_end|>" in text or "<|im_start|>" in text:
72
+ break
73
+ text = tokenizer.decode(generated, skip_special_tokens=True).strip()
74
+ for marker in ("<|im_end|>", "<|im_start|>"):
75
+ if marker in text:
76
+ text = text.split(marker, 1)[0].strip()
77
+ return text
78
+
79
+
80
+ def main() -> None:
81
+ parser = argparse.ArgumentParser(description="Aurora Proelia ChatML inference")
82
+ parser.add_argument("--prompt", help="one prompt; omit for interactive chat")
83
+ parser.add_argument("--system", default=DEFAULT_SYSTEM)
84
+ parser.add_argument("--checkpoint-dir", type=Path, default=Path(__file__).resolve().parent)
85
+ parser.add_argument("--device", default="auto", choices=("auto", "cpu", "cuda", "mps"))
86
+ parser.add_argument("--max-new-tokens", type=int, default=96)
87
+ args = parser.parse_args()
88
+ device = choose_device(args.device)
89
+ model, tokenizer, config = load_model(args.checkpoint_dir, device)
90
+ history: list[dict[str, str]] = [{"role": "system", "content": args.system}]
91
+
92
+ def answer(user_text: str) -> str:
93
+ history.append({"role": "user", "content": user_text})
94
+ prompt = render_chat(history)
95
+ response = generate(model, tokenizer, config, prompt, device, args.max_new_tokens)
96
+ history.append({"role": "assistant", "content": response})
97
+ return response
98
+
99
+ if args.prompt:
100
+ print(answer(args.prompt))
101
+ return
102
+ print(f"Aurora Proelia ChatML · device={device}")
103
+ print("Type /quit to exit, /clear to reset the conversation.")
104
+ while True:
105
+ try:
106
+ user_text = input("You: ").strip()
107
+ except (EOFError, KeyboardInterrupt):
108
+ print()
109
+ break
110
+ if user_text == "/quit":
111
+ break
112
+ if user_text == "/clear":
113
+ history[:] = [{"role": "system", "content": args.system}]
114
+ print("Conversation cleared.")
115
+ continue
116
+ if user_text:
117
+ print(f"Aurora: {answer(user_text)}")
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()