File size: 18,597 Bytes
a512fa8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36ecd25
a512fa8
 
 
36ecd25
a512fa8
 
36ecd25
a512fa8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799fa1f
a512fa8
 
 
 
 
 
 
 
 
e10f2b1
 
 
 
a512fa8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799fa1f
a512fa8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e10f2b1
a512fa8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36ecd25
a512fa8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import json
import multiprocessing as mp
from multiprocessing.managers import BaseManager
import os
from pathlib import Path
import statistics
import subprocess
import sys
import threading
import time

from inference import ROOT, Engine, Stream, dump


class Broker:
    def __init__(self, database, output):
        import duckdb
        self.db=duckdb.connect(str(database),read_only=True,config={'threads':1})
        self.output=output
        self.lock=threading.Lock()
        self.offset=0
        self.total=self.db.execute('SELECT count(*) FROM attempts').fetchone()[0]
        self.committed=0
        self.metrics={}
        self.started=time.time()

    def claim(self,count):
        with self.lock:
            start=self.offset
            self.offset=min(self.total,start+count)
            values=self.db.execute('''SELECT q.payload,a.sample_index,a.attempt_id,q.question_id,a.sampling_id
                FROM attempts a JOIN questions q USING(question_id)
                WHERE a.queue_id>=? AND a.queue_id<? ORDER BY a.queue_id''',[start,self.offset]).fetchall()
        return [dict(json.loads(row),sample_index=sample,attempt_id=identity,prefill_group_id=question,sampling_id=sampling) for row,sample,identity,question,sampling in values]

    def commit(self,count):
        with self.lock:
            self.committed+=count

    def progress(self,rank,value):
        with self.lock:
            self.metrics[str(rank)]=value
            active=[r for r in self.metrics.values() if r['status']=='RUNNING' and time.time()-r['timestamp']<120]
            status=dict(status='RUNNING',expected=self.total,completed=self.committed,claimed=self.offset,
                total_decode_tokens_per_second=sum(r.get('recent_decode_tokens_per_second',r.get('decode_tokens_per_second',0)) for r in active),
                total_useful_decode_tokens=sum(r.get('useful_decode_tokens',0) for r in self.metrics.values()),
                elapsed_seconds=time.time()-self.started,ranks=self.metrics)
            dump(self.output/'STATUS.json',status)


class Manager(BaseManager):
    pass


def prepare(args):
    import duckdb
    output=args.output.resolve()
    output.mkdir(parents=True,exist_ok=True)
    manifest=json.loads((args.model/'evaluation_data/MANIFEST.json').read_text())
    database=output/'tasks.duckdb'
    db=duckdb.connect(str(database),config={'threads':args.cpu_workers})
    if args.suite=='vision':
        path=str(args.model/'evaluation_data/vision/questions/*.jsonl')
        db.execute('CREATE TABLE raw AS SELECT *, id AS source_sample_id FROM read_json_auto(?, union_by_name=true)',[path])
    else:
        db.execute('CREATE TABLE raw AS SELECT * FROM read_parquet(?, union_by_name=true)',[str(args.model/f'evaluation_data/{args.suite}/*.parquet')])
    counts=manifest[args.suite]['sources']
    db.execute('CREATE TABLE sampling(source VARCHAR,k INTEGER)')
    db.executemany('INSERT INTO sampling VALUES (?,?)',[(s,args.k if args.k is not None else
        (64 if n<100 else 16 if n<=10000 else 1) if args.suite=='text' else 1) for s,n in counts.items()])
    where='TRUE'
    parameters=[]
    if args.sources:
        where='source IN ('+','.join('?' for _ in args.sources)+')'
        parameters.extend(args.sources)
    limit=''
    if args.limit_per_source:
        limit=' QUALIFY row_number() OVER(PARTITION BY source ORDER BY source_sample_id)<=?'
        parameters.append(args.limit_per_source)
    db.execute('CREATE TABLE selected AS SELECT * FROM raw WHERE '+where+limit,parameters)
    if args.attempts:
        db.execute('CREATE TABLE requested AS SELECT * FROM read_parquet(?)',[str(args.attempts.resolve())])
        db.execute('DELETE FROM selected WHERE NOT EXISTS (SELECT 1 FROM requested r WHERE r.source=selected.source AND r.source_sample_id=selected.source_sample_id)')
    db.execute('''CREATE TABLE questions AS SELECT (row_number() OVER(ORDER BY source,source_sample_id)-1)::BIGINT question_id,
        source, source_sample_id, to_json(selected)::VARCHAR payload FROM selected''')
    if args.attempts:
        db.execute('''CREATE TABLE attempts AS SELECT (row_number() OVER(ORDER BY hash(q.question_id,r.sample_index,20260923),q.question_id,r.sample_index)-1)::BIGINT queue_id,
            q.question_id,r.sample_index::INTEGER sample_index,(q.question_id*64+r.sample_index)::BIGINT attempt_id,
            r.original_sampling_id::BIGINT sampling_id
            FROM questions q JOIN requested r USING(source,source_sample_id)''')
    else:
        db.execute('''CREATE TABLE attempts AS SELECT (row_number() OVER(ORDER BY hash(q.question_id,r.range,20260923),q.question_id,r.range)-1)::BIGINT queue_id,
            q.question_id,r.range::INTEGER sample_index,(q.question_id*64+r.range)::BIGINT attempt_id,(q.question_id*64+r.range)::BIGINT sampling_id
            FROM questions q JOIN sampling s USING(source), LATERAL range(s.k) r''')
    expected=db.execute('SELECT count(*) FROM attempts').fetchone()[0]
    count=db.execute('SELECT count(*) FROM questions').fetchone()[0]
    selected_counts=dict(db.execute('SELECT source,count(*) FROM questions GROUP BY source').fetchall())
    sampling=dict(db.execute('SELECT source,k FROM sampling').fetchall())
    db.close()
    protocol=dict(suite=args.suite,questions=count,expected=expected,source_counts=selected_counts,
        samples={s:sampling[s] for s in selected_counts},source_size_basis=counts,
        subset=args.limit_per_source is not None,k_override=args.k,gpus=args.gpus,batch=args.batch,
        profile=args.profile,max_new_tokens=args.max_new_tokens,seed=args.seed)
    if args.attempts:
        protocol.update(selected_attempts=str(args.attempts.resolve()),subset=True,
            sampling_identity='original_sampling_id from selected attempts; samples are not expanded again')
    dump(output/'PROTOCOL.json',protocol)
    return database,protocol


def evaluate(args):
    args.output=args.output.resolve()
    args.model=args.model.resolve()
    if args.suite=='memory':
        args.batch,args.profile,args.seed=1,'reference',2026091707
        args.max_new_tokens=min(args.max_new_tokens,1024)
    elif args.suite=='vision':
        args.batch,args.profile,args.seed=128,'reference',20260912
        args.max_new_tokens=min(args.max_new_tokens,32768)
    database,protocol=prepare(args)
    broker=Broker(database,args.output)
    Manager.register('broker',callable=lambda:broker)
    manager=Manager(address=('127.0.0.1',0),authkey=b'edgerock-evaluation')
    server=manager.get_server()
    port=server.address[1]
    threading.Thread(target=server.serve_forever,daemon=True).start()
    dump(args.output/'STATUS.json',dict(status='LOADING',expected=protocol['expected'],completed=0))
    started=time.time()
    def launch(pair):
        rank,gpu=pair
        cmd=[sys.executable,'-I',str(ROOT/'run.py'),'_worker','--rank',str(rank),'--port',str(port),
             '--suite',args.suite,'--model',str(args.model),'--output',str(args.output),'--batch',str(args.batch),
             '--profile',args.profile,'--backend',args.backend,'--max-new-tokens',str(args.max_new_tokens),'--seed',str(args.seed)]
        for name in ['device', 'dtype']:
            if getattr(args, name):
                cmd.extend(['--' + name, getattr(args, name)])
        with (args.output/f'rank-{rank}.log').open('w') as log:
            return subprocess.run(cmd,env={**os.environ,'CUDA_VISIBLE_DEVICES':gpu},stdout=log,stderr=subprocess.STDOUT).returncode
    gpus=args.gpus.split(',')
    with ThreadPoolExecutor(len(gpus)) as pool:
        exits=list(pool.map(launch,enumerate(gpus)))
    if any(exits):
        dump(args.output/'STATUS.json',dict(status='FAILED',worker_exit_codes=exits,expected=broker.total,completed=broker.committed))
        raise SystemExit(1)
    result=dict(status='COMPLETE',expected=broker.total,observed=broker.committed,
        seconds=time.time()-started,ranks=broker.metrics)
    result['total_useful_decode_tokens']=sum(r.get('useful_decode_tokens',0) for r in broker.metrics.values())
    result['weighted_per_gpu_decode_tokens_per_second']=result['total_useful_decode_tokens']/max(1e-9,sum(r.get('decode_seconds',0) for r in broker.metrics.values()))
    dump(args.output/'GENERATION_COMPLETE.json',result)
    dump(args.output/'STATUS.json',result)
    if not args.generate_only:
        dump(args.output/'STATUS.json',dict(status='SCORING',expected=broker.total,generated=broker.committed))
        try:
            score(args)
        except Exception as error:
            dump(args.output/'STATUS.json',dict(status='FAILED',stage='scoring',error=str(error),
                expected=broker.total,generated=broker.committed))
            raise


def worker(args):
    import pyarrow as pa
    import pyarrow.parquet as pq
    Manager.register('broker')
    manager=Manager(address=('127.0.0.1',args.port),authkey=b'edgerock-evaluation')
    manager.connect()
    broker=manager.broker()
    destination=args.output/'generated'/f'rank-{args.rank}'
    destination.mkdir(parents=True,exist_ok=True)
    fragments=0
    def write(rows):
        nonlocal fragments
        if args.suite=='vision':
            rows=[dict(source=row['source'],attempt_id=row['attempt_id'],payload_json=json.dumps(row,ensure_ascii=False)) for row in rows]
        pq.write_table(pa.Table.from_pylist(rows),destination/f'{fragments:06d}.parquet',compression='zstd')
        fragments+=1
        broker.commit(len(rows))
    def report(value):
        broker.progress(args.rank,value)
    if args.suite=='vision':
        from vision import generate_visual
        metrics=generate_visual(args,broker.claim,write,report)
    else:
        engine=Engine(args.model,args.batch,args.profile,backend=args.backend,device=args.device,dtype=args.dtype)
        def fetch(count):
            rows=broker.claim(count)
            for row in rows:
                row['max_tokens']=args.max_new_tokens if args.suite=='text' else min(args.max_new_tokens,row.get('max_tokens',args.max_new_tokens))
            return engine.encode(rows)
        first=fetch(args.batch)
        if not first:
            report(dict(status='COMPLETE',timestamp=time.time(),completed=0))
            return
        stream=Stream(engine,fetch,write,report)
        settings=dict(cap=args.max_new_tokens,seed=args.seed)
        if args.suite=='memory':
            settings.update(temperature=1,top_k=1,top_p=1,presence_penalty=0,
                mechanical=False,stop_interval=32,sampling_backend='torch')
        metrics=engine.run_stream(stream,first,**settings)
    dump(args.output/f'rank-{args.rank}-metrics.json',metrics)
    report(dict(status='COMPLETE',timestamp=time.time(),completed=metrics.get('stream_committed_groups',0),
        useful_decode_tokens=metrics.get('decode_useful_token_events',0),decode_seconds=metrics.get('decode_seconds',0),
        decode_tokens_per_second=metrics.get('decode_tokens_per_second',0),metrics_file=f'rank-{args.rank}-metrics.json'))


_ENGINE=None
_IDS=None
_SUITE=None
_TOKENIZER=None
_ASSETS=None


def init_scoring(engine,suite,assets):
    global _ENGINE,_IDS,_SUITE,_TOKENIZER,_ASSETS
    _ENGINE,_SUITE,_ASSETS=engine,suite,Path(assets)
    from tokenizers import Tokenizer
    tok=Tokenizer.from_file(str(_ASSETS/'tokenizer.json'))
    _TOKENIZER=tok
    _IDS=(tok.encode('<think>',add_special_tokens=False).ids[0],tok.encode('</think>',add_special_tokens=False).ids[0])


def score_fragment(task):
    import pyarrow as pa
    import pyarrow.parquet as pq
    from evaluation.fixed_protocol_score import score_one
    from evaluation.extension_metrics import score_prediction as extension
    from evaluation.visual_metrics import score_prediction as visual
    from evaluation.memory_metrics import score_generation as memory
    path,out=task
    records=pq.read_table(path,use_threads=False).to_pylist()
    results=[]
    for row in records:
        if _SUITE=='vision':
            row=json.loads(row['payload_json'])
            final=row['response'].rsplit('</think>',1)[1].split('<|im_end|>')[0].split('<|endoftext|>')[0].strip() if '</think>' in row['response'] else ''
            result={k:v for k,v in row.items() if k not in {'response','response_ids'}}
            result.update(visual(row,final))
        elif _SUITE=='memory':
            text=_TOKENIZER.decode(row['response_ids'],skip_special_tokens=True)
            result={k:v for k,v in row.items() if k not in {'response','response_ids','prompt_text'}}
            if result.get('answers') is None:
                result.pop('answers',None)
            result.update(memory(result,text))
        elif row['source'] in {'DROP','MuSR','LogiQA2'}:
            result={k:row.get(k) for k in ['source','source_sample_id','sample_index','subgroup','generated_tokens','finished_by_eos','cap_hit','finish_reason']}
            result.update(extension(row))
        else:
            metadata=json.loads(row['metadata_json'])
            if metadata.get('lcb_payload_path'):
                metadata['lcb_payload_path']=str(_ASSETS/'evaluation_data/code_tests'/Path(metadata['lcb_payload_path']).name)
                row['metadata_json']=json.dumps(metadata,ensure_ascii=False)
            result=score_one(_ENGINE,row,*_IDS)
        results.append(result)
    keys=set().union(*(r.keys() for r in results))
    results=[{k:r.get(k) for k in keys} for r in results]
    if _SUITE=='vision':
        results=[dict(source=r['source'],payload_json=json.dumps(r,ensure_ascii=False)) for r in results]
    pq.write_table(pa.Table.from_pylist(results),out,compression='zstd')
    return len(results)


def score(args):
    import pyarrow.parquet as pq
    from evaluation import fixed_protocol_score as scoring
    from evaluation.visual_metrics import aggregate_scores
    protocol=json.loads((args.output/'PROTOCOL.json').read_text())
    suite=protocol['suite']
    destination=args.output/'scored'
    destination.mkdir(exist_ok=True)
    paths=sorted((args.output/'generated').glob('*/*.parquet'))
    tasks=[(p,destination/f'{i:07d}.parquet') for i,p in enumerate(paths)]
    config={'execution_contract':{'verifier_service':{
        'external_verifier_assets':str(ROOT/'runtime/third_party/verifiers'),
        'external_timeout_seconds':180,'math_workers':32,'code_workers':16,'instruction_workers':8}}}
    context=mp.get_context('fork')
    started=time.time()
    with scoring.shared_scoring_engine(config,context) as engine:
        with ProcessPoolExecutor(args.cpu_workers,mp_context=context,initializer=init_scoring,initargs=(engine,suite,args.model)) as pool:
            count=sum(pool.map(score_fragment,tasks))
    with ThreadPoolExecutor(args.cpu_workers) as pool:
        parts=list(pool.map(lambda p:pq.read_table(p,use_threads=False).to_pylist(),sorted(destination.glob('*.parquet'))))
    rows=[r for part in parts for r in part]
    if suite=='vision':
        rows=[json.loads(r['payload_json']) for r in rows]
    grouped=defaultdict(list)
    for r in rows:
        grouped[r['source']].append(r)
    sources={}
    if suite=='vision':
        sources=aggregate_scores(rows)
        for source,values in grouped.items():
            sources[source].update(mean_generated_tokens=statistics.fmean(r['generated_tokens'] for r in values),
                cap_hit_rate=statistics.fmean(r['cap_hit'] for r in values),
                eos_rate=statistics.fmean(r['finished_by_eos'] for r in values),
                extraction_failure_rate=statistics.fmean(r['extraction_failed'] for r in values))
    else:
        for source,values in sorted(grouped.items()):
            if suite=='text' and source not in {'DROP','MuSR','LogiQA2'}:
                sources[source]=scoring.summarize(values)
            elif suite=='text':
                if source=='MuSR':
                    subsets=defaultdict(list)
                    for row in values:subsets[row['subgroup']].append(row['score'])
                    sources[source]=dict(score=statistics.fmean(statistics.fmean(v) for v in subsets.values()),
                        subset_scores={k:statistics.fmean(v) for k,v in subsets.items()})
                else:
                    sources[source]={'score':statistics.fmean(r['score'] for r in values)}
                if source=='DROP':
                    sources[source].update(exact_match=statistics.fmean(r['exact_match'] for r in values),f1=sources[source]['score'])
            else:
                sources[source]={k:statistics.fmean(r[k] for r in values)
                    for k in ['exact_match','all_queries_correct','parsed_query_fraction']}
                families=defaultdict(list)
                pairs=defaultdict(dict)
                for row in values:
                    families[row['task_family']].append(row)
                    pairs[row['pair_id']][row['variant']]=row
                sources[source]['tasks']={name:dict(contexts=len(part),
                    query_accuracy=statistics.fmean(r['exact_match'] for r in part),
                    all_four_correct=statistics.fmean(r['all_queries_correct'] for r in part))
                    for name,part in families.items()}
                paired=defaultdict(list)
                for pair in pairs.values():
                    if 'original' in pair and 'cf' in pair:
                        a,b=pair['original'],pair['cf']
                        for q in a['answers']:
                            key='answer_changed' if a['answers'][q]!=b['answers'][q] else 'answer_stable'
                            paired[key].append(a['query_correct'][q] and b['query_correct'][q])
                sources[source]['paired_queries']={name:dict(pairs=len(part),both_correct=statistics.fmean(part))
                    for name,part in paired.items()}
            sources[source].update(attempts=len(values),mean_generated_tokens=statistics.fmean(r['generated_tokens'] for r in values),
                cap_hit_rate=statistics.fmean(r['cap_hit'] for r in values),eos_rate=statistics.fmean(r['finished_by_eos'] for r in values))
    complete=count==protocol['expected']
    result=dict(status='COMPLETE' if complete else 'INCOMPLETE',suite=suite,expected=protocol['expected'],observed=count,
        sources=sources,seconds=time.time()-started,protocol=protocol,cpu_verifier_revision=scoring.CPU_VERIFIER_REVISION)
    dump(args.output/'RESULTS.json',result)
    dump(args.output/'STATUS.json',result)
    print(json.dumps(result,ensure_ascii=False),flush=True)