Text Generation
PEFT
Safetensors
lora
qwen2.5
alfworld
privileged-information
knowledge-distillation
conversational
Instructions to use TDSMike/ALF-qwen3B-privilege with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use TDSMike/ALF-qwen3B-privilege with PEFT:
from peft import PeftModel from transformers import AutoModelForCausalLM base_model = AutoModelForCausalLM.from_pretrained("/share/shenmingxiang/closing-square/models/Qwen2.5-3B-Instruct") model = PeftModel.from_pretrained(base_model, "TDSMike/ALF-qwen3B-privilege") - Notebooks
- Google Colab
- Kaggle
Download code/eval_student.py from TDSMike/ALF-qwen3B-privilege: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/TDSMike/ALF-qwen3B-privilege/resolve/main/code/eval_student.py
- Command line
-
hf download hf://TDSMike/ALF-qwen3B-privilege/code/eval_student.py
-
curl -L -o eval_student.py https://huggingface.co/TDSMike/ALF-qwen3B-privilege/resolve/main/code/eval_student.py
14 kB
| """Multi-GPU closed-loop ALFWorld evaluation from immutable best checkpoints.""" | |
| from __future__ import annotations | |
| import argparse | |
| from collections import Counter, defaultdict | |
| import heapq | |
| import json | |
| import os | |
| from pathlib import Path | |
| import random | |
| import shutil | |
| import sys | |
| import uuid | |
| PROJECT = Path("/share/shenmingxiang/closing-square") | |
| WS = PROJECT / "verl_workspace" | |
| sys.path.insert(0, str(WS / "third_party" / "Agent-R1")) | |
| sys.path.insert(0, str(PROJECT)) | |
| import pandas as pd | |
| import torch | |
| import torch.distributed as dist | |
| from peft import PeftModel | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| from recipes.alfworld.env.alfworld_wrapper import AlfworldTextworldEnv | |
| from src.prompts import student_prompt, teacher_prompt | |
| from src.prepare_structured_hints import build_hint_fields, render_hint, _load_traj | |
| DEFAULT_MODEL = PROJECT / "models" / "Qwen2.5-3B-Instruct" | |
| DATA_ROOT = WS / "data" / "alfworld" | |
| def set_seed(seed: int) -> None: | |
| random.seed(seed) | |
| torch.manual_seed(seed) | |
| torch.cuda.manual_seed_all(seed) | |
| def constrained_action(model, tokenizer, prompt: str, actions: list[str], device, max_prompt_length: int) -> str: | |
| old_side = tokenizer.truncation_side | |
| tokenizer.truncation_side = "left" | |
| encoded = tokenizer( | |
| prompt, return_tensors="pt", add_special_tokens=True, | |
| truncation=True, max_length=max_prompt_length, | |
| ) | |
| tokenizer.truncation_side = old_side | |
| input_ids = encoded["input_ids"].to(device) | |
| attention_mask = encoded["attention_mask"].to(device) | |
| prompt_length = input_ids.shape[1] | |
| token_to_action: dict[tuple[int, ...], str] = {} | |
| sequences: list[tuple[int, ...]] = [] | |
| for action in actions: | |
| tokens = tuple(tokenizer(action, add_special_tokens=False)["input_ids"]) | |
| token_to_action.setdefault(tokens, action) | |
| sequences.append(tokens + (tokenizer.eos_token_id,)) | |
| max_new_tokens = max(len(sequence) for sequence in sequences) | |
| def allowed_tokens(_batch_id: int, generated: torch.Tensor) -> list[int]: | |
| prefix = tuple(generated[prompt_length:].tolist()) | |
| allowed = { | |
| sequence[len(prefix)] | |
| for sequence in sequences | |
| if len(sequence) > len(prefix) and sequence[: len(prefix)] == prefix | |
| } | |
| return sorted(allowed) if allowed else [tokenizer.eos_token_id] | |
| generated = model.generate( | |
| input_ids=input_ids, | |
| attention_mask=attention_mask, | |
| do_sample=False, | |
| temperature=1.0, | |
| top_p=1.0, | |
| top_k=50, | |
| repetition_penalty=1.0, | |
| num_beams=1, | |
| max_new_tokens=max_new_tokens, | |
| eos_token_id=tokenizer.eos_token_id, | |
| pad_token_id=tokenizer.pad_token_id, | |
| prefix_allowed_tokens_fn=allowed_tokens, | |
| use_cache=True, | |
| ) | |
| continuation = generated[0, prompt_length:].tolist() | |
| if tokenizer.eos_token_id in continuation: | |
| continuation = continuation[: continuation.index(tokenizer.eos_token_id)] | |
| key = tuple(continuation) | |
| if key not in token_to_action: | |
| raise RuntimeError(f"constrained decoding returned unknown token path: {continuation}") | |
| return token_to_action[key] | |
| def run_episode( | |
| model, | |
| tokenizer, | |
| env, | |
| item, | |
| device, | |
| max_steps: int, | |
| max_prompt_length: int, | |
| prompt_mode: str, | |
| ): | |
| obs, info = env.reset_with_info(item["game"]) | |
| hint = item.get("H", "") | |
| if prompt_mode == "teacher" and not hint: | |
| expert_policy = [str(value) for value in info.get("policy_commands", [])] | |
| hint = render_hint( | |
| build_hint_fields(item["task_family"], _load_traj(item["game"]), expert_policy) | |
| ) | |
| history: list[str] = [] | |
| done = False | |
| steps = 0 | |
| repeated = 0 | |
| last_action = None | |
| trajectory = [] | |
| for _ in range(max_steps): | |
| admissible = [str(value) for value in info.get("admissible_commands", [])] | |
| if done or not admissible: | |
| break | |
| if prompt_mode == "teacher": | |
| prompt = teacher_prompt(item["goal"], str(obs), admissible, hint, history) | |
| else: | |
| prompt = student_prompt(item["goal"], str(obs), admissible, history) | |
| action = constrained_action(model, tokenizer, prompt, admissible, device, max_prompt_length) | |
| repeated += int(action == last_action) | |
| next_obs, _, done, info = env.step(action) | |
| trajectory.append( | |
| { | |
| "step": steps, | |
| "observation": str(obs), | |
| "admissible": admissible, | |
| "action": action, | |
| "response": str(next_obs), | |
| "success": bool(info.get("success", False)) if info else False, | |
| } | |
| ) | |
| history.append(f"Action taken: {action}\nEnvironment response: {next_obs}") | |
| obs = next_obs | |
| last_action = action | |
| steps += 1 | |
| return { | |
| "success": bool(info.get("success", False)) if info else False, | |
| "steps": steps, | |
| "repeated_actions": repeated, | |
| "H": hint or None, | |
| "trajectory": trajectory, | |
| } | |
| def atomic_json(path: Path, value, token: str) -> None: | |
| temp = path.parent / f".{path.name}.tmp.{token}" | |
| with temp.open("w", encoding="utf-8") as handle: | |
| json.dump(value, handle, ensure_ascii=False, indent=2) | |
| handle.flush() | |
| os.fsync(handle.fileno()) | |
| os.replace(temp, path) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--name", required=True) | |
| parser.add_argument("--adapter", default="none") | |
| parser.add_argument("--model", type=Path, default=DEFAULT_MODEL) | |
| parser.add_argument("--output", type=Path, required=True) | |
| parser.add_argument("--max_steps", type=int, default=50) | |
| parser.add_argument("--max_prompt_length", type=int, default=1536) | |
| parser.add_argument("--seed", type=int, default=42) | |
| parser.add_argument("--limit_per_split", type=int, default=0) | |
| parser.add_argument("--limit_per_family", type=int, default=0) | |
| parser.add_argument( | |
| "--dataset_mode", choices=("standard", "pilot", "sft_val"), default="standard" | |
| ) | |
| parser.add_argument("--prompt_mode", choices=("student", "teacher"), default="student") | |
| parser.add_argument( | |
| "--hint_data", | |
| type=Path, | |
| default=WS / "data" / "privileged_plan_sft.jsonl", | |
| ) | |
| args = parser.parse_args() | |
| world_size = int(os.environ.get("WORLD_SIZE", "1")) | |
| distributed = world_size > 1 | |
| if distributed: | |
| dist.init_process_group("nccl") | |
| rank = dist.get_rank() | |
| local_rank = int(os.environ["LOCAL_RANK"]) | |
| torch.cuda.set_device(local_rank) | |
| else: | |
| rank = 0 | |
| local_rank = 0 | |
| is_main = rank == 0 | |
| device = torch.device("cuda", local_rank) | |
| set_seed(args.seed) | |
| items = [] | |
| if args.dataset_mode == "sft_val": | |
| by_game = {} | |
| with args.hint_data.open("r", encoding="utf-8") as source: | |
| for line in source: | |
| row = json.loads(line) | |
| if row["sft_split"] != "val": | |
| continue | |
| by_game.setdefault( | |
| row["game_id"], | |
| { | |
| "split": "sft_val", | |
| "game": row["game_id"], | |
| "goal": row["goal"], | |
| "task_family": row["task_family"], | |
| "H": row["H"], | |
| }, | |
| ) | |
| by_family = defaultdict(list) | |
| for item in by_game.values(): | |
| by_family[item["task_family"]].append(item) | |
| rng = random.Random(args.seed) | |
| for family in sorted(by_family): | |
| family_items = sorted(by_family[family], key=lambda value: value["game"]) | |
| rng.shuffle(family_items) | |
| if args.limit_per_family: | |
| family_items = family_items[: args.limit_per_family] | |
| items.extend(family_items) | |
| else: | |
| source_root = DATA_ROOT / "pilot" / "pick_and_place" if args.dataset_mode == "pilot" else DATA_ROOT | |
| for split in ("valid_seen", "valid_unseen"): | |
| frame = pd.read_parquet(source_root / f"{split}.parquet") | |
| if args.limit_per_split: | |
| frame = frame.head(args.limit_per_split) | |
| for _, row in frame.iterrows(): | |
| extra = dict(row["extra_info"]) | |
| items.append( | |
| { | |
| "split": split, | |
| "game": str(extra["game_relative_path"]), | |
| "goal": str(extra.get("goal_text", "")), | |
| "task_family": str(extra["task_family"]), | |
| } | |
| ) | |
| indexed_items = [(index, item) for index, item in enumerate(items) if index % world_size == rank] | |
| if is_main: | |
| print( | |
| f"eval name={args.name} episodes={len(items)} world_size={world_size} " | |
| f"adapter={args.adapter}", | |
| flush=True, | |
| ) | |
| tokenizer = AutoTokenizer.from_pretrained(args.model) | |
| tokenizer.pad_token = tokenizer.eos_token | |
| tokenizer.padding_side = "right" | |
| base = AutoModelForCausalLM.from_pretrained( | |
| args.model, torch_dtype=torch.bfloat16, low_cpu_mem_usage=True | |
| ).to(device) | |
| if args.adapter.lower() != "none": | |
| adapter = Path(args.adapter).resolve() | |
| model = PeftModel.from_pretrained(base, adapter) | |
| else: | |
| adapter = None | |
| model = base | |
| model.eval().requires_grad_(False) | |
| model.config.use_cache = True | |
| token_holder = [f"{os.getpid()}.{uuid.uuid4().hex}" if is_main else ""] | |
| if distributed: | |
| dist.broadcast_object_list(token_holder, src=0) | |
| run_token = token_holder[0] | |
| part_dir = args.output.parent / f".{args.output.name}.parts.{run_token}" | |
| if is_main: | |
| args.output.parent.mkdir(parents=True, exist_ok=True) | |
| part_dir.mkdir(parents=False, exist_ok=False) | |
| if distributed: | |
| dist.barrier() | |
| env = AlfworldTextworldEnv(data_root=str(DATA_ROOT), max_episode_steps=args.max_steps) | |
| temp_part = part_dir / f".rank_{rank:02d}.jsonl.tmp" | |
| final_part = part_dir / f"rank_{rank:02d}.jsonl" | |
| with temp_part.open("w", encoding="utf-8") as handle: | |
| for local_index, (global_index, item) in enumerate(indexed_items, start=1): | |
| try: | |
| result = run_episode( | |
| model, tokenizer, env, item, device, args.max_steps, | |
| args.max_prompt_length, args.prompt_mode, | |
| ) | |
| error = None | |
| except Exception as exception: | |
| result = { | |
| "success": False, | |
| "steps": 0, | |
| "repeated_actions": 0, | |
| "H": None, | |
| "trajectory": [], | |
| } | |
| error = f"{type(exception).__name__}: {exception}" | |
| finally: | |
| env._close_env() | |
| record = {"record_index": global_index, **item, **result, "error": error} | |
| handle.write(json.dumps(record, ensure_ascii=False) + "\n") | |
| if local_index % 5 == 0: | |
| print(f"eval rank={rank} completed={local_index}/{len(indexed_items)}", flush=True) | |
| handle.flush() | |
| os.fsync(handle.fileno()) | |
| os.replace(temp_part, final_part) | |
| if distributed: | |
| dist.barrier() | |
| if is_main: | |
| def lines(path: Path): | |
| with path.open("r", encoding="utf-8") as source: | |
| for line in source: | |
| value = json.loads(line) | |
| yield value["record_index"], line, value | |
| streams = [lines(path) for path in sorted(part_dir.glob("rank_*.jsonl"))] | |
| temp_output = args.output.parent / f".{args.output.name}.tmp.{run_token}" | |
| merged_records = [] | |
| with temp_output.open("w", encoding="utf-8") as output_handle: | |
| for _, line, value in heapq.merge(*streams, key=lambda triple: triple[0]): | |
| output_handle.write(line) | |
| merged_records.append(value) | |
| output_handle.flush() | |
| os.fsync(output_handle.fileno()) | |
| os.replace(temp_output, args.output) | |
| groups = defaultdict(lambda: Counter(total=0, success=0, errors=0, steps=0, repeated=0)) | |
| for record in merged_records: | |
| for key in (record["split"], f"{record['split']}::{record['task_family']}", "pooled"): | |
| groups[key]["total"] += 1 | |
| groups[key]["success"] += int(record["success"]) | |
| groups[key]["errors"] += int(record["error"] is not None) | |
| groups[key]["steps"] += int(record["steps"]) | |
| groups[key]["repeated"] += int(record["repeated_actions"]) | |
| summary_groups = {} | |
| for key, counts in sorted(groups.items()): | |
| total = max(counts["total"], 1) | |
| summary_groups[key] = { | |
| **dict(counts), | |
| "success_rate": counts["success"] / total, | |
| "mean_steps": counts["steps"] / total, | |
| "mean_repeated_actions": counts["repeated"] / total, | |
| } | |
| summary = { | |
| "name": args.name, | |
| "adapter": str(adapter) if adapter else None, | |
| "base_model": str(args.model.resolve()), | |
| "seed": args.seed, | |
| "dataset_mode": args.dataset_mode, | |
| "prompt_mode": args.prompt_mode, | |
| "decoding": "greedy token-trie constrained to admissible commands", | |
| "groups": summary_groups, | |
| } | |
| summary_path = args.output.with_suffix(args.output.suffix + ".summary.json") | |
| atomic_json(summary_path, summary, run_token) | |
| print(json.dumps(summary, ensure_ascii=False, indent=2), flush=True) | |
| if distributed: | |
| dist.barrier() | |
| if is_main: | |
| shutil.rmtree(part_dir) | |
| if distributed: | |
| dist.destroy_process_group() | |
| if __name__ == "__main__": | |
| main() | |