File size: 11,395 Bytes
fe31b61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Preregister, acquire, then evaluate every eligible external cohort."""
from __future__ import annotations

import argparse
from concurrent.futures import ThreadPoolExecutor
import csv
import gzip
import json
from pathlib import Path
import time

import torch

from unionmind.decision_sufficiency.core import (
    calibration, count_samples, dispersion, encode, estimate, plan, push, realized,
)
from .data import ROOT, PROTOCOL, acquire, prepare, sha, write
from .runtime import Timer, replay, select_samples


def lock():
    ROOT.mkdir(parents=True,exist_ok=True)
    assert not (ROOT/'preconfirmation_lock.json').exists()
    old = Path('artifacts/decision_sufficiency_01')
    files = json.loads((old/'preservation_lock.json').read_text())['files']
    for p,digest in json.loads((old/'final_snapshot.json').read_text())['files'].items():
        assert p not in files or files[p] == digest
        files[p] = digest
    for name in ('final_snapshot.json','finalize.log'):
        path = old/name
        if path.exists(): files[str(path)] = sha(path)
    def changed(item):
        p,d = item
        return p if not Path(p).exists() or sha(p) != d else None
    with ThreadPoolExecutor(max_workers=8) as pool:
        errors = [p for p in pool.map(changed,files.items()) if p]
    assert not errors, errors
    write(ROOT/'preservation_lock.json',dict(files=files))
    code = list(Path('unionmind/external_sufficiency').glob('*.py'))+[
        PROTOCOL,Path('tests/test_external_sufficiency.py'),
        Path('unionmind/decision_sufficiency/core.py')]
    write(ROOT/'preconfirmation_lock.json',dict(files={str(p):sha(p) for p in code},
        locked_before_any_source_download=True,registered_threshold_changes_permitted=False,
        archived_results_protected=len(files),created_unix=time.time()))
    print(json.dumps({'locked_files':len(code),'prior_files':len(files)}),flush=True)


def verify_lock():
    locked = json.loads((ROOT/'preconfirmation_lock.json').read_text())
    changed = [p for p,d in locked['files'].items() if sha(p) != d]
    assert not changed, changed


def predictive_scores(model, truth):
    scores = calibration(model,truth)
    mass = model.sum((2,3))
    conditional = model/mass[...,None,None]
    scores['joint_conditional_log_loss'] = -(truth*conditional.clamp_min(1e-300).log()).sum((1,2,3))
    # Predict withheld observations conditional on known H and current evidence.
    given_h = model/model.sum(3,keepdim=True).clamp_min(1e-300)
    scores['observation_given_h_log_loss'] = -(truth*given_h.clamp_min(1e-300).log()).sum((1,2,3))
    return {k:float(v[0]) for k,v in scores.items()}


def evaluate(protocol):
    verify_lock()
    manifest = json.loads((ROOT/'prepared/manifest.json').read_text())
    out = ROOT/'evaluation'
    out.mkdir(exist_ok=True)
    assert not (out/'metrics.csv.gz').exists()
    device = protocol['device']
    fieldnames = ['task_index','dataset','environment','task','regime','sample_seed','sample_count',
        'model','representation','budget','cost','utility','queries','abstention','estimated_utility',
        'fitted_compression_loss','fitted_dispersion','empirical_hindsight_utility',
        'empirical_compression_loss','empirical_model_loss','posterior_brier','posterior_log_loss',
        'posterior_absolute_error','joint_conditional_log_loss','observation_given_h_log_loss',
        'raw_posterior_brier','raw_posterior_log_loss','raw_joint_conditional_log_loss',
        'construction_seconds','evaluation_records']
    rows = 0
    maximum_replay_error = 0.
    maximum_decomposition_error = 0.
    primary_trajectory_records = 0
    skipped = []
    start = time.perf_counter()
    torch.cuda.reset_peak_memory_stats(device)
    with gzip.open(out/'metrics.csv.gz','wt',newline='') as f:
        writer = csv.DictWriter(f,fieldnames=fieldnames)
        writer.writeheader()
        for task in manifest['tasks']:
            archive = torch.load(ROOT/'prepared'/task['file'],map_location=device,weights_only=True)
            held = archive['evaluation']
            truth_counts = count_samples(held[None],6)
            truth = truth_counts/truth_counts.sum()
            hindsight = plan(truth)['expected']
            saved = {}
            for regime in protocol['regimes']:
                for seed in protocol['adapter']['sample_seeds']:
                    samples = select_samples(archive,regime,seed)
                    if samples is None:
                        skipped.append(dict(task_index=task['index'],regime=regime,seed=seed,
                                            reason='fewer_than_256_target_adaptation_records'))
                        continue
                    for model_name in protocol['estimation_models']:
                        with Timer(device) as fitted:
                            model = estimate(count_samples(samples[None],6),model_name)
                        raw_scores = predictive_scores(model,truth)
                        full_fitted = plan(model)['expected']
                        for method in protocol['representations']:
                            with Timer(device) as constructed:
                                code,k = encode(model,method)
                                compact = push(model,code,k)
                                policy = plan(compact)
                            truth_code = push(truth,code,k)
                            scores = predictive_scores(compact,truth_code)
                            actual = realized(truth_code,policy)
                            empirical_code_hindsight = plan(truth_code)['expected']
                            fitted_loss = full_fitted-policy['expected']
                            empirical_compression = hindsight-empirical_code_hindsight
                            empirical_model = empirical_code_hindsight-actual['utility']
                            maxerr = (hindsight-actual['utility']-empirical_compression-empirical_model).abs().max()
                            maximum_decomposition_error = max(maximum_decomposition_error,float(maxerr))
                            d = float(dispersion(model,code,compact)[0])
                            primary = regime == protocol['primary']['regime'] and (
                                model_name == protocol['primary']['model'] or method == 'identity')
                            if primary:
                                trace = []
                                for budget in protocol['planner']['query_budgets']:
                                    for ci,cost in enumerate(protocol['planner']['query_costs']):
                                        action,queries,seen = replay(policy,code.to(torch.uint8),
                                            (held//4)%64,held%4,budget,ci)
                                        utility = torch.where(action == 2,0.,
                                            torch.where(action == held//256,1.,-2.)).double()-cost*queries.double()
                                        error = abs(float(utility.mean())-float(actual['utility'][0,budget,ci]))
                                        maximum_replay_error = max(maximum_replay_error,error)
                                        assert error < 1e-12
                                        assert bool((queries <= budget).all())
                                        assert torch.equal(((seen&1)>0).long()+((seen&2)>0).long(),queries)
                                        trace.append(torch.stack((action,queries,seen),1).to(torch.uint8))
                                        primary_trajectory_records += len(held)
                                saved[f'{seed}:{model_name}:{method}'] = dict(
                                    samples=samples,model=model,code=code.to(torch.uint8),
                                    compact=compact,choices=policy['choices'],values=policy['values'],
                                    expected=policy['expected'],traces=torch.stack(trace))
                            for budget in protocol['planner']['query_budgets']:
                                for ci,cost in enumerate(protocol['planner']['query_costs']):
                                    writer.writerow(dict(task_index=task['index'],dataset=task['dataset'],
                                        environment=task['environment'],task=task['task'],regime=regime,
                                        sample_seed=seed,sample_count=len(samples),model=model_name,
                                        representation=method,budget=budget,cost=cost,
                                        utility=float(actual['utility'][0,budget,ci]),
                                        queries=float(actual['queries'][0,budget,ci]),
                                        abstention=float(actual['abstention'][0,budget,ci]),
                                        estimated_utility=float(policy['expected'][0,budget,ci]),
                                        fitted_compression_loss=float(fitted_loss[0,budget,ci]),
                                        fitted_dispersion=d,empirical_hindsight_utility=float(hindsight[0,budget,ci]),
                                        empirical_compression_loss=float(empirical_compression[0,budget,ci]),
                                        empirical_model_loss=float(empirical_model[0,budget,ci]),
                                        **scores,raw_posterior_brier=raw_scores['posterior_brier'],
                                        raw_posterior_log_loss=raw_scores['posterior_log_loss'],
                                        raw_joint_conditional_log_loss=raw_scores['joint_conditional_log_loss'],
                                        construction_seconds=fitted.seconds+constructed.seconds,
                                        evaluation_records=len(held)))
                                    rows += 1
            torch.save(saved,out/f'primary_task_{task["index"]:03}.pt')
            print(json.dumps({'evaluated_task':task['index'],'dataset':task['dataset'],
                              'environment':task['environment'],'rows_so_far':rows,
                              'elapsed_seconds':time.perf_counter()-start}),flush=True)
    verify_lock()
    write(out/'summary.json',dict(metric_rows=rows,primary_trajectory_records=primary_trajectory_records,
        maximum_replay_error=maximum_replay_error,maximum_decomposition_error=maximum_decomposition_error,
        skipped_secondary_regimes=skipped,evaluation_seconds=time.perf_counter()-start,
        peak_gpu_allocated_bytes=torch.cuda.max_memory_allocated(device),
        peak_gpu_reserved_bytes=torch.cuda.max_memory_reserved(device),
        empirical_hindsight_oracle_is_not_a_population_oracle=True,
        primary_selection_changed=False))


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('command',choices=['lock','acquire','prepare','evaluate'])
    args = parser.parse_args()
    if args.command == 'lock': return lock()
    verify_lock()
    protocol = json.loads(PROTOCOL.read_text())
    if args.command == 'acquire': return acquire(protocol)
    device = protocol['device']
    torch.cuda.set_device(device)
    torch.backends.cuda.matmul.allow_tf32 = False
    torch.backends.cudnn.allow_tf32 = False
    if args.command == 'prepare': return prepare(protocol)
    return evaluate(protocol)


if __name__ == '__main__': main()