ObsidianSmall-Base / generate.py
Metris's picture
Upload 5 files
e0a700d verified
Raw
History Blame Contribute Delete
7.77 kB
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
from typing import Any
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Generate text with ObsidianSmall-Base using "
"full-sequence Multiscreen decoding."
)
)
parser.add_argument(
"--model-dir",
type=Path,
default=Path(__file__).resolve().parent,
)
parser.add_argument("--prompt", type=str)
parser.add_argument("--prompt-file", type=Path)
parser.add_argument("--interactive", action="store_true")
parser.add_argument(
"--backend",
choices=("auto", "torch", "triton"),
default="auto",
)
parser.add_argument(
"--device",
choices=("auto", "cpu", "cuda"),
default="auto",
)
parser.add_argument(
"--dtype",
choices=("auto", "float32", "float16", "bfloat16"),
default="auto",
)
parser.add_argument("--max-new-tokens", type=int, default=128)
parser.add_argument("--temperature", type=float, default=0.8)
parser.add_argument("--top-k", type=int, default=40)
parser.add_argument("--top-p", type=float, default=1.0)
parser.add_argument("--seed", type=int, default=1337)
return parser.parse_args()
def extract_state_dict(payload: Any, torch: Any) -> dict[str, Any]:
if isinstance(payload, dict):
for container in ("model", "state_dict", "model_state_dict"):
candidate = payload.get(container)
if isinstance(candidate, dict):
payload = candidate
break
if not isinstance(payload, dict):
raise TypeError(
f"Unsupported checkpoint type: {type(payload).__name__}"
)
state_dict: dict[str, Any] = {}
for original_name, value in payload.items():
if not torch.is_tensor(value):
continue
name = str(original_name)
for prefix in ("_orig_mod.", "module.", "model."):
while name.startswith(prefix):
name = name[len(prefix):]
state_dict[name] = value
if not state_dict:
raise RuntimeError("No model tensors were found")
return state_dict
def sample_token(
logits: Any,
torch: Any,
temperature: float,
top_k: int,
top_p: float,
) -> Any:
if temperature <= 0:
return logits.argmax(dim=-1, keepdim=True)
logits = logits / temperature
if top_k > 0:
k = min(top_k, logits.size(-1))
threshold = torch.topk(logits, k).values[:, -1:]
logits = logits.masked_fill(
logits < threshold,
float("-inf"),
)
if 0.0 < top_p < 1.0:
sorted_logits, sorted_indices = torch.sort(
logits,
descending=True,
)
sorted_probs = torch.softmax(sorted_logits, dim=-1)
cumulative_probs = sorted_probs.cumsum(dim=-1)
remove = cumulative_probs > top_p
remove[:, 1:] = remove[:, :-1].clone()
remove[:, 0] = False
sorted_logits = sorted_logits.masked_fill(
remove,
float("-inf"),
)
filtered = torch.full_like(
logits,
float("-inf"),
)
logits = filtered.scatter(
dim=-1,
index=sorted_indices,
src=sorted_logits,
)
probabilities = torch.softmax(logits, dim=-1)
return torch.multinomial(
probabilities,
num_samples=1,
)
def resolve_prompt(args: argparse.Namespace) -> str:
if args.prompt is not None:
return args.prompt
if args.prompt_file is not None:
return args.prompt_file.read_text(encoding="utf-8")
raise SystemExit(
"Provide --prompt, --prompt-file, or --interactive."
)
def main() -> None:
args = parse_args()
model_dir = args.model_dir.resolve()
os.environ["MULTISCREEN_BACKEND"] = args.backend
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
sys.path.insert(0, str(model_dir / "runtime"))
import torch
from litgpt import Config, GPT, Tokenizer
if args.device == "auto":
device = torch.device(
"cuda" if torch.cuda.is_available() else "cpu"
)
else:
device = torch.device(args.device)
if args.backend == "triton" and device.type != "cuda":
raise RuntimeError("The Triton backend requires CUDA")
if args.dtype == "auto":
dtype = (
torch.bfloat16
if device.type == "cuda"
else torch.float32
)
else:
dtype = {
"float32": torch.float32,
"float16": torch.float16,
"bfloat16": torch.bfloat16,
}[args.dtype]
torch.manual_seed(args.seed)
if device.type == "cuda":
torch.cuda.manual_seed_all(args.seed)
config = Config.from_file(
model_dir / "model_config.yaml"
)
model = GPT(config)
try:
checkpoint = torch.load(
model_dir / "lit_model.pth",
map_location="cpu",
weights_only=False,
)
except TypeError:
checkpoint = torch.load(
model_dir / "lit_model.pth",
map_location="cpu",
)
state_dict = extract_state_dict(checkpoint, torch)
incompatible = model.load_state_dict(
state_dict,
strict=False,
)
if incompatible.missing_keys or incompatible.unexpected_keys:
raise RuntimeError(
"Checkpoint mismatch:\n"
f"Missing: {incompatible.missing_keys}\n"
f"Unexpected: {incompatible.unexpected_keys}"
)
model = model.to(
device=device,
dtype=dtype,
)
model.eval()
tokenizer = Tokenizer(model_dir)
print(f"Device: {device}")
print(f"Backend: {args.backend}")
print(f"Precision: {dtype}")
print(
"Decoding: full sequence "
"(incremental KV cache is not used)"
)
print()
def complete(prompt: str) -> str:
tokens = tokenizer.encode(
prompt,
device=device,
).reshape(1, -1).long()
with torch.inference_mode():
for _ in range(args.max_new_tokens):
context = tokens[:, -config.block_size:]
output = model(context)
logits = (
output[0]
if isinstance(output, tuple)
else output
)
next_token = sample_token(
logits[:, -1, :].float(),
torch=torch,
temperature=args.temperature,
top_k=args.top_k,
top_p=args.top_p,
)
tokens = torch.cat(
(tokens, next_token),
dim=1,
)
if (
tokenizer.eos_id is not None
and next_token.item() == tokenizer.eos_id
):
break
return tokenizer.decode(
tokens[0].detach().cpu()
)
if args.interactive:
print("Enter a prompt. Use Ctrl+D or /quit to exit.")
while True:
try:
prompt = input("\nPrompt> ")
except EOFError:
print()
break
if prompt.strip().lower() in {"/quit", "/exit"}:
break
if not prompt.strip():
continue
print("\n" + complete(prompt))
else:
print(complete(resolve_prompt(args)))
if __name__ == "__main__":
main()