302 lines
11 KiB
Python
302 lines
11 KiB
Python
"""Compare original Qwen and Native Memory on general regression tasks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import gc
|
|
import json
|
|
import re
|
|
import sys
|
|
import time
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import torch
|
|
|
|
from .qwen_integration import load_memory_config, load_qwen_base, load_qwen_dynamic, load_tokenizer
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--model-path", default=".")
|
|
parser.add_argument("--adapter", default="dynamic_memory_lab/qwen_memory_adapter_native_v3")
|
|
parser.add_argument("--data", default="dynamic_memory_lab/data/comprehensive_general.jsonl")
|
|
parser.add_argument("--output", default="dynamic_memory_lab/comprehensive_benchmark_native_v3.json")
|
|
parser.add_argument("--max-new-tokens", type=int, default=32)
|
|
parser.add_argument("--perf-repeats", type=int, default=3)
|
|
parser.add_argument("--no-4bit", action="store_true")
|
|
parser.add_argument("--category-drop-limit", type=float, default=0.10)
|
|
parser.add_argument("--overall-drop-limit", type=float, default=0.05)
|
|
return parser.parse_args()
|
|
|
|
|
|
def load_cases(path: str | Path) -> list[dict[str, Any]]:
|
|
cases = []
|
|
for line in Path(path).read_text(encoding="utf-8").splitlines():
|
|
if line.strip():
|
|
cases.append(json.loads(line))
|
|
if not cases:
|
|
raise ValueError(f"no benchmark cases found in {path}")
|
|
return cases
|
|
|
|
|
|
def normalize(text: str) -> str:
|
|
return re.sub(r"[\s`*_#,。!?、;:,.!?;:'\"()()\[\]{}]", "", text).lower()
|
|
|
|
|
|
def contains_answer(text: str, acceptable: list[str]) -> bool:
|
|
normalized = normalize(text)
|
|
for answer in acceptable:
|
|
expected = normalize(str(answer))
|
|
if not expected:
|
|
continue
|
|
if expected.isdigit() and len(expected) == 1:
|
|
if re.search(rf"(?<!\d){re.escape(expected)}(?!\d)", normalized):
|
|
return True
|
|
elif expected in normalized:
|
|
return True
|
|
return False
|
|
|
|
|
|
def prompt_inputs(tokenizer: Any, prompt: str, device: torch.device) -> dict[str, torch.Tensor]:
|
|
encoded = tokenizer.apply_chat_template(
|
|
[{"role": "user", "content": prompt}],
|
|
tokenize=True,
|
|
add_generation_prompt=True,
|
|
enable_thinking=False,
|
|
)
|
|
input_ids = encoded["input_ids"] if isinstance(encoded, dict) or hasattr(encoded, "__getitem__") else encoded
|
|
if isinstance(input_ids, torch.Tensor):
|
|
if input_ids.ndim == 1:
|
|
input_ids = input_ids.unsqueeze(0)
|
|
input_ids = input_ids.to(device)
|
|
else:
|
|
if input_ids and isinstance(input_ids[0], list):
|
|
input_ids = input_ids[0]
|
|
input_ids = torch.tensor([input_ids], dtype=torch.long, device=device)
|
|
return {"input_ids": input_ids, "attention_mask": torch.ones_like(input_ids)}
|
|
|
|
|
|
def _decode_generation(tokenizer: Any, output: torch.Tensor, prompt: dict[str, torch.Tensor]) -> str:
|
|
start = prompt["input_ids"].shape[1]
|
|
return tokenizer.decode(output[0, start:].detach().cpu().tolist(), skip_special_tokens=True).strip()
|
|
|
|
|
|
def evaluate_model(
|
|
model: Any,
|
|
tokenizer: Any,
|
|
cases: list[dict[str, Any]],
|
|
*,
|
|
adapted: bool,
|
|
max_new_tokens: int,
|
|
perf_repeats: int,
|
|
) -> dict[str, Any]:
|
|
device = model._find_layer_device() if adapted else model.get_input_embeddings().weight.device
|
|
rows: list[dict[str, Any]] = []
|
|
category_values: dict[str, list[float]] = defaultdict(list)
|
|
category_examples: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
started = time.perf_counter()
|
|
for case in cases:
|
|
if adapted:
|
|
model.reset_memory()
|
|
prompt = prompt_inputs(tokenizer, str(case["prompt"]), device)
|
|
with torch.inference_mode():
|
|
if adapted:
|
|
output = model.generate(
|
|
**prompt,
|
|
max_new_tokens=max_new_tokens,
|
|
do_sample=False,
|
|
update_memory=False,
|
|
use_cache=True,
|
|
pad_token_id=tokenizer.pad_token_id,
|
|
)
|
|
else:
|
|
output = model.generate(
|
|
**prompt,
|
|
max_new_tokens=max_new_tokens,
|
|
do_sample=False,
|
|
use_cache=True,
|
|
pad_token_id=tokenizer.pad_token_id,
|
|
)
|
|
generated = _decode_generation(tokenizer, output, prompt)
|
|
passed = contains_answer(generated, list(case["acceptable"]))
|
|
category = str(case["category"])
|
|
category_values[category].append(float(passed))
|
|
if len(category_examples[category]) < 3:
|
|
category_examples[category].append(
|
|
{
|
|
"id": case["id"],
|
|
"prompt": case["prompt"],
|
|
"acceptable": case["acceptable"],
|
|
"generated": generated,
|
|
"passed": passed,
|
|
}
|
|
)
|
|
rows.append(
|
|
{
|
|
"id": case["id"],
|
|
"category": category,
|
|
"acceptable": case["acceptable"],
|
|
"generated": generated,
|
|
"passed": passed,
|
|
}
|
|
)
|
|
elapsed = time.perf_counter() - started
|
|
|
|
perf_case = cases[0]
|
|
if adapted:
|
|
model.reset_memory()
|
|
perf_prompt = prompt_inputs(tokenizer, str(perf_case["prompt"]), device)
|
|
if device.type == "cuda":
|
|
torch.cuda.synchronize(device)
|
|
perf_start = time.perf_counter()
|
|
generated_tokens = 0
|
|
for _ in range(max(1, perf_repeats)):
|
|
if adapted:
|
|
model.reset_memory()
|
|
with torch.inference_mode():
|
|
if adapted:
|
|
perf_output = model.generate(
|
|
**perf_prompt,
|
|
max_new_tokens=max_new_tokens,
|
|
do_sample=False,
|
|
update_memory=False,
|
|
use_cache=True,
|
|
pad_token_id=tokenizer.pad_token_id,
|
|
)
|
|
else:
|
|
perf_output = model.generate(
|
|
**perf_prompt,
|
|
max_new_tokens=max_new_tokens,
|
|
do_sample=False,
|
|
use_cache=True,
|
|
pad_token_id=tokenizer.pad_token_id,
|
|
)
|
|
generated_tokens += int(perf_output.shape[1] - perf_prompt["input_ids"].shape[1])
|
|
if device.type == "cuda":
|
|
torch.cuda.synchronize(device)
|
|
perf_elapsed = time.perf_counter() - perf_start
|
|
score = sum(sum(values) for values in category_values.values()) / len(cases)
|
|
return {
|
|
"device": str(device),
|
|
"cases": len(cases),
|
|
"elapsed_seconds": elapsed,
|
|
"overall_score": score,
|
|
"categories": {
|
|
category: {
|
|
"count": len(values),
|
|
"score": sum(values) / len(values),
|
|
"examples": category_examples[category],
|
|
}
|
|
for category, values in sorted(category_values.items())
|
|
},
|
|
"performance": {
|
|
"repeats": max(1, perf_repeats),
|
|
"max_new_tokens": max_new_tokens,
|
|
"tokens_per_second": generated_tokens / max(perf_elapsed, 1e-9),
|
|
"seconds_per_run": perf_elapsed / max(1, perf_repeats),
|
|
},
|
|
"rows": rows,
|
|
}
|
|
|
|
|
|
def release(model: Any) -> None:
|
|
del model
|
|
gc.collect()
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
|
|
def main() -> None:
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
args = parse_args()
|
|
cases = load_cases(args.data)
|
|
tokenizer = load_tokenizer(args.model_path)
|
|
use_4bit = not args.no_4bit
|
|
report: dict[str, Any] = {
|
|
"model_path": str(Path(args.model_path).resolve()),
|
|
"adapter": str(Path(args.adapter).resolve()),
|
|
"data": str(Path(args.data).resolve()),
|
|
"quantization": "4bit_nf4" if use_4bit else "none",
|
|
"cases": len(cases),
|
|
}
|
|
|
|
print("loading baseline")
|
|
baseline = load_qwen_base(args.model_path, load_in_4bit=use_4bit)
|
|
baseline.eval()
|
|
baseline_device = baseline.get_input_embeddings().weight.device
|
|
if baseline_device.type == "cuda":
|
|
torch.cuda.reset_peak_memory_stats(baseline_device)
|
|
report["baseline"] = evaluate_model(
|
|
baseline,
|
|
tokenizer,
|
|
cases,
|
|
adapted=False,
|
|
max_new_tokens=args.max_new_tokens,
|
|
perf_repeats=args.perf_repeats,
|
|
)
|
|
if baseline_device.type == "cuda":
|
|
report["baseline"]["peak_memory_gb"] = torch.cuda.max_memory_allocated(baseline_device) / 1024**3
|
|
release(baseline)
|
|
|
|
config = load_memory_config(args.adapter)
|
|
config.persistent_memory = False
|
|
print(f"loading native adapter mode={config.mode} layers={config.layer_indices}")
|
|
adapted = load_qwen_dynamic(
|
|
args.model_path,
|
|
memory_config=config,
|
|
load_in_4bit=use_4bit,
|
|
)
|
|
adapted.load_memory_adapter(args.adapter)
|
|
adapted.eval()
|
|
adapted_device = adapted._find_layer_device()
|
|
if adapted_device.type == "cuda":
|
|
torch.cuda.reset_peak_memory_stats(adapted_device)
|
|
report["native_memory"] = evaluate_model(
|
|
adapted,
|
|
tokenizer,
|
|
cases,
|
|
adapted=True,
|
|
max_new_tokens=args.max_new_tokens,
|
|
perf_repeats=args.perf_repeats,
|
|
)
|
|
if adapted_device.type == "cuda":
|
|
report["native_memory"]["peak_memory_gb"] = torch.cuda.max_memory_allocated(adapted_device) / 1024**3
|
|
|
|
baseline_score = report["baseline"]["overall_score"]
|
|
native_score = report["native_memory"]["overall_score"]
|
|
baseline_categories = report["baseline"]["categories"]
|
|
native_categories = report["native_memory"]["categories"]
|
|
category_deltas = {
|
|
category: native_categories[category]["score"] - baseline_categories[category]["score"]
|
|
for category in baseline_categories.keys() & native_categories.keys()
|
|
}
|
|
report["regression"] = {
|
|
"overall_delta": native_score - baseline_score,
|
|
"category_deltas": category_deltas,
|
|
"overall_drop_limit": args.overall_drop_limit,
|
|
"category_drop_limit": args.category_drop_limit,
|
|
"overall_regression_alert": native_score < baseline_score - args.overall_drop_limit,
|
|
"category_regression_alerts": {
|
|
category: delta < -args.category_drop_limit for category, delta in category_deltas.items()
|
|
},
|
|
"pass": native_score >= baseline_score - args.overall_drop_limit
|
|
and all(delta >= -args.category_drop_limit for delta in category_deltas.values()),
|
|
}
|
|
legacy_path = Path(args.adapter).parent / "benchmark_qwen_native_v3_eval.json"
|
|
if legacy_path.exists():
|
|
report["same_memory_benchmark"] = json.loads(legacy_path.read_text(encoding="utf-8"))
|
|
native_eval_path = Path(args.adapter) / "native_eval_report.json"
|
|
if native_eval_path.exists():
|
|
report["native_memory_holdout"] = json.loads(native_eval_path.read_text(encoding="utf-8"))
|
|
output = Path(args.output)
|
|
output.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
print(json.dumps({key: report[key] for key in ("baseline", "native_memory", "regression")}, ensure_ascii=False, indent=2))
|
|
print(f"saved={output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|