MOF-deprecated / script /infer_worker.py
StarLiu714's picture
Upload folder using huggingface_hub
24c3614 verified
Raw
History Blame Contribute Delete
15.5 kB
import os
import json
import math
import time
import csv
import logging
import argparse
from typing import List, Dict, Any, Tuple, Optional
import torch
import torch.nn as nn
from tqdm import tqdm
from transformers import AutoTokenizer, AutoModel
from peft import LoraConfig, get_peft_model
# Flash SDP (Optional)
torch.backends.cuda.enable_flash_sdp(True)
torch.backends.cuda.enable_mem_efficient_sdp(True)
torch.backends.cuda.enable_math_sdp(False)
# =========================
# logging
# =========================
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")
logger = logging.getLogger("infer_worker")
# =========================
# System Prompt
# =========================
SYSTEM_PROMPT = """
Act as an expert in reticular chemistry. You will receive reaction conditions as a JSON object with the fields: metal_precursor,
organic_linker, modulator, solvent, metal_concentration_mM, M_L_ratio, temperature_C, and time_h. Based on these inputs, output exactly one uppercase label:
'P' if the conditions are likely to yield a crystalline metal-organic framework under experimental conditions, or 'N' if not..
"""
# =========================
# Data Loading
# =========================
def load_messages_from_jsonl(path: str) -> List[List[Dict[str, Any]]]:
all_messages = []
with open(path, "r", encoding="utf-8-sig") as f:
for line in f:
line = line.strip()
if not line:
continue
data = json.loads(line)
if "messages" in data:
all_messages.append(data["messages"])
return all_messages
def standardize_json_input(json_string: str) -> str:
try:
data = json.loads(json_string)
keep = {
"metal_precursor": data.get("metal_precursor"),
"organic_linker": data.get("organic_linker"),
"modulator": data.get("modulator"),
"solvent": data.get("solvent"),
"metal_concentration_mM": data.get("metal_concentration_mM"),
"M_L_ratio": data.get("M_L_ratio"),
"temperature_C": data.get("temperature_C"),
"time_h": data.get("time_h"),
}
return json.dumps(keep, ensure_ascii=False)
except Exception:
return json_string
def char_norm(s: Optional[str]) -> Optional[str]:
s = (s or "").strip().upper()
return s if s in ("P", "N") else None
def standardize_messages(messages: List[Dict[str, Any]]) -> Tuple[List[Dict[str, str]], Optional[str]]:
user_content = None
assistant_content = None
for m in messages:
if m.get("role") == "user":
user_content = m.get("content")
elif m.get("role") == "assistant":
assistant_content = m.get("content")
user_content = standardize_json_input(user_content or "")
standardized = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_content},
]
gold = char_norm(assistant_content)
return standardized, gold
def get_user_before_last_assistant(messages):
if not messages:
return None, None
last_asst_idx = None
for i in range(len(messages) - 1, -1, -1):
if messages[i].get("role") == "assistant":
last_asst_idx = i
break
if last_asst_idx is None:
return None, None
gold_char = char_norm(messages[last_asst_idx].get("content"))
user_raw = None
for j in range(last_asst_idx - 1, -1, -1):
if messages[j].get("role") == "user":
user_raw = messages[j].get("content")
break
return user_raw, gold_char
# =========================
# Model
# =========================
def build_model_with_lora(base_model_name: str, cache_dir: str, device: torch.device):
lora_config = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["q_proj", "v_proj", "o_proj"],
lora_dropout=0.01,
bias="none",
task_type="SEQ_CLS"
)
backbone = AutoModel.from_pretrained(
base_model_name,
torch_dtype=torch.bfloat16,
device_map=None, # 强制单卡
cache_dir=cache_dir,
).to(device)
backbone = get_peft_model(backbone, lora_config)
class QwenWithLoRAForBinaryClassification(nn.Module):
def __init__(self, backbone):
super().__init__()
self.backbone = backbone
self.config = backbone.config
hidden_size = backbone.config.hidden_size
self.classifier = nn.Linear(hidden_size, 2)
def forward(self, input_ids, attention_mask=None, labels=None):
outputs = self.backbone(
input_ids=input_ids,
attention_mask=attention_mask,
return_dict=True,
)
mask = attention_mask.unsqueeze(-1)
pooled = (outputs.last_hidden_state * mask).sum(1) / mask.sum(1)
if (self.classifier.weight.device != pooled.device) or (self.classifier.weight.dtype != pooled.dtype):
self.classifier = self.classifier.to(device=pooled.device, dtype=pooled.dtype)
logits = self.classifier(pooled)
loss = None
if labels is not None:
loss = nn.CrossEntropyLoss()(logits, labels)
return {"loss": loss, "logits": logits}
return QwenWithLoRAForBinaryClassification(backbone).to(device)
def load_trained_weights(model: nn.Module, ckpt_dir: str):
candidates = [
os.path.join(ckpt_dir, "pytorch_model.bin"),
os.path.join(ckpt_dir, "model.safetensors"),
os.path.join(ckpt_dir, "adapter_model.bin"),
]
weight_path = None
for p in candidates:
if os.path.isfile(p):
weight_path = p
break
if weight_path is None:
raise FileNotFoundError(f"在 {ckpt_dir} 找不到权重文件")
logger.info(f"Loading weights from: {weight_path}")
if weight_path.endswith(".safetensors"):
from safetensors.torch import load_file
state = load_file(weight_path)
else:
state = torch.load(weight_path, map_location="cpu")
missing, unexpected = model.load_state_dict(state, strict=False)
logger.info(f"load_state_dict done. missing={len(missing)} unexpected={len(unexpected)}")
# =========================
# Inference (batch)
# =========================
@torch.inference_mode()
def predict_batch(
model,
tokenizer,
device: torch.device,
input_texts: List[str],
max_length: int,
):
enc = tokenizer(
input_texts,
truncation=True,
max_length=max_length,
padding="longest",
return_tensors="pt",
)
enc = {k: v.pin_memory().to(device, non_blocking=True) for k, v in enc.items()}
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
out = model(**enc)
logits = out["logits"] if isinstance(out, dict) else out.logits
probs = torch.softmax(logits, dim=-1)
prob_p = probs[:, 1].detach().float().cpu().tolist()
pred_int = probs.argmax(dim=-1).detach().cpu().tolist()
return pred_int, prob_p
# =========================
# progress (resume)
# =========================
def load_progress(progress_path: str) -> int:
if not os.path.exists(progress_path):
return -1
try:
with open(progress_path, "r", encoding="utf-8") as f:
obj = json.load(f)
return int(obj.get("last_done_index", -1))
except Exception:
return -1
def save_progress(progress_path: str, last_done_index: int):
tmp = progress_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump({"last_done_index": int(last_done_index)}, f, ensure_ascii=False)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, progress_path)
# =========================
# worker main
# =========================
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--base_model_name", default="openai")
ap.add_argument("--cache_dir", default=".")
ap.add_argument("--ckpt_dir", required=True)
ap.add_argument("--data_jsonl", required=True)
ap.add_argument("--out_dir", required=True)
ap.add_argument("--batch_size", type=int, default=64)
ap.add_argument("--max_length", type=int, default=512)
ap.add_argument("--num_shards", type=int, required=True)
ap.add_argument("--shard_id", type=int, required=True)
ap.add_argument("--eval_every_steps", type=int, default=100) # Compatible with launcher
ap.add_argument("--flush_every_steps", type=int, default=1) # 1=flush every step; can be larger for speed
args = ap.parse_args()
os.makedirs(args.out_dir, exist_ok=True)
if not torch.cuda.is_available():
raise RuntimeError("CUDA not available: multi-GPU inference requires GPU")
device = torch.device("cuda")
# tokenizer
tokenizer = AutoTokenizer.from_pretrained(
args.base_model_name,
trust_remote_code=True,
cache_dir=args.cache_dir,
local_files_only=False,
enable_thinking=False,
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
def apply_chat_template(messages: List[Dict[str, str]]) -> str:
return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
# load data
logger.info(f"[shard {args.shard_id}/{args.num_shards}] Loading data...")
messages_all = load_messages_from_jsonl(args.data_jsonl)
n = len(messages_all)
logger.info(f"[shard {args.shard_id}] total samples={n}")
# outputs per shard
shard_tag = f"shard{args.shard_id:03d}_of_{args.num_shards:03d}"
pred_all_path = os.path.join(args.out_dir, f"preds_all_{shard_tag}.jsonl")
pred_err_path = os.path.join(args.out_dir, f"preds_errors_{shard_tag}.jsonl")
table_csv_path = os.path.join(args.out_dir, f"preds_table_{shard_tag}.csv")
progress_path = os.path.join(args.out_dir, f"progress_{shard_tag}.json")
# resume
last_done = load_progress(progress_path) # Original index
logger.info(f"[shard {args.shard_id}] resume last_done_index={last_done}")
# open files append
f_all = open(pred_all_path, "a", encoding="utf-8")
f_err = open(pred_err_path, "a", encoding="utf-8")
# CSV: Do not extract key values, just keep the fields you want + raw data
need_header = not os.path.exists(table_csv_path) or os.path.getsize(table_csv_path) == 0
f_csv = open(table_csv_path, "a", encoding="utf-8-sig", newline="")
fieldnames = [
"index",
"gold",
"pred",
"pN",
"pP",
"prob_P",
"user_raw",
"input_text",
]
w = csv.DictWriter(f_csv, fieldnames=fieldnames)
if need_header:
w.writeheader()
f_csv.flush()
os.fsync(f_csv.fileno())
# model
logger.info(f"[shard {args.shard_id}] Building model on {device}...")
model = build_model_with_lora(args.base_model_name, args.cache_dir, device)
model.eval()
logger.info(f"[shard {args.shard_id}] Loading ckpt...")
load_trained_weights(model, args.ckpt_dir)
logger.info(f"[shard {args.shard_id}] ready.")
# running accuracy
gold_seen = 0
correct = 0
# iterate indices belonging to this shard
def belongs(i: int) -> bool:
return (i % args.num_shards) == args.shard_id and i > last_done
indices = [i for i in range(n) if belongs(i)]
total_local = len(indices)
steps = math.ceil(total_local / args.batch_size)
logger.info(f"[shard {args.shard_id}] local_samples={total_local}, steps={steps}, bs={args.batch_size}")
try:
pbar = tqdm(range(steps), desc=f"Infer {shard_tag}")
for step in pbar:
batch_idxs = indices[step * args.batch_size: (step + 1) * args.batch_size]
# build batch texts
input_texts = []
user_raws = []
golds = []
for orig_i in batch_idxs:
msgs = messages_all[orig_i]
user_raw, gold_raw = get_user_before_last_assistant(msgs)
# Continue using your original standardize_messages to ensure user content is a string
std_msgs, gold_std = standardize_messages(msgs)
gold = gold_std if gold_std else gold_raw
input_text = apply_chat_template(std_msgs)
input_texts.append(input_text)
user_raws.append(user_raw if user_raw is not None else "")
golds.append(gold)
pred_int, prob_p_list = predict_batch(
model=model,
tokenizer=tokenizer,
device=device,
input_texts=input_texts,
max_length=args.max_length,
)
# write results immediately
for j, orig_i in enumerate(batch_idxs):
pred = "P" if int(pred_int[j]) == 1 else "N"
prob_p = float(prob_p_list[j])
user_raw = user_raws[j]
gold = golds[j]
# CSV: Do not extract key values,
# just keep the fields you want + raw data
row = {
"index": orig_i,
"gold": gold,
"pred": pred,
"pN": f"{(1.0 - prob_p):.8f}",
"pP": f"{prob_p:.8f}",
"prob_P": f"{prob_p:.8f}",
"user_raw": user_raw,
"input_text": input_texts[j],
}
w.writerow(row)
# jsonl only if gold exists
if gold in ("P", "N"):
item = {
"index": orig_i,
"gold": gold,
"pred": pred,
"prob_P": prob_p,
"user_raw": user_raw,
"input_text": input_texts[j],
}
f_all.write(json.dumps(item, ensure_ascii=False) + "\n")
if pred != gold:
f_err.write(json.dumps(item, ensure_ascii=False) + "\n")
gold_seen += 1
if pred == gold:
correct += 1
# progress: last done = max index processed in this shard so far
save_progress(progress_path, max(batch_idxs))
# flush policy
if (step + 1) % args.flush_every_steps == 0:
f_all.flush(); os.fsync(f_all.fileno())
f_err.flush(); os.fsync(f_err.fileno())
f_csv.flush(); os.fsync(f_csv.fileno())
# eval policy (Keep your original parameters and output)
if (step + 1) % args.eval_every_steps == 0:
if gold_seen > 0:
acc = correct / gold_seen
logger.info(f"[{shard_tag} step {step+1}/{steps}] running_acc={acc:.6f} (gold_seen={gold_seen})")
else:
logger.info(f"[{shard_tag} step {step+1}/{steps}] running_acc=N/A (no gold)")
finally:
try:
f_all.close()
except Exception:
pass
try:
f_err.close()
except Exception:
pass
try:
f_csv.close()
except Exception:
pass
logger.info(f"[shard {args.shard_id}] DONE. outputs: {pred_all_path} {pred_err_path} {table_csv_path}")
if __name__ == "__main__":
main()