Add Natural Memory architecture and tooling

This commit is contained in:
WpyQwq
2026-09-05 08:53:41 +08:00
parent 0acf8b06ee
commit 516351f0b5
56 changed files with 18319 additions and 0 deletions
+423
View File
@@ -0,0 +1,423 @@
"""Compare the unmodified local Qwen checkpoint with a memory-surgery adapter."""
from __future__ import annotations
import argparse
import gc
import json
import math
import time
from pathlib import Path
from typing import Any, Callable
import torch
from .qwen_integration import QwenMemoryConfig, load_memory_config, load_qwen_base, load_qwen_dynamic, load_tokenizer
from .train_qwen_memory import encode_messages, load_records, pad_batch
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--model-path", default=".")
parser.add_argument("--data", default="dynamic_memory_lab/data/demo_stream.jsonl")
parser.add_argument(
"--adapter",
default="dynamic_memory_lab/qwen_memory_adapter_surgery_smoke",
help="dynamic-memory adapter directory; its memory_config.json selects the surgery mode",
)
parser.add_argument("--output", default="dynamic_memory_lab/benchmark_qwen.json")
parser.add_argument("--max-length", type=int, default=512)
parser.add_argument("--repeats", type=int, default=3)
parser.add_argument("--warmup", type=int, default=1)
parser.add_argument("--max-new-tokens", type=int, default=8)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--no-4bit", action="store_true")
return parser.parse_args()
def _adapter_config(adapter_dir: str | Path) -> QwenMemoryConfig:
return load_memory_config(adapter_dir)
def _score_output(output: Any, labels: torch.Tensor) -> tuple[float, int, int, int, bool]:
shifted_labels = labels[..., 1:]
predictions = output.logits[..., :-1, :].argmax(dim=-1)
target_positions = shifted_labels != -100
token_count = int(target_positions.sum().item())
if output.loss is None or token_count == 0:
raise RuntimeError("benchmark example has no supervised target tokens")
correct_tokens = int((predictions[target_positions] == shifted_labels[target_positions]).sum().item())
first_target = target_positions.nonzero(as_tuple=False)[0]
first_token_correct = int(
predictions[first_target[0], first_target[1]] == shifted_labels[first_target[0], first_target[1]]
)
sequence_ok = correct_tokens == token_count
return float(output.loss.detach().item()), token_count, correct_tokens, first_token_correct, sequence_ok
def _evaluate_base(model: Any, tokenizer: Any, records: list[dict[str, Any]], max_length: int) -> dict[str, float]:
device = model.get_input_embeddings().weight.device
total_nll = 0.0
total_tokens = 0
correct_tokens = 0
correct_first_tokens = 0
correct_sequences = 0
for record in records:
query = encode_messages(tokenizer, record["query"], max_length)
input_ids, attention_mask, labels = pad_batch([query], int(tokenizer.pad_token_id))
with torch.inference_mode():
output = model(
input_ids=input_ids.to(device),
attention_mask=attention_mask.to(device),
labels=labels.to(device),
use_cache=False,
)
loss, tokens, tokens_correct, first_token_correct, sequence_ok = _score_output(output, labels.to(device))
total_nll += loss * tokens
total_tokens += tokens
correct_tokens += tokens_correct
correct_first_tokens += first_token_correct
correct_sequences += int(sequence_ok)
mean_loss = total_nll / total_tokens
return {
"loss": mean_loss,
"perplexity": math.exp(mean_loss),
"token_accuracy": correct_tokens / total_tokens,
"first_target_token_accuracy": correct_first_tokens / len(records),
"exact_sequence_accuracy": correct_sequences / len(records),
"supervised_tokens": total_tokens,
}
def _evaluate_dynamic(
model: Any,
tokenizer: Any,
records: list[dict[str, Any]],
max_length: int,
) -> dict[str, float]:
device = model._find_layer_device()
pad_id = int(tokenizer.pad_token_id)
total_nll = 0.0
total_tokens = 0
correct_tokens = 0
correct_first_tokens = 0
correct_sequences = 0
for record in records:
model.reset_memory()
memory = encode_messages(tokenizer, record["memory"], max_length)
query = encode_messages(tokenizer, record["query"], max_length)
memory_input, memory_mask, _ = pad_batch([memory], pad_id)
query_input, query_mask, query_labels = pad_batch([query], pad_id)
with torch.inference_mode():
memory_output = model(
input_ids=memory_input.to(device),
attention_mask=memory_mask.to(device),
read_memory=False,
update_memory=True,
return_memory=True,
use_cache=False,
)
output = model(
input_ids=query_input.to(device),
attention_mask=query_mask.to(device),
labels=query_labels.to(device),
memory_state=memory_output.memory,
read_memory=True,
update_memory=False,
return_memory=True,
use_cache=False,
)
loss, tokens, tokens_correct, first_token_correct, sequence_ok = _score_output(
output, query_labels.to(device)
)
total_nll += loss * tokens
total_tokens += tokens
correct_tokens += tokens_correct
correct_first_tokens += first_token_correct
correct_sequences += int(sequence_ok)
model.reset_memory()
mean_loss = total_nll / total_tokens
return {
"loss": mean_loss,
"perplexity": math.exp(mean_loss),
"token_accuracy": correct_tokens / total_tokens,
"first_target_token_accuracy": correct_first_tokens / len(records),
"exact_sequence_accuracy": correct_sequences / len(records),
"supervised_tokens": total_tokens,
}
def _sync(device: torch.device) -> None:
if device.type == "cuda":
torch.cuda.synchronize(device)
def _measure(name: str, fn: Callable[[], int], repeats: int, warmup: int, device: torch.device) -> dict[str, float]:
for _ in range(warmup):
fn()
_sync(device)
started = time.perf_counter()
processed = 0
for _ in range(repeats):
processed += fn()
_sync(device)
elapsed = time.perf_counter() - started
return {
"seconds": elapsed / repeats,
"tokens_per_second": processed / elapsed,
}
def _generation_prompt(tokenizer: Any, messages: list[dict[str, Any]], device: torch.device) -> dict[str, torch.Tensor]:
encoded = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
enable_thinking=False,
)
input_ids = encoded["input_ids"] if hasattr(encoded, "__getitem__") and "input_ids" in encoded else encoded
if isinstance(input_ids, torch.Tensor):
input_ids = input_ids.tolist()
if input_ids and isinstance(input_ids[0], list):
input_ids = input_ids[0]
ids = torch.tensor([input_ids], dtype=torch.long, device=device)
return {"input_ids": ids, "attention_mask": torch.ones_like(ids)}
def _measure_base_generation(
model: Any,
tokenizer: Any,
record: dict[str, Any],
repeats: int,
warmup: int,
max_new_tokens: int,
) -> dict[str, float]:
device = model.get_input_embeddings().weight.device
prompt = _generation_prompt(tokenizer, record["query"][:-1], device)
def run() -> int:
output = model.generate(
**prompt,
max_new_tokens=max_new_tokens,
do_sample=False,
use_cache=True,
pad_token_id=tokenizer.pad_token_id,
)
return int(output.shape[1] - prompt["input_ids"].shape[1])
return _measure("baseline_generation", run, repeats, warmup, device)
def _measure_dynamic_generation(
model: Any,
tokenizer: Any,
record: dict[str, Any],
max_length: int,
repeats: int,
warmup: int,
max_new_tokens: int,
) -> dict[str, float]:
device = model._find_layer_device()
pad_id = int(tokenizer.pad_token_id)
memory = encode_messages(tokenizer, record["memory"], max_length)
memory_input, memory_mask, _ = pad_batch([memory], pad_id)
prompt = _generation_prompt(tokenizer, record["query"][:-1], device)
def run() -> int:
model.reset_memory()
with torch.inference_mode():
model(
input_ids=memory_input.to(device),
attention_mask=memory_mask.to(device),
read_memory=False,
update_memory=True,
return_memory=True,
use_cache=False,
)
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,
)
return int(output.shape[1] - prompt["input_ids"].shape[1])
result = _measure("dynamic_generation", run, repeats, warmup, device)
model.reset_memory()
return result
def _clean_generated(text: str) -> str:
return text.replace(" ", "").replace("\r", "").replace("\n", "").strip()
def _generation_quality_base(model: Any, tokenizer: Any, records: list[dict[str, Any]], max_new_tokens: int) -> dict[str, Any]:
device = model.get_input_embeddings().weight.device
contains = 0
prefixes = 0
examples = []
for record in records:
prompt = _generation_prompt(tokenizer, record["query"][:-1], device)
with torch.inference_mode():
output = model.generate(
**prompt,
max_new_tokens=max_new_tokens,
do_sample=False,
use_cache=True,
pad_token_id=tokenizer.pad_token_id,
)
generated = _clean_generated(tokenizer.decode(output[0, prompt["input_ids"].shape[1] :], skip_special_tokens=True))
expected = str(record["answer"])
contains += int(expected in generated)
prefixes += int(generated.startswith(expected))
if len(examples) < 3:
examples.append({"expected": expected, "generated": generated})
return {
"answer_contains_accuracy": contains / len(records),
"answer_prefix_accuracy": prefixes / len(records),
"examples": examples,
}
def _generation_quality_dynamic(
model: Any,
tokenizer: Any,
records: list[dict[str, Any]],
max_length: int,
max_new_tokens: int,
) -> dict[str, Any]:
device = model._find_layer_device()
pad_id = int(tokenizer.pad_token_id)
contains = 0
prefixes = 0
examples = []
for record in records:
model.reset_memory()
memory = encode_messages(tokenizer, record["memory"], max_length)
memory_input, memory_mask, _ = pad_batch([memory], pad_id)
prompt = _generation_prompt(tokenizer, record["query"][:-1], device)
with torch.inference_mode():
model(
input_ids=memory_input.to(device),
attention_mask=memory_mask.to(device),
read_memory=False,
update_memory=True,
return_memory=True,
use_cache=False,
)
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,
)
generated = _clean_generated(tokenizer.decode(output[0, prompt["input_ids"].shape[1] :], skip_special_tokens=True))
expected = str(record["answer"])
contains += int(expected in generated)
prefixes += int(generated.startswith(expected))
if len(examples) < 3:
examples.append({"expected": expected, "generated": generated})
model.reset_memory()
return {
"answer_contains_accuracy": contains / len(records),
"answer_prefix_accuracy": prefixes / len(records),
"examples": examples,
}
def _release(model: Any) -> None:
del model
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
def main() -> None:
args = parse_args()
if args.repeats < 1 or args.warmup < 0:
raise ValueError("repeats must be >= 1 and warmup must be >= 0")
torch.manual_seed(args.seed)
tokenizer = load_tokenizer(args.model_path)
records = load_records(args.data)
use_4bit = not args.no_4bit
results: dict[str, Any] = {
"model_path": str(Path(args.model_path).resolve()),
"data": str(Path(args.data).resolve()),
"adapter": str(Path(args.adapter).resolve()),
"records": len(records),
"max_length": args.max_length,
"quantization": "4bit_nf4" if use_4bit else "none",
}
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)
results["baseline"] = {
"device": str(baseline_device),
"scores": _evaluate_base(baseline, tokenizer, records, args.max_length),
"generation_quality": _generation_quality_base(baseline, tokenizer, records, args.max_new_tokens),
"generation": _measure_base_generation(
baseline, tokenizer, records[0], args.repeats, args.warmup, args.max_new_tokens
),
}
if baseline_device.type == "cuda":
results["baseline"]["peak_memory_gb"] = torch.cuda.max_memory_allocated(baseline_device) / 1024**3
_release(baseline)
baseline = None
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
memory_config = _adapter_config(args.adapter)
print(f"loading dynamic mode={memory_config.mode} layers={memory_config.layer_indices}")
dynamic = load_qwen_dynamic(
args.model_path,
memory_config=memory_config,
load_in_4bit=use_4bit,
)
dynamic.load_memory_adapter(args.adapter)
dynamic.eval()
dynamic_device = dynamic._find_layer_device()
if dynamic_device.type == "cuda":
torch.cuda.reset_peak_memory_stats(dynamic_device)
results["dynamic_memory"] = {
"device": str(dynamic_device),
"mode": memory_config.mode,
"layers": list(dynamic.layer_indices),
"scores": _evaluate_dynamic(dynamic, tokenizer, records, args.max_length),
"generation_quality": _generation_quality_dynamic(
dynamic, tokenizer, records, args.max_length, args.max_new_tokens
),
"generation": _measure_dynamic_generation(
dynamic,
tokenizer,
records[0],
args.max_length,
args.repeats,
args.warmup,
args.max_new_tokens,
),
"trainable_parameters": sum(parameter.numel() for parameter in dynamic.trainable_parameters),
}
if dynamic_device.type == "cuda":
results["dynamic_memory"]["peak_memory_gb"] = torch.cuda.max_memory_allocated(dynamic_device) / 1024**3
_release(dynamic)
dynamic = None
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(results, indent=2), encoding="utf-8")
print(json.dumps(results, indent=2, ensure_ascii=False))
print(f"saved={output_path.resolve()}")
if __name__ == "__main__":
main()