Arithmetic-SLM / inference.py
PhysiQuanty's picture
Create inference.py
b85c5c9 verified
Raw
History Blame Contribute Delete
16 kB
#!/usr/bin/env python3
import argparse
import random
from typing import Dict, List, Optional, Set
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
IM_START = "[IM_START]"
IM_END = "[IM_END]"
NO_THINK = "/no think"
# ============================================================
# UTILS
# ============================================================
def get_dtype(name: str):
name = str(name).lower()
if name in {"bf16", "bfloat16"}:
return torch.bfloat16
if name in {"fp16", "float16", "half"}:
return torch.float16
if name in {"fp32", "float32", "float"}:
return torch.float32
raise ValueError(f"Unknown dtype: {name}")
def set_seed(seed: int):
if seed is None:
return
if seed < 0:
seed = random.randint(0, 2**31 - 1)
print(f"[INFO] random seed: {seed}")
random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
def build_prompt(args) -> str:
if args.no_think:
return (
f"{IM_START}user\n"
f"{args.prompt} {NO_THINK}"
f"{IM_END}\n"
f"{IM_START}assistant\n"
"<think>\n</think>\n"
)
return args.prompt
def decode(tokenizer, ids: List[int]) -> str:
return tokenizer.decode(ids, skip_special_tokens=False)
def extract_completion(full_text: str, prompt: str) -> str:
if full_text.startswith(prompt):
return full_text[len(prompt):]
pos = full_text.rfind(prompt)
if pos != -1:
return full_text[pos + len(prompt):]
return full_text
def strip_after_stop_text(text: str, stop_strings: List[str]) -> str:
best = None
for s in stop_strings:
if not s:
continue
pos = text.find(s)
if pos != -1:
if best is None or pos < best:
best = pos
if best is None:
return text
return text[:best]
def build_stop_sequences(tokenizer, stop_strings: List[str]) -> List[List[int]]:
out = []
for s in stop_strings:
ids = tokenizer.encode(s, add_special_tokens=False)
if ids:
out.append(ids)
return out
def endswith_sequence(ids: List[int], suffix: List[int]) -> bool:
if not suffix:
return False
if len(ids) < len(suffix):
return False
return ids[-len(suffix):] == suffix
# ============================================================
# SAMPLING
# ============================================================
def apply_repetition_penalty(
logits: torch.Tensor,
generated_ids: List[int],
penalty: float,
) -> torch.Tensor:
if penalty is None or penalty == 1.0:
return logits
if penalty <= 0:
raise ValueError("--repetition-penalty must be > 0")
for tid in set(generated_ids):
if tid < 0 or tid >= logits.numel():
continue
if logits[tid] > 0:
logits[tid] = logits[tid] / penalty
else:
logits[tid] = logits[tid] * penalty
return logits
def apply_frequency_presence_penalty(
logits: torch.Tensor,
generated_ids: List[int],
frequency_penalty: float,
presence_penalty: float,
) -> torch.Tensor:
if not generated_ids:
return logits
if frequency_penalty == 0.0 and presence_penalty == 0.0:
return logits
counts: Dict[int, int] = {}
for tid in generated_ids:
counts[tid] = counts.get(tid, 0) + 1
for tid, count in counts.items():
if tid < 0 or tid >= logits.numel():
continue
if frequency_penalty:
logits[tid] -= frequency_penalty * count
if presence_penalty:
logits[tid] -= presence_penalty
return logits
def get_banned_ngram_tokens(
generated_ids: List[int],
no_repeat_ngram_size: int,
) -> Set[int]:
n = no_repeat_ngram_size
banned = set()
if n <= 0:
return banned
if len(generated_ids) + 1 < n:
return banned
prefix_len = n - 1
current_prefix = tuple(generated_ids[-prefix_len:])
ngram_map = {}
for i in range(len(generated_ids) - n + 1):
prefix = tuple(generated_ids[i:i + prefix_len])
next_token = generated_ids[i + prefix_len]
if prefix not in ngram_map:
ngram_map[prefix] = set()
ngram_map[prefix].add(next_token)
banned.update(ngram_map.get(current_prefix, set()))
return banned
def apply_no_repeat_ngram(
logits: torch.Tensor,
generated_ids: List[int],
no_repeat_ngram_size: int,
) -> torch.Tensor:
if no_repeat_ngram_size <= 0:
return logits
banned = get_banned_ngram_tokens(
generated_ids=generated_ids,
no_repeat_ngram_size=no_repeat_ngram_size,
)
for tid in banned:
if 0 <= tid < logits.numel():
logits[tid] = -float("inf")
return logits
def apply_top_k(logits: torch.Tensor, top_k: int) -> torch.Tensor:
if top_k is None or top_k <= 0:
return logits
top_k = min(top_k, logits.size(-1))
values, _ = torch.topk(logits, top_k)
cutoff = values[-1]
logits[logits < cutoff] = -float("inf")
return logits
def apply_top_p(logits: torch.Tensor, top_p: float) -> torch.Tensor:
if top_p is None or top_p >= 1.0:
return logits
if top_p <= 0:
raise ValueError("--top-p must be > 0")
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
sorted_probs = F.softmax(sorted_logits, dim=-1)
cumulative = torch.cumsum(sorted_probs, dim=-1)
remove = cumulative > top_p
remove[1:] = remove[:-1].clone()
remove[0] = False
indices_to_remove = sorted_indices[remove]
logits[indices_to_remove] = -float("inf")
return logits
def apply_min_p(logits: torch.Tensor, min_p: float) -> torch.Tensor:
if min_p is None or min_p <= 0:
return logits
probs = F.softmax(logits, dim=-1)
max_prob = torch.max(probs)
keep = probs >= (min_p * max_prob)
logits[~keep] = -float("inf")
return logits
def apply_typical_p(logits: torch.Tensor, typical_p: float) -> torch.Tensor:
if typical_p is None or typical_p >= 1.0:
return logits
if typical_p <= 0:
raise ValueError("--typical-p must be > 0")
probs = F.softmax(logits, dim=-1)
log_probs = F.log_softmax(logits, dim=-1)
entropy = -(probs * log_probs).sum()
shifted_scores = torch.abs((-log_probs) - entropy)
sorted_scores, sorted_indices = torch.sort(shifted_scores, descending=False)
sorted_probs = probs[sorted_indices]
cumulative_probs = torch.cumsum(sorted_probs, dim=-1)
remove = cumulative_probs > typical_p
remove[1:] = remove[:-1].clone()
remove[0] = False
indices_to_remove = sorted_indices[remove]
logits[indices_to_remove] = -float("inf")
return logits
def apply_bad_words(
logits: torch.Tensor,
tokenizer,
bad_words: List[str],
):
for word in bad_words:
ids = tokenizer.encode(word, add_special_tokens=False)
if len(ids) == 1:
tid = ids[0]
if 0 <= tid < logits.numel():
logits[tid] = -float("inf")
return logits
def sample_next_token(
logits: torch.Tensor,
generated_ids: List[int],
tokenizer,
args,
) -> int:
logits = logits.float().clone()
logits = apply_bad_words(
logits=logits,
tokenizer=tokenizer,
bad_words=args.bad_words,
)
logits = apply_repetition_penalty(
logits=logits,
generated_ids=generated_ids,
penalty=args.repetition_penalty,
)
logits = apply_frequency_presence_penalty(
logits=logits,
generated_ids=generated_ids,
frequency_penalty=args.frequency_penalty,
presence_penalty=args.presence_penalty,
)
logits = apply_no_repeat_ngram(
logits=logits,
generated_ids=generated_ids,
no_repeat_ngram_size=args.no_repeat_ngram_size,
)
if args.temperature <= 0:
return int(torch.argmax(logits).item())
logits = logits / args.temperature
logits = apply_top_k(logits, args.top_k)
logits = apply_top_p(logits, args.top_p)
logits = apply_min_p(logits, args.min_p)
logits = apply_typical_p(logits, args.typical_p)
probs = F.softmax(logits, dim=-1)
if torch.isnan(probs).any() or torch.isinf(probs).any() or probs.sum() <= 0:
return int(torch.argmax(logits).item())
return int(torch.multinomial(probs, num_samples=1).item())
# ============================================================
# MODEL
# ============================================================
def model_forward_logits(model, input_ids: torch.Tensor):
out = model(input_ids=input_ids)
if hasattr(out, "logits"):
return out.logits
if isinstance(out, tuple):
return out[0]
raise RuntimeError("Impossible de récupérer logits depuis la sortie du modèle.")
@torch.no_grad()
def generate_manual(
model,
tokenizer,
input_ids: torch.Tensor,
args,
) -> torch.Tensor:
idx = input_ids
generated_after_prompt: List[int] = []
stop_sequences = build_stop_sequences(
tokenizer,
stop_strings=args.stop_strings,
)
eos_id = tokenizer.eos_token_id
for step in range(args.max_new_tokens):
idx_cond = idx[:, -args.ctx_len:] if args.ctx_len > 0 else idx
logits = model_forward_logits(model, idx_cond)
logits = logits[:, -1, :][0]
if step < args.min_new_tokens:
if eos_id is not None and 0 <= eos_id < logits.numel():
logits[eos_id] = -float("inf")
for seq in stop_sequences:
if len(seq) == 1:
tid = seq[0]
if 0 <= tid < logits.numel():
logits[tid] = -float("inf")
next_id = sample_next_token(
logits=logits,
generated_ids=generated_after_prompt,
tokenizer=tokenizer,
args=args,
)
next_tensor = torch.tensor(
[[next_id]],
dtype=torch.long,
device=idx.device,
)
idx = torch.cat([idx, next_tensor], dim=1)
generated_after_prompt.append(next_id)
full_ids = idx[0].tolist()
if step >= args.min_new_tokens:
if eos_id is not None and next_id == eos_id:
break
should_stop = False
for seq in stop_sequences:
if endswith_sequence(full_ids, seq):
should_stop = True
break
if should_stop:
break
return idx
# ============================================================
# CLI
# ============================================================
def parse_args():
p = argparse.ArgumentParser(
description="Inference script for Arithmetic-SLM using [IM_START]/[IM_END]."
)
p.add_argument(
"--model",
default="PhysiQuanty/Arithmetic-SLM",
help="HF model id or local path.",
)
p.add_argument(
"--prompt",
default="59 + 45 =",
help="Raw arithmetic prompt. With --no-think, inserted in chat template.",
)
p.add_argument(
"--no-think",
action="store_true",
help="Use production no-think template with [IM_START]/[IM_END].",
)
p.add_argument(
"--device",
default="cuda" if torch.cuda.is_available() else "cpu",
)
p.add_argument(
"--dtype",
default="bfloat16",
choices=["bfloat16", "bf16", "float16", "fp16", "float32", "fp32"],
)
p.add_argument("--ctx-len", type=int, default=2048)
p.add_argument("--max-new-tokens", type=int, default=64)
p.add_argument("--min-new-tokens", type=int, default=1)
p.add_argument("--temperature", type=float, default=0.7)
p.add_argument("--top-k", type=int, default=40)
p.add_argument("--top-p", type=float, default=0.90)
p.add_argument("--min-p", type=float, default=0.0)
p.add_argument("--typical-p", type=float, default=1.0)
p.add_argument("--repetition-penalty", type=float, default=1.05)
p.add_argument("--frequency-penalty", type=float, default=0.10)
p.add_argument("--presence-penalty", type=float, default=0.0)
p.add_argument("--no-repeat-ngram-size", type=int, default=4)
p.add_argument("--seed", type=int, default=-1)
p.add_argument(
"--stop-string",
action="append",
default=None,
help="Additional stop string. Can be passed multiple times.",
)
p.add_argument(
"--bad-word",
action="append",
default=None,
help="Single-token word/token to ban. Can be passed multiple times.",
)
p.add_argument(
"--print-full",
action="store_true",
help="Print full prompt + completion.",
)
p.add_argument(
"--trust-remote-code",
action="store_true",
default=True,
)
args = p.parse_args()
if args.max_new_tokens <= 0:
raise ValueError("--max-new-tokens must be > 0")
if args.min_new_tokens < 0:
raise ValueError("--min-new-tokens must be >= 0")
if args.temperature < 0:
raise ValueError("--temperature must be >= 0")
if args.top_k < 0:
raise ValueError("--top-k must be >= 0")
if not (0 < args.top_p <= 1.0):
raise ValueError("--top-p must be in (0, 1]")
if args.min_p < 0:
raise ValueError("--min-p must be >= 0")
if not (0 < args.typical_p <= 1.0):
raise ValueError("--typical-p must be in (0, 1]")
if args.repetition_penalty <= 0:
raise ValueError("--repetition-penalty must be > 0")
if args.no_repeat_ngram_size < 0:
raise ValueError("--no-repeat-ngram-size must be >= 0")
stop_strings = [
IM_END,
IM_START,
]
if args.stop_string:
stop_strings.extend(args.stop_string)
args.stop_strings = stop_strings
bad_words = []
if args.bad_word:
bad_words.extend(args.bad_word)
args.bad_words = bad_words
return args
def main():
args = parse_args()
set_seed(args.seed)
dtype = get_dtype(args.dtype)
print(f"[INFO] model: {args.model}")
print(f"[INFO] device: {args.device}")
print(f"[INFO] dtype: {args.dtype}")
print(f"[INFO] template: {'no_think' if args.no_think else 'raw'}")
print(f"[INFO] IM_START: {IM_START}")
print(f"[INFO] IM_END: {IM_END}")
print(f"[INFO] NO_THINK: {NO_THINK}")
print(f"[INFO] temperature: {args.temperature}")
print(f"[INFO] top_k: {args.top_k}")
print(f"[INFO] top_p: {args.top_p}")
print(f"[INFO] min_p: {args.min_p}")
print(f"[INFO] typical_p: {args.typical_p}")
print()
tokenizer = AutoTokenizer.from_pretrained(
args.model,
trust_remote_code=args.trust_remote_code,
)
model = AutoModelForCausalLM.from_pretrained(
args.model,
dtype=dtype,
trust_remote_code=args.trust_remote_code,
).to(args.device)
model.eval()
prompt = build_prompt(args)
encoded = tokenizer(
prompt,
return_tensors="pt",
add_special_tokens=False,
)
encoded.pop("token_type_ids", None)
input_ids = encoded["input_ids"].to(args.device)
output_ids = generate_manual(
model=model,
tokenizer=tokenizer,
input_ids=input_ids,
args=args,
)
full_text = decode(tokenizer, output_ids[0].tolist())
if args.print_full:
print(full_text)
return
completion = extract_completion(full_text, prompt)
completion = strip_after_stop_text(completion, args.stop_strings)
print(completion)
if __name__ == "__main__":
main()