minimind/scripts/inference.py

243 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import argparse
import os
import sys
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_root)
from typing import List, Optional, Tuple
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from accelerate import Accelerator
from peft import PeftModel
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from model.model_lora import apply_lora, load_lora
def parse_args():
parser = argparse.ArgumentParser(
description="Universal inference: CPU, single-GPU, single-node multi-GPU"
)
parser.add_argument("--model-path", default="/mnt/share/djx/minimind/out", help="Path or HF repo id of the model")
parser.add_argument("--dtype", default="auto", choices=["auto", "fp32", "fp16", "bf16"], help="Model dtype")
parser.add_argument("--device", default="auto", choices=["auto", "cpu", "cuda"], help="Select device kind")
parser.add_argument("--num-gpus", type=int, default=0, help="Number of GPUs to use; 0 means CPU. If >0 and device=cuda, uses device_map=auto")
parser.add_argument("--prompt", default=None, help="Prompt text; if omitted, read from stdin or --input-file")
parser.add_argument("--input-file", default=None, help="Path to a text file; one prompt per line. Ignored if --prompt provided.")
parser.add_argument("--output-file", default=None, help="Write generations to file. When --input-file is set, outputs JSONL with fields {prompt, generation}.")
parser.add_argument("--max-new-tokens", type=int, default=128, help="Max new tokens to generate")
parser.add_argument("--temperature", type=float, default=0.7, help="Sampling temperature")
parser.add_argument("--top_p", type=float, default=0.9, help="Top-p nucleus sampling")
parser.add_argument("--do-sample", action="store_true", help="Enable sampling; by default greedy if not set")
parser.add_argument("--batch-size", type=int, default=1, help="Batch size for parallel generation. Effective on GPU and multi-GPU with device_map=auto.")
parser.add_argument("--use-lora", action="store_true", help="Enable LoRA adapter loading (requires PEFT)")
parser.add_argument("--lora-path", default=None, help="Path to LoRA adapter weights (PEFT). If not set, --use-lora is ignored.")
# Align with eval_llm.py for native MiniMind loading
parser.add_argument('--save-dir', default='out', type=str, help="Model weights directory (native MiniMind)")
parser.add_argument('--weight', default='full_sft', type=str, help="Weight prefix (pretrain, full_sft, rlhf, reason, ppo_actor, grpo, spo)")
parser.add_argument('--lora-weight', default='None', type=str, help="LoRA weight name for native MiniMind (None to disable)")
parser.add_argument('--hidden-size', default=512, type=int, help="Hidden size (512=Small-26M, 640=MoE-145M, 768=Base-104M)")
parser.add_argument('--num-hidden-layers', default=8, type=int, help="Number of hidden layers (Small/MoE=8, Base=16)")
parser.add_argument('--use-moe', default=0, type=int, choices=[0, 1], help="Use MoE architecture (0/1)")
return parser.parse_args()
def select_dtype(dtype_str: str):
if dtype_str == "auto":
return None
if dtype_str == "fp32":
return torch.float32
if dtype_str == "fp16":
return torch.float16
if dtype_str == "bf16":
return torch.bfloat16 if hasattr(torch, "bfloat16") else torch.float16
return None
def build_device_map(device: str, num_gpus: int):
if device == "cpu" or (device == "auto" and not torch.cuda.is_available()):
return None # no device_map; model stays on CPU
if num_gpus <= 1:
return {"": 0} # place all on cuda:0
# multi-GPU: let transformers shard automatically across available GPUs
return "auto"
def load_model_and_tokenizer(args, dtype_opt, device_map_opt, accelerator: Optional[Accelerator] = None):
# Align behavior with eval_llm.py
tok = AutoTokenizer.from_pretrained(args.model_path, use_fast=True)
# Native MiniMind path if 'model' in path
if 'out' in args.model_path:
print("Loading native MiniMind model...")
model = MiniMindForCausalLM(MiniMindConfig(
hidden_size=args.hidden_size,
num_hidden_layers=args.num_hidden_layers,
use_moe=bool(args.use_moe),
))
moe_suffix = '_moe' if args.use_moe else ''
ckp = f'{args.model_path}/{args.weight}_{args.hidden_size}{moe_suffix}.pth'
state = torch.load(ckp, map_location=args.device)
model.load_state_dict(state, strict=True)
if args.lora_weight != 'None':
apply_lora(model)
load_lora(model, f'./{args.save_dir}/lora/{args.lora_weight}_{args.hidden_size}.pth')
else:
kwargs = {}
if dtype_opt is not None:
kwargs["torch_dtype"] = dtype_opt
if device_map_opt is not None:
kwargs["device_map"] = device_map_opt
kwargs["low_cpu_mem_usage"] = True
model = AutoModelForCausalLM.from_pretrained(args.model_path, trust_remote_code=True, **kwargs)
# PEFT LoRA only if proper adapter folder provided
if args.use_lora and args.lora_path:
try:
model = PeftModel.from_pretrained(model, args.lora_path)
except Exception as e:
raise RuntimeError(f"Failed to load PEFT LoRA at '{args.lora_path}': {e}")
if accelerator is not None:
model = accelerator.prepare(model)
model.eval()
return model, tok
def get_prompts(args) -> List[str]:
if args.prompt is not None:
return [args.prompt]
if args.input_file:
with open(args.input_file, "r", encoding="utf-8") as f:
lines = [ln.strip() for ln in f.readlines()]
return [ln for ln in lines if ln]
# read from stdin (single prompt)
print("Enter prompt. Press Ctrl-D to finish:")
return [sys.stdin.read().strip()]
def main():
args = parse_args()
if args.device == "cpu" or (args.device == "auto" and not torch.cuda.is_available()):
device = "cpu"
else:
device = "cuda"
# Optional Accelerate setup
accelerator: Optional[Accelerator] = None
if device == "cuda" and args.num_gpus > 1:
accelerator = Accelerator()
dtype = select_dtype(args.dtype)
device_map = build_device_map(device, args.num_gpus)
model, tok = load_model_and_tokenizer(
args,
dtype,
device_map,
accelerator,
)
if accelerator is None and device == "cuda" and device_map in (None, {"": 0}):
# single-GPU: move to cuda:0 if not already via device_map
model.to("cuda")
prompts = get_prompts(args)
gen_kwargs = {
"max_new_tokens": args.max_new_tokens,
"temperature": args.temperature,
"top_p": args.top_p,
"do_sample": args.do_sample,
}
if device == "cuda":
try:
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
except Exception:
pass
outputs: List[str] = []
bs = max(1, args.batch_size)
if accelerator is None:
# Single-process path with optional multi-GPU device_map
for i in range(0, len(prompts), bs):
batch_prompts = prompts[i:i+bs]
inputs = tok(batch_prompts, return_tensors="pt", padding=True, truncation=False)
# Remove unused keys (e.g., token_type_ids) to avoid generate() validation errors
inputs.pop("token_type_ids", None)
if device == "cuda" and device_map in (None, {"": 0}):
inputs = {k: v.to("cuda") for k, v in inputs.items()}
with torch.inference_mode():
out = model.generate(**inputs, **gen_kwargs)
for j, seq in enumerate(out):
text = tok.decode(seq, skip_special_tokens=True)
prompt_j = batch_prompts[j]
gen = text[len(prompt_j):] if text.startswith(prompt_j) else text
outputs.append(gen)
else:
# Accelerate multi-process path: shard prompts, gather results, keep stable order
all_items: List[Tuple[int, str]] = [(i, p) for i, p in enumerate(prompts)]
local_items = [it for idx, it in enumerate(all_items) if idx % accelerator.num_processes == accelerator.process_index]
local_results: List[Tuple[int, str]] = []
for i in range(0, len(local_items), bs):
batch = local_items[i:i+bs]
idxs = [it[0] for it in batch]
batch_prompts = [it[1] for it in batch]
inputs = tok(batch_prompts, return_tensors="pt", padding=True)
inputs.pop("token_type_ids", None)
inputs = {k: v.to(model.device) for k, v in inputs.items()}
with torch.inference_mode():
out = model.generate(**inputs, **gen_kwargs)
for j, seq in enumerate(out):
text = tok.decode(seq, skip_special_tokens=True)
prompt_j = batch_prompts[j]
gen = text[len(prompt_j):] if text.startswith(prompt_j) else text
local_results.append((idxs[j], gen))
gathered = accelerator.gather(local_results)
if accelerator.is_main_process:
# restore order
gathered_sorted = sorted(gathered, key=lambda x: x[0])
outputs = [gen for _, gen in gathered_sorted]
else:
return
if args.output_file:
# If multiple prompts, write JSONL; else write plain text
import json
dirpath = os.path.dirname(args.output_file)
if dirpath:
os.makedirs(dirpath, exist_ok=True)
with open(args.output_file, "w", encoding="utf-8") as f:
if len(prompts) > 1:
for p, g in zip(prompts, outputs):
f.write(json.dumps({"prompt": p, "generation": g}, ensure_ascii=False) + "\n")
else:
f.write(outputs[0])
else:
if len(outputs) == 1:
print(outputs[0])
else:
for p, g in zip(prompts, outputs):
print("===== PROMPT =====")
print(p)
print("=== GENERATION ===")
print(g)
if __name__ == "__main__":
main()
# python scripts/inference.py \
# --model-path /mnt/share/djx/minimind/out/full_sft_512.pth \
# --save-dir out \
# --weight full_sft \
# --hidden-size 512 \
# --num-hidden-layers 8 \
# --use-moe 0 \
# --device cuda --num-gpus 1 \
# --prompt "你好,介绍一下LoRA是什么?"