Laya
Safetensors
English
biology
dna
protein
File size: 13,400 Bytes
d35dd87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
"""Monitored B1/text-only follow-up, three seeds, automatic dev-only reports."""
from __future__ import annotations
import argparse
import fcntl
import hashlib
import json
import math
from pathlib import Path
import statistics
import subprocess
import sys
import time

from laya_metrics import classification_metrics
from laya_overnight import expected_eval

ROOT=Path(__file__).resolve().parents[1]
RUNS=ROOT/'artifacts/laya_controls'
PREVIOUS=ROOT/'artifacts/laya_direct_legacy'
SEEDS=(20260922,20260923,20260924)
TASKS=('promoter_detection','fold_class')
KINDS=('b1','text_only')


def sha(path):return hashlib.sha256(path.read_bytes()).hexdigest()


def write_json(path,value):
    temp=path.with_suffix('.tmp');temp.write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n');temp.replace(path)


def status(state,**values):
    data={'state':state,'updated_utc':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),
          'no_cpt':True,'test_access':False,'poll_seconds':60,**values}
    write_json(RUNS/'status.json',data);print(json.dumps(data),flush=True)


def validate(path,kind,seed,smoke=False):
    s=json.loads((path/'summary.json').read_text())
    def require(value,reason):
        if not value:raise RuntimeError(f'{path.name}: {reason}')
    require(s['kind']==kind and s['seed']==seed,'wrong model/seed')
    require(s['formal']!=smoke and s['smoke_only']==smoke,'wrong result type')
    require(s['no_cpt'] and not s['test_access'],'CPT/test gate failed')
    require(s['training']['finite'] and s['checkpoint_reload_logits_match'] and s['checkpoint_reload_input_ids_match'],'training/reload failure')
    require(s['evaluated_splits']==['selection_dev','calibration'],'split changed')
    require(all(v['truncated_count']==0 for v in s['item_stats'].values()),'truncation')
    require(s['representation_sha256']==sha(ROOT/'artifacts/laya_direct_bpe_legacy_representation/metadata.json'),'representation mismatch')
    require(s['expansion']['new_vocab_size']==78367 and s['expansion']['maximum_initialization_error']==0,'initialization mismatch')
    if kind=='b1':
        require(set(s['new_embedding_gradient_probe'])==set(TASKS),'missing task gradient')
        require(all(v['gradient_norm']>0 and math.isfinite(v['gradient_norm']) for v in s['new_embedding_gradient_probe'].values()),'invalid gradient')
    else:require(not s['embedding_receives_sequence_supervision'],'text-only received sequence')
    if not smoke:
        require(s['training']['updates']==3005 and s['training']['examples_consumed']==96160,'budget mismatch')
        require(s['n_rows']=={'train':32050,'selection_dev':1978,'calibration':1986},'data count mismatch')
        reference=json.loads((PREVIOUS/f'full_bpe_seed{seed}/summary.json').read_text())
        require(all(s['item_stats'][split]['membership_sha256']==reference['item_stats'][split]['membership_sha256']
                    for split in s['item_stats']),'candidate/control membership differs')
    for split in ('selection_dev','calibration'):
        records=[json.loads(line) for line in (path/f'{split}_predictions.jsonl').read_text().splitlines()]
        truth=expected_eval(split)
        require(len(records)==s['n_rows'][split] and len({r['id'] for r in records})==len(records),'duplicate/missing predictions')
        if not smoke:require({r['id'] for r in records}==set(truth),'prediction membership mismatch')
        for r in records:
            require(all(r[key]==truth[r['id']][key] for key in ('label','task','n_classes')),'label mismatch')
        for task in TASKS:
            selected=[r for r in records if r['task']==task]
            for mode,temp in [('raw',1.),('calibrated',s['calibration_temperature'][task]['temperature'])]:
                metrics=classification_metrics([r['logits'] for r in selected],[r['label'] for r in selected],
                                                n_classes=selected[0]['n_classes'],temperature=temp)
                for key in ('accuracy','balanced_accuracy','macro_f1','mcc','nll','brier','ece15'):
                    reported=s['evaluation'][split][mode][task][key]
                    require(math.isfinite(reported) and abs(reported-metrics[key])<1e-9,'metric recomputation failed')
    return s


def diagnose_complete():
    for seed in SEEDS:
        for condition in ('raw','full_bpe'):
            p=RUNS/f'sequence_diagnostics/{condition}_seed{seed}/summary.json'
            if not p.exists():return False
            s=json.loads(p.read_text())
            if s['seed']!=seed or s['condition']!=condition or not s['baseline_logits_reproduced'] or s['test_access']:
                raise RuntimeError('Invalid sequence diagnostic')
            if set(s['variants'])!={'sequence_removed','residue_shuffle_0','residue_shuffle_1','residue_shuffle_2','candidate_permutation'}:
                raise RuntimeError('Incomplete diagnostic variants')
    return True


def moments(values):
    return {'mean':statistics.mean(values),'std':statistics.stdev(values) if len(values)>1 else None,'n':len(values)}


def fmt(values,scale=100):
    m=moments(values)
    return f"{scale*m['mean']:.2f}"+(f" ± {scale*m['std']:.2f}" if m['std'] is not None else '')


def report():
    summaries={'candidate':{},'b1':{},'text_only':{}}
    for seed in SEEDS:
        summaries['candidate'][seed]=json.loads((PREVIOUS/f'full_bpe_seed{seed}/summary.json').read_text())
        for kind in KINDS:
            path=RUNS/f'{kind}_seed{seed}'
            if (path/'summary.json').exists():summaries[kind][seed]=validate(path,kind,seed)
    lines=['# Laya BPE 基础功能:分类接口与序列依赖对照','',
           '仅 selection_dev;无 CPT;test 未访问。完整生物 BPE 固定为功能配置,不再搜索词表规模。',
           '均值±标准差为跨训练 seed 的样本标准差,单个 seed 不报告标准差。','',
           '## 模型对照','',
           '| 模型 | 已完成 seed 数 | 任务 | Accuracy (%) | Macro-F1 (%) | MCC | 校准 NLL |',
           '|---|---:|---|---:|---:|---:|---:|']
    for kind,runs in summaries.items():
        if not runs:continue
        for task in TASKS:
            metrics=[s['evaluation']['selection_dev'] for s in runs.values()]
            lines.append(f"| {kind} | {len(runs)} | {task} | {fmt([m['raw'][task]['accuracy'] for m in metrics])} | {fmt([m['raw'][task]['macro_f1'] for m in metrics])} | {fmt([m['raw'][task]['mcc'] for m in metrics],1)} | {fmt([m['calibrated'][task]['nll'] for m in metrics],1)} |")
    lines+=['','candidate 为动态候选评分接口;b1 为相同初始化、近似相同参数量的固定类别输出头;text_only 为相同候选模型在训练与评估时均移除序列。部分 seed 完成时,不把其均值与三 seed 基线当作完整配对比较。','',
            '## 推理时扰动诊断','',
            '序列重排不保证保持生物标签。下表移除/重排后的数值是相对原始标签的匹配率,用于依赖诊断,不是扰动序列的生物分类准确率。每个模型先平均三个固定重排副本,再报告跨三个训练 seed 的均值±标准差。','',
            '| 表示 | 任务 | 原始 Accuracy (%) | 移除序列:原标签匹配率 (%) | 重排序列:原标签匹配率 (%) | 候选重排预测一致率 (%) |',
            '|---|---|---:|---:|---:|---:|']
    diagnostics={}
    for condition in ('full_bpe','raw'):
        values=[]
        for seed in SEEDS:
            path=RUNS/f'sequence_diagnostics/{condition}_seed{seed}/summary.json'
            if path.exists():values.append(json.loads(path.read_text()))
        diagnostics[condition]=values
        if not values:continue
        for task in TASKS:
            original=[v['original']['raw'][task]['accuracy'] for v in values]
            removed=[v['variants']['sequence_removed']['reference_label_metrics']['raw'][task]['accuracy'] for v in values]
            shuffled=[statistics.mean(v['variants'][f'residue_shuffle_{i}']['reference_label_metrics']['raw'][task]['accuracy'] for i in range(3)) for v in values]
            agreement=[v['variants']['candidate_permutation']['comparison_to_original'][task]['prediction_agreement'] for v in values]
            lines.append(f'| {condition} | {task} | {fmt(original)} | {fmt(removed)} | {fmt(shuffled)} | {fmt(agreement)} |')
    paired={}
    lines+=['','## 配对结果与解释边界','']
    for kind in KINDS:
        paired[kind]={}
        for task in TASKS:
            seeds=sorted(summaries[kind])
            if not seeds:continue
            delta=[summaries[kind][seed]['evaluation']['selection_dev']['raw'][task]['accuracy']-
                   summaries['candidate'][seed]['evaluation']['selection_dev']['raw'][task]['accuracy'] for seed in seeds]
            paired[kind][task]=moments(delta)
            lines.append(f'- {kind} 相对相同 seed 的 candidate,{task} Accuracy 平均差值 {100*statistics.mean(delta):+.2f} 个百分点({len(seeds)} 个配对 seed)。')
    lines+=['','输入扰动只检查依赖性;不能证明模型学到因果机制。候选重排按类别语义还原 logits 后比较,若一致率低于 100% 则说明位置敏感性。B1 不读取候选字符串,以已知任务选择固定 2/7 类输出;它保留原 Laya 上下文表示层和读出 MLP,但最后类别层重新初始化,且输入长度不同。','',
            '本轮完成后队列停止。固定 test 评估协议及尚未验证的同源独立性仍需单独处理,不因开发集结果自动解锁 test。','']
    (ROOT/'research/laya_controls_results.md').write_text('\n'.join(lines))
    write_json(RUNS/'results.json',{'no_cpt':True,'test_access':False,
         'completed_seeds':{k:list(v) for k,v in summaries.items()},'paired_accuracy_delta_control_minus_candidate':paired,
         'report':str(ROOT/'research/laya_controls_results.md')})


def freeze():
    previous=json.loads((PREVIOUS/'frozen_pair/run_manifest.json').read_text())['hashes']
    own=[ROOT/'scripts'/name for name in ('laya_control_experiment.py','laya_sequence_diagnostics.py',
          'run_laya_controls.py','laya_overnight.py','test_laya_controls.py')]
    own += [ROOT/'artifacts/laya_model/model.safetensors',ROOT/'artifacts/laya_model/rl_agent_config.json']
    for seed in SEEDS:
        for condition in ('raw','full_bpe'):
            own += [PREVIOUS/f'{condition}_seed{seed}/summary.json']
    hashes={**previous,**{str(p.relative_to(ROOT)):sha(p) for p in own}}
    data={'seeds':list(SEEDS),'models':list(KINDS),'updates':3005,'micro_batch':8,'grad_accum':4,
          'no_cpt':True,'test_access':False,'hashes':hashes}
    path=RUNS/'run_manifest.json'
    if path.exists() and json.loads(path.read_text())!=data:raise RuntimeError('Control manifest changed')
    write_json(path,data)


def verify():
    for name,expected in json.loads((RUNS/'run_manifest.json').read_text())['hashes'].items():
        if sha(ROOT/name)!=expected:raise RuntimeError(f'Frozen file changed: {name}')


def launch(kind,seed,smoke=False):
    verify()
    output=RUNS/(f'smoke_{kind}' if smoke else f'{kind}_seed{seed}')
    if (output/'summary.json').exists():validate(output,kind,seed,smoke);return
    if output.exists():raise RuntimeError(f'Partial output exists: {output}')
    while True:
        mem=subprocess.check_output(['nvidia-smi','--query-gpu=memory.used','--format=csv,noheader,nounits'],text=True)
        if int(mem.strip().splitlines()[0])<500:break
        status('waiting_for_gpu',kind=kind,seed=seed,smoke=smoke);time.sleep(60)
    command=[sys.executable,'-u',str(ROOT/'scripts/laya_control_experiment.py'),'--kind',kind,
             '--seed',str(seed),'--output-dir',str(output)]
    if smoke:command.append('--smoke')
    log=RUNS/'logs'/f'{output.name}.log'
    with log.open('w') as stream:
        process=subprocess.Popen(command,cwd=ROOT,stdout=stream,stderr=subprocess.STDOUT)
        while process.poll() is None:
            status('running',kind=kind,seed=seed,smoke=smoke,pid=process.pid,log=str(log))
            time.sleep(60)
        if process.returncode:raise RuntimeError(f'{output.name} failed: {process.returncode}; see {log}')
    validate(output,kind,seed,smoke)


def main():
    p=argparse.ArgumentParser(description=__doc__)
    p.add_argument('--diagnostic-pid',type=int)
    p.add_argument('--report-only',action='store_true')
    a=p.parse_args()
    if a.report_only:report();return
    with (RUNS/'queue.lock').open('w') as lock:
        fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
        try:
            freeze();verify()
            while not diagnose_complete():
                if not a.diagnostic_pid or not Path(f'/proc/{a.diagnostic_pid}').exists():
                    raise RuntimeError('Diagnostics ended without all six completed results')
                status('waiting_for_diagnostics',diagnostic_pid=a.diagnostic_pid);time.sleep(60)
            report()
            for kind in KINDS:launch(kind,SEEDS[0],smoke=True)
            for seed in SEEDS:
                for kind in KINDS:
                    launch(kind,seed)
                    report()
            status('complete',completed_runs=6,completed_seeds=list(SEEDS),report=str(ROOT/'research/laya_controls_results.md'))
        except Exception as exc:
            status('failed',error=str(exc));raise


if __name__=='__main__':main()