EvalHarness/evalharness/fingerprint/collect_fp_extra_reference.py
ruoxi_sun 09b2add673 fingerprint: integrate fp_fusion model fingerprint benchmark
New evalharness/fingerprint/ package (from evalstone fp_fusion v1.1,
2026-09-07 pruning final): probe battery -> concurrent collection ->
five scoring views (verify/attribution/variant/adversarial/robustness),
bundled family aliases + 27 reference fingerprints (12 fp_fusion schema).

- CLI: 'evalharness fingerprint run ...' (REMAINDER passthrough, single
  source of arg definitions) + 'fingerprint list' for bundled references
- imports rewritten package-relative; direct 'python3 run_fp_fusion.py'
  execution kept working via package bootstrap
- offline analysis/collection scripts made path-independent (previously
  pinned to a /opt/evalscope path absent on this host)
- shell scripts: hardcoded API key -> FP_API_KEY/OPENAI_API_KEY env vars
- --reference accepts short names resolved against bundled references/
- pyproject: +httpx dependency, package-data references/*.json
- tests/test_fingerprint.py: 10 offline tests (battery definitions,
  assembly counts, normalization, signals, verdict ladder, CLI wiring)
- README: fingerprint section + architecture entry

Verified on H20-1: tests 10/10, installed CLI OK, full-protocol run vs
vectron GLM-5.3 reproduces baseline (score 0.9451, s_idn 0.846).
2026-09-11 03:52:21 +00:00

128 lines
5.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""FP-Fusion extra cells 采集10 个新增 cellbinary-* + day-of-week × en/zh
对齐 fp_fusion engine D 层采样;带 enable_thinking=false 禁思考 + api-key。"""
import argparse
import asyncio
import json
import math
import random
import sys
import time
from collections import Counter
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2])) # 仓库根/site-packages, 使 evalharness 包可导入
import httpx
from evalharness.fingerprint.battery import NEW_CELLS, D_TEMPERATURE
from evalharness.fingerprint.engine import normalize_answer
SYS_ONE_WORD = {'en': 'Answer with exactly one word. No punctuation, no explanation.',
'zh': '只回答一个词,不要标点,不要解释。'}
client_key = ''
def normalize_for_ref(cell_id, raw):
domains = {f"{c['id']}:{lang}": c['domain'] for c in NEW_CELLS for lang in ('en', 'zh')}
return normalize_answer(raw, domains[cell_id])
async def sample_cell(client, api_url, model, cell_id, pool, n, sem, retries, timeout):
samples = []
for i in range(n):
prompt = random.choice(pool)
body = {'model': model,
'messages': [{'role': 'system',
'content': SYS_ONE_WORD['zh' if cell_id.endswith(':zh') else 'en']},
{'role': 'user', 'content': prompt}],
'temperature': D_TEMPERATURE, 'max_tokens': 16, 'stream': False,
'chat_template_kwargs': {'enable_thinking': False}}
headers = {'Content-Type': 'application/json'}
if client_key:
headers['Authorization'] = f'Bearer {client_key}'
for attempt in range(retries):
try:
async with sem:
r = await client.post(f"{api_url.rstrip('/')}/chat/completions",
json=body, headers=headers, timeout=timeout)
r.raise_for_status()
data = r.json()
content = (data.get('choices') or [{}])[0].get('message', {}).get('content') or ''
samples.append(content)
break
except Exception as e:
if attempt == retries - 1:
print(f' [{cell_id}] sample {i} failed: {str(e)[:80]}', file=sys.stderr)
samples.append('')
else:
await asyncio.sleep(1.5 * (attempt + 1))
return cell_id, samples
async def main_async(args):
sem = asyncio.Semaphore(args.concurrency)
timeout = httpx.Timeout(max(args.timeout, 120))
async with httpx.AsyncClient(timeout=timeout) as client:
tasks = []
for c in NEW_CELLS:
for lang in ('en', 'zh'):
cell_id = f"{c['id']}:{lang}"
tasks.append(asyncio.create_task(sample_cell(
client, args.api_url, args.model, cell_id, c['par'][lang],
args.samples, sem, args.retries, timeout)))
results = await asyncio.gather(*tasks)
cells = {}
for cell_id, samples in results:
counts = Counter()
valid = invalid = refusal = empty = error = 0
for s in samples:
if s == '':
error += 1; continue
norm, cat = normalize_for_ref(cell_id, s)
if cat == 'valid' and norm is not None:
counts[norm] += 1; valid += 1
elif cat == 'refusal': refusal += 1
elif cat == 'empty': empty += 1
else: invalid += 1
total = len(samples)
entropy = 0.0
if total and counts:
entropy = -sum((v / total) * math.log2(v / total) for v in counts.values())
cells[cell_id] = {'cellId': cell_id, 'counts': {str(k): v for k, v in counts.items()},
'validCount': valid, 'invalidCount': invalid,
'refusalCount': refusal, 'emptyCount': empty,
'errorCount': error, 'totalCount': total,
'entropyBits': entropy, 'normalizedEntropy': 0.0,
'medianLatencyMs': None, 'meanCompletionTokens': None,
'meanReasoningTokens': None}
print(f' {cell_id}: valid={valid}/{total}', flush=True)
return cells
def main():
p = argparse.ArgumentParser(description='Collect fp_fusion extra reference cells')
p.add_argument('--api-url', required=True)
p.add_argument('--model', required=True)
p.add_argument('--api-key', default='')
p.add_argument('--out', required=True)
p.add_argument('--samples', type=int, default=25)
p.add_argument('--concurrency', type=int, default=2)
p.add_argument('--retries', type=int, default=8)
p.add_argument('--timeout', type=float, default=120)
args = p.parse_args()
global client_key
client_key = args.api_key
cells = asyncio.run(main_async(args))
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
payload = {'formatVersion': 1, 'protocol': 'one-token/v1', 'model': args.model,
'collectedAt': time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime()),
'samplesPerCell': args.samples, 'postReasoning': False,
'extraCellsOnly': True, 'cells': cells}
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding='utf-8')
print(f'extra reference written -> {out} ({len(cells)} cells)')
if __name__ == '__main__':
main()