morphtree 2.1.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- morphtree/__init__.py +28 -0
- morphtree/__main__.py +3 -0
- morphtree/_experimental/__init__.py +1 -0
- morphtree/_experimental/admission.py +364 -0
- morphtree/_experimental/base.py +215 -0
- morphtree/_experimental/component_cache_v2.py +263 -0
- morphtree/_experimental/evolution_report.py +307 -0
- morphtree/_experimental/execution_v2.py +697 -0
- morphtree/_experimental/facade.py +289 -0
- morphtree/_experimental/integrated.py +748 -0
- morphtree/_experimental/learning.py +418 -0
- morphtree/_experimental/manifest.json +204 -0
- morphtree/_experimental/numeric_fixture.py +21 -0
- morphtree/_experimental/parameter_contract.py +38 -0
- morphtree/_experimental/postfit.py +51 -0
- morphtree/_experimental/preprocessing.py +133 -0
- morphtree/_experimental/processes.py +191 -0
- morphtree/_experimental/producer.py +542 -0
- morphtree/_experimental/product.py +156 -0
- morphtree/_experimental/product_cli.py +43 -0
- morphtree/_experimental/roundtrip_v3.py +311 -0
- morphtree/_experimental/runtime_io.py +65 -0
- morphtree/_experimental/sampling.py +26 -0
- morphtree/_experimental/support.py +281 -0
- morphtree/_experimental/target.py +59 -0
- morphtree/_experimental/training.py +53 -0
- morphtree/_experimental/worker.py +682 -0
- morphtree/_installed.py +17 -0
- morphtree/_native/__init__.py +1 -0
- morphtree/_native/bin/win_amd64/morphtree_kernels.dll +0 -0
- morphtree/_native/buffers.py +82 -0
- morphtree/_native/loader.py +39 -0
- morphtree/_native/manifest.json +29 -0
- morphtree/_native/runtime.py +48 -0
- morphtree/_native/src/histogram.c +61 -0
- morphtree/_native/src/traversal.c +51 -0
- morphtree/api.py +388 -0
- morphtree/api_compat.py +65 -0
- morphtree/backend.py +162 -0
- morphtree/booster.py +252 -0
- morphtree/cli.py +52 -0
- morphtree/config.py +262 -0
- morphtree/control.py +20 -0
- morphtree/data.py +300 -0
- morphtree/diagnostics.py +158 -0
- morphtree/estimators.py +156 -0
- morphtree/examples.py +25 -0
- morphtree/exporting.py +224 -0
- morphtree/legacy_artifacts.py +32 -0
- morphtree/metrics.py +156 -0
- morphtree/model.py +1010 -0
- morphtree/mutations.py +205 -0
- morphtree/native_lab_backend.py +79 -0
- morphtree/numpy_backend.py +180 -0
- morphtree/optuna_tuner.py +154 -0
- morphtree/persistence.py +414 -0
- morphtree/prediction_output.py +35 -0
- morphtree/prepared_bins.py +82 -0
- morphtree/public.py +106 -0
- morphtree/resource_plan.py +170 -0
- morphtree/scalable.py +781 -0
- morphtree/scalable_backend.py +74 -0
- morphtree/scalable_checkpoint.py +129 -0
- morphtree/scalable_data.py +255 -0
- morphtree/scalable_receipts.py +92 -0
- morphtree/scalable_target.py +68 -0
- morphtree/scalable_variants.py +133 -0
- morphtree/sklearn_view.py +79 -0
- morphtree/source_markers.py +25 -0
- morphtree/species.py +47 -0
- morphtree/temporary.py +65 -0
- morphtree/training_fragment.py +130 -0
- morphtree/tree.py +231 -0
- morphtree/tuning.py +150 -0
- morphtree-2.1.0.dist-info/METADATA +212 -0
- morphtree-2.1.0.dist-info/RECORD +79 -0
- morphtree-2.1.0.dist-info/WHEEL +4 -0
- morphtree-2.1.0.dist-info/entry_points.txt +2 -0
- morphtree-2.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
"""Thin installed v2 adapter: existing admission/store/native executor only.
|
|
2
|
+
|
|
3
|
+
Software fixture operations reserve cache intents but never call native fit.
|
|
4
|
+
LIVE is intentionally closed until the complete finite worker/owner activation
|
|
5
|
+
profile is separately qualified; no API call, cache or old permit opens it.
|
|
6
|
+
"""
|
|
7
|
+
import copy
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from contextvars import ContextVar
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from dataclasses import asdict, replace
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from morphtree.config import AlgorithmGenome, HyperParams
|
|
15
|
+
from morphtree.persistence import read_json, LoadLimits
|
|
16
|
+
from .admission import AdmissionError, digest, sha, installed_identity
|
|
17
|
+
from .runtime_io import configure, artifact_path, WORKDIR, reference
|
|
18
|
+
from .target import _NamedRows, _split, detect_target
|
|
19
|
+
from .preprocessing import _m26_rows
|
|
20
|
+
from .training import NativeExecutor, NATIVE
|
|
21
|
+
from . import component_cache_v2 as cache
|
|
22
|
+
|
|
23
|
+
ACTIVE=ContextVar('comp002-owned-v2-session',default=None)
|
|
24
|
+
PUBLIC_SCOPE=ContextVar('product004-owned-public-qualification',default=None)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PreFitBoundary(RuntimeError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SearchBudgetExhausted(TimeoutError):
|
|
32
|
+
"""Search cutoff inside the unchanged hard call budget;not a new permit."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def require_session():
|
|
37
|
+
session=ACTIVE.get()
|
|
38
|
+
if type(session) is not Session:
|
|
39
|
+
from .producer import Session as MultiphaseSession, PIPE_CURRENT
|
|
40
|
+
if type(session) is not MultiphaseSession or PIPE_CURRENT.get()!=session.policy_pin:
|
|
41
|
+
raise AdmissionError({'status':'BLOCKED','reason':'EXACT_INSTALLED_V2_SESSION_REQUIRED'})
|
|
42
|
+
session.check()
|
|
43
|
+
return session
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def validate_model(path, value):
|
|
47
|
+
"""M026B0 task checks, with exact original producer seed before safe load.
|
|
48
|
+
|
|
49
|
+
Seed stays in the request/provenance, not restored from arbitrary report.
|
|
50
|
+
Loaded normalized reports are not mistaken for fresh producer witnesses.
|
|
51
|
+
"""
|
|
52
|
+
task=value['task']; data=read_json(path, LoadLimits(max_bytes=16*1024**2))
|
|
53
|
+
expected={'binary':'MorphTreeModel','regression':'MorphTreeRegressionModel','multiclass':'MorphTreeMulticlassModel'}[task]
|
|
54
|
+
if task!='binary' and value['structure']!=AlgorithmGenome().to_dict():
|
|
55
|
+
from morphtree.config import TASK_GROWTH_FORMATS,validate_task_genome
|
|
56
|
+
validate_task_genome(task,AlgorithmGenome(**value['structure']))
|
|
57
|
+
expected=TASK_GROWTH_FORMATS[task]
|
|
58
|
+
if data.get('format')!=expected:
|
|
59
|
+
raise ValueError('Native task mismatch')
|
|
60
|
+
params=data['algorithm']['parameters'] if task=='binary' else data['parameters']
|
|
61
|
+
if params!=value['params'] or data['n_features']!=value['features']:
|
|
62
|
+
raise ValueError('Native actual parameters/features differ')
|
|
63
|
+
if task=='binary' and data['algorithm']['genome']!=value['structure']:
|
|
64
|
+
raise ValueError('Native genome differs')
|
|
65
|
+
if task!='binary' and data.get('genome',AlgorithmGenome().to_dict())!=value['structure']:
|
|
66
|
+
raise ValueError('Native nonbinary payload and actual requested genome differ')
|
|
67
|
+
if task=='multiclass' and (data['strategy']!='ovr' or data['classes']!=value['preprocessing']['encoded_classes']):
|
|
68
|
+
raise ValueError('OvR columns differ')
|
|
69
|
+
# Exact producer payload is persisted before the safe loader projects report.
|
|
70
|
+
if data['report'].get('backend')!='python-stdlib' or data['report'].get('config',{}).get('seed')!=value['seed']:
|
|
71
|
+
raise ValueError('Missing/mismatched original producer seed/backend witness')
|
|
72
|
+
model=NATIVE[task]._from_payload(data, limits=LoadLimits(max_bytes=16*1024**2), backend='pure')
|
|
73
|
+
k=len(value['preprocessing']['encoded_classes']) if task=='multiclass' else 1
|
|
74
|
+
if len(model.model_.trees)!=value['params']['n_estimators']*k:
|
|
75
|
+
raise ValueError('Actual scalar trees differ')
|
|
76
|
+
return model
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Session:
|
|
80
|
+
"""Adapter to the qualified finite primitives, not a second scheduler.
|
|
81
|
+
|
|
82
|
+
One explicit AdmissionEngine and immutable source/plan. Canonical per-key
|
|
83
|
+
leases/INTENT/COMPLETE/STOP come from ComponentStore. No implicit retraining.
|
|
84
|
+
"""
|
|
85
|
+
def __init__(self, engine, *, root, plan, fixture=None):
|
|
86
|
+
if ACTIVE.get() is not None:raise RuntimeError('No nested session construction')
|
|
87
|
+
configure(root); self.root=artifact_path(root); self.engine=engine
|
|
88
|
+
if self.root!=engine.workdir or self.root!=Path(engine.q['input_root']).resolve():
|
|
89
|
+
raise ValueError('Exact admitted input/output scope required')
|
|
90
|
+
self.plan=copy.deepcopy(plan); self.pin=digest(plan)
|
|
91
|
+
public=plan.get('mode')=='PUBLIC_USER'
|
|
92
|
+
required={'schema','mode','source','protocol','seed','params','structural','max_requests','seconds'}|({'user_limits'} if public else set())
|
|
93
|
+
if set(plan)!=required or plan['schema']!='comp002-v2-session-v1':
|
|
94
|
+
raise ValueError('Exact finite versioned profile required')
|
|
95
|
+
if plan['protocol']!=engine.q['protocol']:
|
|
96
|
+
raise ValueError('Session/AdmissionEngine protocol mismatch')
|
|
97
|
+
self.mode=plan['mode']; self.structural=plan['structural']
|
|
98
|
+
if self.mode not in ('FIXTURE','PREFIT','LIVE','PUBLIC_USER') or type(self.structural) is not bool:
|
|
99
|
+
raise ValueError('Explicit mode/structural opt-in required')
|
|
100
|
+
if self.mode=='LIVE':
|
|
101
|
+
raise AdmissionError({'status':'BLOCKED','reason':'COMP002_LIVE_WORKER_OWNER_PROFILE_NOT_YET_QUALIFIED'})
|
|
102
|
+
if self.mode=='FIXTURE' and not callable(fixture) or self.mode!='FIXTURE' and fixture is not None:
|
|
103
|
+
raise ValueError('Explicit fixture provider only in software mode')
|
|
104
|
+
if public and engine.q.get('usage')!='ORDINARY_USER_NOT_SCIENTIFIC':
|
|
105
|
+
raise ValueError('Public Session cannot use a scientific qualification')
|
|
106
|
+
if not public and engine.q.get('usage')=='ORDINARY_USER_NOT_SCIENTIFIC':
|
|
107
|
+
raise ValueError('User qualification cannot enter scientific/fixture mode')
|
|
108
|
+
if type(plan['max_requests']) is not int or not 1<=plan['max_requests']<=(128 if public else 32) or not 0<plan['seconds']<=(300 if public else 180):
|
|
109
|
+
raise ValueError('Frozen software scope at most32 requests/180s')
|
|
110
|
+
HyperParams(**plan['params'])
|
|
111
|
+
if plan['params']['n_estimators']>8 or type(plan['seed']) is not int:
|
|
112
|
+
raise ValueError('Existing finite adapter bound')
|
|
113
|
+
self.fixture=fixture; self.started=time.monotonic(); self.prepared=None
|
|
114
|
+
self.physical=0; self.prefit=0; self.fixture_calls=0
|
|
115
|
+
self.component=None; self.logical=0; self.root.mkdir(exist_ok=True)
|
|
116
|
+
self._oof_capture=None; self._diagnostic_proofs={}
|
|
117
|
+
if public:
|
|
118
|
+
limits=plan['user_limits']
|
|
119
|
+
if set(limits)!={'calls','scalar_trees','trials','artifact_bytes'} or limits['calls']!=128 or limits['scalar_trees']!=8192 or type(limits['trials']) is not int or not 0<=limits['trials']<=4 or limits['artifact_bytes']!=32*1024**2:
|
|
120
|
+
raise ValueError('Exact bounded ordinary-user resource profile required')
|
|
121
|
+
if engine.q['limits']['fits']!=limits['calls'] or engine.q['limits']['trials']!=limits['trials']:
|
|
122
|
+
raise ValueError('User admission and execution budgets must match')
|
|
123
|
+
self._user_limits=copy.deepcopy(limits);self.phase='FAMILY';self.frozen=None;self._user_trials=0
|
|
124
|
+
self._user_started=0;self._user_completed=0;self._user_trees=0;self._closed=False
|
|
125
|
+
self._search_deadline=None;self._search_closed=False
|
|
126
|
+
self._component_costs=[]
|
|
127
|
+
policy=artifact_path('SESSION.json')
|
|
128
|
+
if policy.exists(): raise ValueError('New software session; never renew/replay a partial run')
|
|
129
|
+
cache.configure(self.root,engine.package)
|
|
130
|
+
cache._write(policy,{'plan':self.plan,'runtime':engine.identity,'deadline':engine.deadline})
|
|
131
|
+
self.policy_sha=sha(policy)
|
|
132
|
+
self.store=cache.MultitaskStore(artifact_path('store'),max_compute_jobs=plan['max_requests'],seconds=plan['seconds'])
|
|
133
|
+
self.public_scope=PUBLIC_SCOPE.get() if public else None
|
|
134
|
+
if self.public_scope is not None:self.public_scope.bind_session(self)
|
|
135
|
+
self.check()
|
|
136
|
+
|
|
137
|
+
def check(self):
|
|
138
|
+
if self.mode=='PUBLIC_USER':
|
|
139
|
+
if self.public_scope is not None:
|
|
140
|
+
if PUBLIC_SCOPE.get() is not self.public_scope:raise PermissionError('Lost public campaign binding')
|
|
141
|
+
self.public_scope.check()
|
|
142
|
+
# Hot cooperative cancellation checks do not hash the complete
|
|
143
|
+
# dataset/runtime on every split. Full integrity remains mandatory
|
|
144
|
+
# at activation, component entry and before component commitment.
|
|
145
|
+
if self._closed or self.engine.stop.exists() or (self.root/'STOP').exists() or (self.store.root/'STOP').exists():
|
|
146
|
+
raise RuntimeError('STOP permanent for this user call')
|
|
147
|
+
if time.time()>=self.engine.deadline or time.monotonic()-self.started>=self.plan['seconds']:
|
|
148
|
+
raise TimeoutError('Finite ordinary-user deadline')
|
|
149
|
+
if getattr(self,'_component_deadline',None) is not None and time.monotonic()>=self._component_deadline:
|
|
150
|
+
raise TimeoutError('Ordinary-user cooperative component deadline')
|
|
151
|
+
if (not self._search_closed and self._search_deadline is not None
|
|
152
|
+
and time.monotonic()>=self._search_deadline):
|
|
153
|
+
raise SearchBudgetExhausted('Search window ended;hard call budget is unchanged')
|
|
154
|
+
return
|
|
155
|
+
configure(self.root);cache.configure(self.root,self.engine.package)
|
|
156
|
+
if digest(self.plan)!=self.pin or sha(self.root/'SESSION.json')!=self.policy_sha:
|
|
157
|
+
raise ValueError('Changed immutable session')
|
|
158
|
+
if digest(self.engine.q)!=self.engine._q_pin:
|
|
159
|
+
raise ValueError('Mutated qualification dossier')
|
|
160
|
+
if installed_identity(self.engine.package,self.engine.q)!=self.engine.identity:
|
|
161
|
+
raise ValueError('Changed installed runtime')
|
|
162
|
+
if self.engine.stop.exists() or (self.root/'STOP').exists() or (self.store.root/'STOP').exists():
|
|
163
|
+
raise RuntimeError('STOP permanent including cache hits')
|
|
164
|
+
if time.time()>=self.engine.deadline or time.monotonic()-self.started>=self.plan['seconds']:
|
|
165
|
+
raise TimeoutError('Original finite deadline')
|
|
166
|
+
|
|
167
|
+
def begin_search(self):
|
|
168
|
+
if self.mode!='PUBLIC_USER':return
|
|
169
|
+
self.check()
|
|
170
|
+
if self._search_deadline is not None or self._search_closed:raise PermissionError('Search cannot renew')
|
|
171
|
+
reserve=min(10.,self.plan['seconds']*.1)
|
|
172
|
+
self._search_deadline=self.started+self.plan['seconds']-reserve
|
|
173
|
+
self.event('USER_SEARCH_WINDOW',finalization_reserve_seconds=reserve,hard_seconds=self.plan['seconds'])
|
|
174
|
+
|
|
175
|
+
def finish_search(self,reason):
|
|
176
|
+
if self.mode!='PUBLIC_USER' or ACTIVE.get() is not self:
|
|
177
|
+
raise PermissionError('Only the active ordinary user call may close search')
|
|
178
|
+
# Irreversible closing can suppress only the SOFT search cutoff. Hard
|
|
179
|
+
# deadline,STOP,campaign binding and component deadline remain checked.
|
|
180
|
+
self._search_closed=True
|
|
181
|
+
self.check()
|
|
182
|
+
self.event('USER_SEARCH_CLOSED',reason=reason,new_learning_allowed=False)
|
|
183
|
+
|
|
184
|
+
def _learning_open(self):
|
|
185
|
+
self.check()
|
|
186
|
+
if self.mode=='PUBLIC_USER' and self._search_closed:
|
|
187
|
+
raise PermissionError('Search closed;only retained-state finalization remains')
|
|
188
|
+
|
|
189
|
+
def admit_batch(self,task,family,params,*,folds=1,kind='FAMILY'):
|
|
190
|
+
"""Conservative preflight,not a reservation or a timing guarantee.
|
|
191
|
+
|
|
192
|
+
Actual physical/global reservations remain at the native boundary.
|
|
193
|
+
A batch that cannot fit the remaining quota is never partially started.
|
|
194
|
+
Observed inclusive component costs only screen likely overruns;hard
|
|
195
|
+
cancellation still applies and uncommitted children are never adopted.
|
|
196
|
+
"""
|
|
197
|
+
if self.mode!='PUBLIC_USER':return
|
|
198
|
+
self._learning_open()
|
|
199
|
+
if task!=self.prepared['task'] or family not in ('boost','bag') or type(folds) is not int or not 1<=folds<=2:
|
|
200
|
+
raise ValueError('Exact task/family and at most two TRAIN folds required')
|
|
201
|
+
p=HyperParams(**params);calls=folds*(1 if family=='boost' else p.n_estimators)
|
|
202
|
+
scalar=folds*p.n_estimators*(len(self.prepared['classes']) if task=='multiclass' else 1)
|
|
203
|
+
reason=None
|
|
204
|
+
if self._user_started+calls>self._user_limits['calls'] or self._user_trees+scalar>self._user_limits['scalar_trees']:
|
|
205
|
+
reason='WHOLE_BATCH_EXCEEDS_REMAINING_CALL_QUOTA'
|
|
206
|
+
if self.public_scope is not None:
|
|
207
|
+
used=self.public_scope.used();profile=self.public_scope.policy['profile']
|
|
208
|
+
if used['calls']+calls>profile['calls'] or used['scalar_trees']+scalar>profile['scalar_trees']:
|
|
209
|
+
reason='WHOLE_BATCH_EXCEEDS_REMAINING_GLOBAL_QUOTA'
|
|
210
|
+
observed=max(self._component_costs,default=0.)
|
|
211
|
+
estimate=1.5*observed*calls if observed else None
|
|
212
|
+
remaining=None if self._search_deadline is None else self._search_deadline-time.monotonic()
|
|
213
|
+
if estimate is not None and remaining is not None and estimate>=remaining:
|
|
214
|
+
reason='OBSERVED_COST_SCREEN_NO_ROOM_FOR_WHOLE_BATCH'
|
|
215
|
+
self.event('USER_BATCH_PREFLIGHT',batch_kind=kind,task=task,family=family,calls=calls,
|
|
216
|
+
scalar_trees=scalar,estimated_seconds=estimate,remaining_search_seconds=remaining,
|
|
217
|
+
timing_scope='OPERATIONAL_HEURISTIC_NOT_GUARANTEED_OR_BENCHMARK',
|
|
218
|
+
decision='STOP_BEFORE_BATCH' if reason else 'ALLOW_WITH_EXISTING_PER_COMPONENT_GUARDS',reason=reason)
|
|
219
|
+
if reason:raise SearchBudgetExhausted(reason)
|
|
220
|
+
|
|
221
|
+
@contextmanager
|
|
222
|
+
def activate(self):
|
|
223
|
+
if self.mode=='PUBLIC_USER':self._user_integrity()
|
|
224
|
+
self.check()
|
|
225
|
+
if ACTIVE.get() is not None: raise RuntimeError('No nested concurrent session')
|
|
226
|
+
with cache.exclusive_lock(artifact_path('session-writer')):
|
|
227
|
+
token=ACTIVE.set(self)
|
|
228
|
+
try: yield self
|
|
229
|
+
finally: ACTIVE.reset(token)
|
|
230
|
+
|
|
231
|
+
def event(self, kind, **values):
|
|
232
|
+
if self.mode=='PUBLIC_USER':
|
|
233
|
+
op=values.get('value',{}).get('operation')
|
|
234
|
+
if kind=='ORCHESTRATION' and op=='evolution_intent':
|
|
235
|
+
if self.frozen is not None:raise ValueError('No mutation after public freeze')
|
|
236
|
+
self.phase='STRUCTURE'
|
|
237
|
+
if kind=='ORCHESTRATION' and op=='structure_frozen' and 'packet' in values['value']:
|
|
238
|
+
if self.frozen is not None and self.frozen!=values['value']['packet']:
|
|
239
|
+
raise ValueError('Frozen user program changed')
|
|
240
|
+
self.frozen=copy.deepcopy(values['value']['packet']);self.phase='NUMERIC'
|
|
241
|
+
self.store.event(kind,phase=self.phase,**values);return
|
|
242
|
+
self.store.event(kind, **values)
|
|
243
|
+
|
|
244
|
+
def counters(self):
|
|
245
|
+
p=self.store.root/'EVENTS.jsonl'
|
|
246
|
+
rows=[json.loads(line) for line in p.read_text('utf8').splitlines()] if p.exists() else []
|
|
247
|
+
result={'physical_fits':self.physical,'fixture_operations':self.fixture_calls,
|
|
248
|
+
'prefit_entries':self.prefit,'trials':0,
|
|
249
|
+
'OOF':sum(r['event']=='NATIVE_FIT_COMPLETED' and r.get('phase')=='OOF' for r in rows),
|
|
250
|
+
'OOF_count_semantics':'COMPLETED_PHYSICAL_NATIVE_FIT_COMPONENTS_NOT_FIXTURE_OR_CACHE',
|
|
251
|
+
'OOF_fixture_components':sum(r.get('components',0) for r in rows
|
|
252
|
+
if r['event']=='OOF_CONTEXT_PRODUCED' and r.get('physical_fits_scope')=='FIXTURE'),
|
|
253
|
+
'calibrators':0,
|
|
254
|
+
'store':self.store.counts(),'elapsed_operational_seconds':time.monotonic()-self.started,
|
|
255
|
+
'CPU':'UNKNOWN','peak_RSS':'UNKNOWN','performance_qualified':False}
|
|
256
|
+
if self.mode=='PUBLIC_USER':
|
|
257
|
+
created=[r for r in rows if r['event']=='NUMERIC_TRIAL_ASKED']
|
|
258
|
+
result.update(physical_fits_started=self._user_started,physical_fits_completed=self._user_completed,
|
|
259
|
+
scalar_trees_started=self._user_trees,Optuna_trials=sum(r['real'] for r in created),trials=sum(r['real'] for r in created),
|
|
260
|
+
trial_reservations=self._user_trials,simulated_trials=sum(not r['real'] for r in created),
|
|
261
|
+
scope='ORDINARY_USER_OPERATIONAL_NOT_SCIENTIFIC',closed=self._closed)
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
def evolution_context(self, selected, program):
|
|
265
|
+
"""Use only the exact Session's admitted fixed-parent fold operation.
|
|
266
|
+
|
|
267
|
+
Base software sessions stay fixture-only. A versioned owned worker may
|
|
268
|
+
implement oof_operation, with separate owner/role/phase/cumulative caps.
|
|
269
|
+
Constructing a context never creates that authorization.
|
|
270
|
+
"""
|
|
271
|
+
self.check()
|
|
272
|
+
# A repeated unchanged parent may reuse a revalidated context; it does
|
|
273
|
+
# not consume another inspect token or replay physical fitting.
|
|
274
|
+
for proof in getattr(self,'_diagnostic_proofs',{}).values():
|
|
275
|
+
if proof['parent']==selected and proof['context']['program_sha256']==digest(program):
|
|
276
|
+
if self.verify_evolution_context(proof['context'],selected):
|
|
277
|
+
self.event('OOF_CONTEXT_REUSED',context=digest(proof['context']),physical_fits=0)
|
|
278
|
+
return copy.deepcopy(proof['context'])
|
|
279
|
+
with self.oof_operation(selected,program) as evidence:
|
|
280
|
+
if evidence is None:return None
|
|
281
|
+
return self._produce_oof_context(selected,program,evidence)
|
|
282
|
+
|
|
283
|
+
@contextmanager
|
|
284
|
+
def oof_operation(self, selected, program):
|
|
285
|
+
if self.mode=='PUBLIC_USER':
|
|
286
|
+
if not self.structural or self.frozen is not None:
|
|
287
|
+
yield None;return
|
|
288
|
+
self._user_integrity();previous=self.phase;self.phase='OOF'
|
|
289
|
+
try:yield 'AUTHENTIC_USER_TRAIN_FOLDS_NOT_INDEPENDENT_SCIENTIFIC_VALIDATION'
|
|
290
|
+
finally:self.phase=previous
|
|
291
|
+
return
|
|
292
|
+
yield ('SOFTWARE_FOLD_FIXTURE_NOT_TRAINED_OOF'
|
|
293
|
+
if type(self) is Session and self.mode=='FIXTURE' and self.structural else None)
|
|
294
|
+
|
|
295
|
+
def _produce_oof_context(self, selected, program, evidence):
|
|
296
|
+
self.check();start=time.monotonic()
|
|
297
|
+
from .integrated import learning_program
|
|
298
|
+
from .sampling import oof_folds
|
|
299
|
+
from .preprocessing import TrainEncoder
|
|
300
|
+
from morphtree.diagnostics import ErrorMemory, ErrorProfiler
|
|
301
|
+
import math
|
|
302
|
+
a=self.prepared
|
|
303
|
+
if a is None:return None
|
|
304
|
+
task=a['task']
|
|
305
|
+
if task!='binary' and not callable(getattr(ErrorProfiler,'profile_task',None)):
|
|
306
|
+
return None # Older native runtimes keep their binary-only boundary.
|
|
307
|
+
if program!=learning_program(selected) or selected['params']!=self.plan['params']:
|
|
308
|
+
raise ValueError('OOF requires exact current fixed program and numeric parameters')
|
|
309
|
+
raw=[a['rows'][i] for i in a['ti']];y=[a['y'][i] for i in a['ti']]
|
|
310
|
+
ids=['ROW:'+str(i) for i in a['ti']]
|
|
311
|
+
parent_encoder=TrainEncoder.from_dict(selected['recipe']['encoder'])
|
|
312
|
+
encoded=parent_encoder.transform(_NamedRows(raw,a['names']))
|
|
313
|
+
if digest({'X':encoded,'y':y,'ids':a['ti']})!=selected['learning_identity']['TRAIN']:
|
|
314
|
+
raise ValueError('OOF parent differs from actual fixed TRAIN')
|
|
315
|
+
try:held_folds=oof_folds(y,task,self.plan['seed'])
|
|
316
|
+
except ValueError:
|
|
317
|
+
self.event('OOF_ABSTAIN',reason='INSUFFICIENT_TRAIN_CLASS_OR_ROW_SUPPORT',task=task)
|
|
318
|
+
return None
|
|
319
|
+
self.admit_batch(task,selected['family'],selected['params'],folds=len(held_folds),kind='OOF')
|
|
320
|
+
pred=[None]*len(y);proofs=[]
|
|
321
|
+
# Write the plan before component intents. No SEARCH labels/predictions
|
|
322
|
+
# enter folds, feature fitting or the profiler.
|
|
323
|
+
key=digest({'parent':selected,'program':program})
|
|
324
|
+
plan={'parent_sha256':digest(selected),'program_sha256':digest(program),
|
|
325
|
+
'TRAIN_ids':ids,'held_folds':held_folds,'mode':self.mode,
|
|
326
|
+
'evidence':evidence,'fold_local_preprocessing':True}
|
|
327
|
+
path=artifact_path('OOF-'+key+'.json')
|
|
328
|
+
if path.exists():
|
|
329
|
+
if cache._read(path)!=plan:raise ValueError('Changed fixed-parent OOF plan')
|
|
330
|
+
else:cache._write(path,plan)
|
|
331
|
+
for held in held_folds:
|
|
332
|
+
self.check();fit=[i for i in range(len(y)) if i not in set(held)]
|
|
333
|
+
if not fit or not held or (task!='regression' and set(y[i] for i in fit)!=set(y)):
|
|
334
|
+
return None # Do not repair labels or leak held rows into fit.
|
|
335
|
+
e=TrainEncoder().fit([raw[i] for i in fit]);e.state_['names']=a['names']
|
|
336
|
+
fx=e.transform(_NamedRows([raw[i] for i in fit],a['names']))
|
|
337
|
+
hx=e.transform(_NamedRows([raw[i] for i in held],a['names']))
|
|
338
|
+
fy=[y[i] for i in fit];fold_ids=[ids[i] for i in fit]
|
|
339
|
+
identity={'preprocessing':e.to_dict(),'seed':self.plan['seed'],'task':task,
|
|
340
|
+
'family':selected['family'],'TRAIN_ids':fold_ids,
|
|
341
|
+
'actual_TRAIN_X':fx,'actual_TRAIN_y':fy}
|
|
342
|
+
# The owned worker validates this exact raw complement/encoder and
|
|
343
|
+
# sample again before a native entry. It is not caller admission.
|
|
344
|
+
self._fold_scope={'fit':fit,'held':held,'TRAIN_ids':ids,'encoder':e.to_dict(),
|
|
345
|
+
'X':fx,'y':fy,'ids':fold_ids,'parent':copy.deepcopy(selected),
|
|
346
|
+
'program':copy.deepcopy(program),'family':selected['family']}
|
|
347
|
+
self._oof_capture=[]
|
|
348
|
+
try:
|
|
349
|
+
model=self.executor(None).fit_family(task,selected['family'],fx,fy,
|
|
350
|
+
seed=self.plan['seed'],params=HyperParams(**selected['params']),
|
|
351
|
+
genome=AlgorithmGenome(**program['genome']),check=self.check,identity=identity)
|
|
352
|
+
receipts=copy.deepcopy(self._oof_capture)
|
|
353
|
+
finally:self._oof_capture=None;self._fold_scope=None
|
|
354
|
+
values=model.probabilities_or_values(hx)
|
|
355
|
+
for i,v in zip(held,values):pred[i]=v
|
|
356
|
+
proofs.append({'fit':fit,'held':held,'encoder':e.to_dict(),'receipts':receipts,
|
|
357
|
+
'held_X':hx,'prediction':values})
|
|
358
|
+
from .support import prediction_contract
|
|
359
|
+
prediction_contract(task,y,pred)
|
|
360
|
+
px=[[float('nan') if v is None else v for v in row] for row in encoded]
|
|
361
|
+
if task=='binary':
|
|
362
|
+
memory=ErrorMemory(len(y));memory.update(y,pred)
|
|
363
|
+
profile=ErrorProfiler().profile(px,y,memory,px,y,pred)
|
|
364
|
+
else:profile=ErrorProfiler().profile_task(px,y,pred,task)
|
|
365
|
+
profile['interpretation']='Current-program TRAIN-fold associations;not causal or unbiased evidence for the complete adaptive selection procedure'
|
|
366
|
+
profile['acquisition_evidence']=evidence
|
|
367
|
+
allowed=[]
|
|
368
|
+
if program['genome']==AlgorithmGenome().to_dict():
|
|
369
|
+
allowed=[digest(learning_program({'task':task,'family':selected['family'],
|
|
370
|
+
'recipe':{'genome':AlgorithmGenome(growth_policy='depthwise').to_dict()}}))]
|
|
371
|
+
context={'role':'TRAIN_OOF','parent_packet_sha256':digest(selected),
|
|
372
|
+
'program_sha256':digest(program),'learning_identity':copy.deepcopy(selected['learning_identity']),
|
|
373
|
+
'profile':profile,'allowed_programs':allowed,
|
|
374
|
+
'evidence':evidence}
|
|
375
|
+
proof={'context':copy.deepcopy(context),'parent':copy.deepcopy(selected),
|
|
376
|
+
'plan':path.name,'plan_sha256':sha(path),'folds':proofs,'mode':self.mode,
|
|
377
|
+
'acquisition_seconds_operational':time.monotonic()-start,
|
|
378
|
+
'cost_scope':'INCLUSIVE_OPERATIONAL_NOT_PERFORMANCE','CPU':'UNKNOWN','peak_RSS':'UNKNOWN'}
|
|
379
|
+
durable=artifact_path('OOF-PROOF-'+digest(context)+'.json');cache._write(durable,proof)
|
|
380
|
+
self._diagnostic_proofs[digest(context)]={'context':copy.deepcopy(context),
|
|
381
|
+
'parent':copy.deepcopy(selected),'artifact':durable.name,'sha256':sha(durable)}
|
|
382
|
+
self.event('OOF_CONTEXT_PRODUCED',parent=digest(selected),context=digest(context),
|
|
383
|
+
evidence=evidence,components=sum(len(f['receipts']) for f in proofs),
|
|
384
|
+
physical_fits_scope=self.mode)
|
|
385
|
+
return context
|
|
386
|
+
|
|
387
|
+
def verify_evolution_context(self, context, selected):
|
|
388
|
+
"""Reopen committed components and reproduce every held prediction.
|
|
389
|
+
|
|
390
|
+
No caller-provided boolean or metadata hash establishes authenticity.
|
|
391
|
+
The proof is private to this exact software session; trained LIVE OOF
|
|
392
|
+
remains blocked rather than inheriting this fixture qualification.
|
|
393
|
+
"""
|
|
394
|
+
self.check()
|
|
395
|
+
if self.mode=='FIXTURE':
|
|
396
|
+
if context.get('evidence')!='SOFTWARE_FOLD_FIXTURE_NOT_TRAINED_OOF':return False
|
|
397
|
+
elif self.mode=='PUBLIC_USER':
|
|
398
|
+
if type(self) is not Session or context.get('evidence')!='AUTHENTIC_USER_TRAIN_FOLDS_NOT_INDEPENDENT_SCIENTIFIC_VALIDATION':return False
|
|
399
|
+
elif self.mode=='LIVE':
|
|
400
|
+
from .producer import Session as Owned, evolution_profile
|
|
401
|
+
if type(self) is not Owned or not evolution_profile():return False
|
|
402
|
+
self.permit()
|
|
403
|
+
if context.get('evidence')!='AUTHENTIC_FIXED_PROGRAM_TRAIN_FOLDS_ADAPTIVE_UTILITY_UNPROVED':return False
|
|
404
|
+
else:return False
|
|
405
|
+
proof=self._diagnostic_proofs.get(digest(context))
|
|
406
|
+
if proof is None or proof['context']!=context or proof['parent']!=selected:return False
|
|
407
|
+
durable=artifact_path(proof['artifact'])
|
|
408
|
+
if sha(durable)!=proof['sha256']:raise ValueError('Changed durable diagnostic proof')
|
|
409
|
+
proof=cache._read(durable)
|
|
410
|
+
if proof['mode']!=self.mode or proof['context']!=context or proof['parent']!=selected:return False
|
|
411
|
+
if sha(artifact_path(proof['plan']))!=proof['plan_sha256']:raise ValueError('Changed OOF plan')
|
|
412
|
+
from .support import FamilyModel
|
|
413
|
+
from .preprocessing import TrainEncoder
|
|
414
|
+
a=self.prepared;raw=[a['rows'][i] for i in a['ti']];y=[a['y'][i] for i in a['ti']]
|
|
415
|
+
covered=[]
|
|
416
|
+
for fold in proof['folds']:
|
|
417
|
+
self.check();models=[]
|
|
418
|
+
fit,held=fold['fit'],fold['held'];covered+=held
|
|
419
|
+
if set(fit)&set(held) or sorted(fit+held)!=list(range(len(raw))):raise ValueError('Not a TRAIN-only complement')
|
|
420
|
+
e=TrainEncoder().fit([raw[i] for i in fit]);e.state_['names']=a['names']
|
|
421
|
+
if e.to_dict()!=fold['encoder'] or e.transform(_NamedRows([raw[i] for i in held],a['names']))!=fold['held_X']:
|
|
422
|
+
raise ValueError('Fold-local preprocessing or held observations changed')
|
|
423
|
+
for row in fold['receipts']:
|
|
424
|
+
request=cache.FitRequest(cache.canonical(row['request']))
|
|
425
|
+
v=request.value()
|
|
426
|
+
expected=cache.learning_request(
|
|
427
|
+
{'role':'TRAIN','ids':['ROW:'+str(a['ti'][i]) for i in fit],
|
|
428
|
+
'X':e.transform(_NamedRows([raw[i] for i in fit],a['names'])),
|
|
429
|
+
'y':[y[i] for i in fit]},v['indices'],
|
|
430
|
+
**{k:v[k] for k in ('protocol','split_sha256','preprocessing','task','family',
|
|
431
|
+
'structure','params','config','objective','seed','backend','dependencies','sources')})
|
|
432
|
+
if request!=expected or v['structure']!=selected['recipe']['genome'] or v['family']!=selected['family']:
|
|
433
|
+
raise ValueError('Fold component does not bind the actual TRAIN complement/current program')
|
|
434
|
+
receipt=self.store._result(request,artifact_path(row['folder']))
|
|
435
|
+
if digest(receipt)!=row['receipt_sha256']:raise ValueError('Changed OOF committed receipt')
|
|
436
|
+
if request.value()['dependencies']['mode']!=self.mode:raise ValueError('Cross-mode OOF cache provenance')
|
|
437
|
+
if self.mode in ('LIVE','PUBLIC_USER') and not any(r['event']=='NATIVE_FIT_COMPLETED' and r.get('phase')=='OOF' and r.get('key')==request.key for r in self.store.events()):
|
|
438
|
+
raise ValueError('No authenticated completed OOF native call')
|
|
439
|
+
models.append(validate_model(artifact_path(receipt['artifact']['path']),request.value()))
|
|
440
|
+
if not models:raise ValueError('Missing actual fold components')
|
|
441
|
+
model=FamilyModel(selected['task'],models,[1/len(models)]*len(models))
|
|
442
|
+
if model.probabilities_or_values(fold['held_X'])!=fold['prediction']:
|
|
443
|
+
raise ValueError('OOF prediction differs from committed native component')
|
|
444
|
+
if sorted(covered)!=list(range(len(raw))):raise ValueError('Held coverage differs')
|
|
445
|
+
return True
|
|
446
|
+
|
|
447
|
+
def _capture_oof_component(self, request, lease):
|
|
448
|
+
if getattr(self,'_oof_capture',None) is not None:
|
|
449
|
+
self._oof_capture.append({'request':request.value(),
|
|
450
|
+
'folder':lease.folder.relative_to(self.root).as_posix(),'receipt_sha256':digest(lease.result)})
|
|
451
|
+
|
|
452
|
+
def bind_input(self, estimator, X, y):
|
|
453
|
+
self.check(); rows,names=_m26_rows(X); task,targets,classes=detect_target(y,estimator.task)
|
|
454
|
+
if estimator.random_state!=self.plan['seed'] or self.mode!='PUBLIC_USER' and estimator.capacity is not None:
|
|
455
|
+
raise ValueError('Exact seed/profile capacity required')
|
|
456
|
+
if self.mode=='PUBLIC_USER':
|
|
457
|
+
expected=asdict(HyperParams(n_estimators=2,max_leaves=8,max_depth=4,max_bins=15,min_samples_leaf=2,row_fraction=1.))
|
|
458
|
+
if estimator.capacity:expected.update(estimator.capacity)
|
|
459
|
+
if expected!=self.plan['params']:raise ValueError('Public requested capacity differs from immutable session')
|
|
460
|
+
source=self.plan['source']
|
|
461
|
+
if len(rows)!=len(source['rows']) or any(r!={'id':'ROW:'+str(i),'X':rows[i],'y':y[i]} for i,r in enumerate(source['rows'])):
|
|
462
|
+
raise ValueError('Actual supplied data differ from admitted source')
|
|
463
|
+
ti,si=_split(targets,task,self.plan['seed'])
|
|
464
|
+
if source['partitions']!={'TRAIN':['ROW:'+str(i) for i in ti],
|
|
465
|
+
'SEARCH':['ROW:'+str(i) for i in si],'GUARD_FINAL':[]}:
|
|
466
|
+
raise ValueError('Actual ordered split differs')
|
|
467
|
+
if source['OOF']: raise ValueError('No OOF producer admitted in COMP002')
|
|
468
|
+
self.prepared={'rows':rows,'names':names,'y':targets,'classes':classes,'task':task,'ti':ti,'si':si}
|
|
469
|
+
|
|
470
|
+
def parameters(self, unused):
|
|
471
|
+
return HyperParams(**self.plan['params'])
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def source(self):return self.plan['source']
|
|
475
|
+
|
|
476
|
+
@contextmanager
|
|
477
|
+
def orchestration_operation(self, estimator):
|
|
478
|
+
a=self.prepared; p=self.plan['params']; k=len(a['classes']) or 1
|
|
479
|
+
families=['boost','bag'] if estimator.families=='auto' else [estimator.families]
|
|
480
|
+
forms=[{'family':f,'members':1 if f=='boost' else p['n_estimators'],
|
|
481
|
+
'stages':p['n_estimators'] if f=='boost' else 1,'classes':k,
|
|
482
|
+
'genome_sha256':digest(AlgorithmGenome().to_dict())} for f in families]
|
|
483
|
+
form=forms[0] if len(forms)==1 else {'candidates':forms}
|
|
484
|
+
if hasattr(self,'admission_recipe'):
|
|
485
|
+
form=self.admission_recipe(estimator,form)
|
|
486
|
+
request=self.engine.request(operation='inspect',task=a['task'],recipe=form,
|
|
487
|
+
source=self.source,role='TRAIN',protocol=self.engine.q['protocol'],
|
|
488
|
+
authorization=self.engine.q['authorization']['id'],components=[])
|
|
489
|
+
# One exact software operation contains the canonical per-component
|
|
490
|
+
# intents. This inspect capability never itself authorizes training.
|
|
491
|
+
with self.engine.operation(request): yield
|
|
492
|
+
|
|
493
|
+
def optional_optuna(self):
|
|
494
|
+
if self.mode!='PUBLIC_USER':return False
|
|
495
|
+
import importlib.util
|
|
496
|
+
return importlib.util.find_spec('optuna') is not None
|
|
497
|
+
def numeric_admitted(self, executor):
|
|
498
|
+
return self.mode=='PUBLIC_USER' and self._user_limits['trials']>0
|
|
499
|
+
def load_optuna(self):
|
|
500
|
+
if self.mode=='PUBLIC_USER':
|
|
501
|
+
if not self.numeric_admitted(None):raise PermissionError('No numerical user budget')
|
|
502
|
+
import importlib
|
|
503
|
+
from .producer import NumericBinding
|
|
504
|
+
module=importlib.import_module('optuna')
|
|
505
|
+
from importlib.metadata import distribution
|
|
506
|
+
dist=distribution('optuna')
|
|
507
|
+
if module.__version__!='5.0.0' or dist.version!='5.0.0' or not Path(module.__file__).resolve().is_relative_to(Path(dist.locate_file('optuna')).resolve()):
|
|
508
|
+
raise ValueError('Exact supported Optuna5.0.0 distribution required;no shadowed driver')
|
|
509
|
+
return NumericBinding(self,module)
|
|
510
|
+
raise AdmissionError({'status':'BLOCKED','reason':'OPTIONAL_DEPENDENCY_AND_LIVE_TRIAL_PROFILE_REQUIRED'})
|
|
511
|
+
|
|
512
|
+
def reserve_trial(self, *, real_driver=True):
|
|
513
|
+
self._learning_open()
|
|
514
|
+
if self.mode!='PUBLIC_USER' or self.frozen is None or self.phase!='NUMERIC':
|
|
515
|
+
raise PermissionError('User trial requires an already frozen program')
|
|
516
|
+
if type(real_driver) is not bool:raise ValueError('Explicit driver evidence required')
|
|
517
|
+
self.check()
|
|
518
|
+
if self._user_trials>=self._user_limits['trials']:raise RuntimeError('Numerical user budget exhausted')
|
|
519
|
+
if self.public_scope is not None:self.public_scope.reserve(self,operation='trial',trees=0,
|
|
520
|
+
key=digest({'frozen':self.frozen,'ordinal':self._user_trials,'source':digest(self.source)}))
|
|
521
|
+
r=self._user_learning_request('trial',self.frozen['task'],self.frozen['family'],
|
|
522
|
+
HyperParams(**self.frozen['params']),len(self.frozen['recipe']['classes']),self.frozen['recipe']['genome'])
|
|
523
|
+
with self.engine.user_learning(r):pass # Reservation precedes Optuna.ask, not a completed numerical trial.
|
|
524
|
+
self._user_trials+=1
|
|
525
|
+
self.event('NUMERIC_TRIAL_INTENT',real=real_driver,frozen_sha256=digest(self.frozen))
|
|
526
|
+
|
|
527
|
+
def _user_learning_request(self,operation,task,family,params,classes,genome):
|
|
528
|
+
recipe={'family':family,'members':params.n_estimators if family=='bag' and operation=='trial' else 1,'stages':params.n_estimators if family=='boost' else 1,
|
|
529
|
+
'classes':classes if task!='regression' else 1,'genome_sha256':digest(genome)}
|
|
530
|
+
r=self.engine.request(operation=operation,task=task,recipe=recipe,source=self.source,role='TRAIN',
|
|
531
|
+
protocol=self.engine.q['protocol'],authorization=self.engine.q['authorization']['id'],components=[],
|
|
532
|
+
requested_cost={'fits':int(operation=='fit'),'trials':int(operation=='trial')})
|
|
533
|
+
r['component_key']=digest({'operation':operation,'ordinal':self._user_started if operation=='fit' else self._user_trials,'recipe':recipe})
|
|
534
|
+
r['id']=digest({k:v for k,v in r.items() if k!='id'})
|
|
535
|
+
return r
|
|
536
|
+
|
|
537
|
+
def packet_lineage(self,genome):
|
|
538
|
+
if self.mode=='PUBLIC_USER' and self.frozen is not None:
|
|
539
|
+
if genome.to_dict()!=self.frozen['recipe']['genome']:raise ValueError('Changed frozen numerical genome')
|
|
540
|
+
return copy.deepcopy(self.frozen['recipe'].get('lineage',{}))
|
|
541
|
+
return {}
|
|
542
|
+
|
|
543
|
+
def _user_integrity(self):
|
|
544
|
+
if self.mode!='PUBLIC_USER':raise PermissionError('Ordinary-user boundary only')
|
|
545
|
+
self.check()
|
|
546
|
+
configure(self.root);cache.configure(self.root,self.engine.package)
|
|
547
|
+
if digest(self.plan)!=self.pin or sha(self.root/'SESSION.json')!=self.policy_sha or digest(self.engine.q)!=self.engine._q_pin:
|
|
548
|
+
raise ValueError('Mutated ordinary-user plan/qualification')
|
|
549
|
+
if installed_identity(self.engine.package,self.engine.q)!=self.engine.identity:
|
|
550
|
+
raise ValueError('Changed installed ordinary-user runtime')
|
|
551
|
+
if self._user_limits!=self.plan['user_limits']:raise ValueError('Changed ordinary-user resource limits')
|
|
552
|
+
|
|
553
|
+
def close_user_run(self):
|
|
554
|
+
if self.mode!='PUBLIC_USER' or self._closed:return
|
|
555
|
+
self.event('USER_RUN_CLOSED',counts=self.counters(),authority_residual=0)
|
|
556
|
+
self._closed=True
|
|
557
|
+
self.engine.stop_permanently('Ordinary user call finished;never a scientific training permit')
|
|
558
|
+
|
|
559
|
+
def executor(self, estimator):
|
|
560
|
+
session=self
|
|
561
|
+
class Executor(NativeExecutor):
|
|
562
|
+
evidence_level={'PUBLIC_USER':'USER_API_NATIVE_EXECUTION_NOT_SCIENTIFIC_CONFIRMATION','FIXTURE':'COMP003_SOFTWARE_FIXTURE_NOT_TRAINING',
|
|
563
|
+
'PREFIT':'COMP003_PREFIT_NOT_TRAINING','LIVE':'COMP003_EXACT_OWNER_ADMITTED_NATIVE_TRAINING'}[session.mode]
|
|
564
|
+
def execute_native(self, model, X, y, *, identity, cancel):
|
|
565
|
+
return session.execute_native(model,X,y,identity=identity,cancel=cancel)
|
|
566
|
+
return Executor()
|
|
567
|
+
|
|
568
|
+
def execute_native(self, model, X, y, *, identity, cancel):
|
|
569
|
+
start=time.monotonic();before=getattr(self,'_user_completed',0)
|
|
570
|
+
try:return self._execute_native(model,X,y,identity=identity,cancel=cancel)
|
|
571
|
+
finally:
|
|
572
|
+
if self.mode=='PUBLIC_USER' and self._user_completed>before:
|
|
573
|
+
elapsed=time.monotonic()-start
|
|
574
|
+
self._component_costs.append(elapsed)
|
|
575
|
+
self.event('USER_COMPONENT_OPERATION_COST',seconds=elapsed,
|
|
576
|
+
scope='INCLUSIVE_COMPONENT_OPERATION_ONLY_NOT_TOTAL_FIT_OR_BENCHMARK')
|
|
577
|
+
|
|
578
|
+
def _execute_native(self, model, X, y, *, identity, cancel):
|
|
579
|
+
self._learning_open()
|
|
580
|
+
if self.mode=='PUBLIC_USER':
|
|
581
|
+
self._user_integrity()
|
|
582
|
+
if self.phase=='NUMERIC':
|
|
583
|
+
if self.frozen is None or identity['family']!=self.frozen['family'] or model.base_genome.to_dict()!=self.frozen['recipe']['genome']:
|
|
584
|
+
raise ValueError('Numerical tuning cannot change the frozen program')
|
|
585
|
+
base=HyperParams(**self.frozen['params'])
|
|
586
|
+
if identity['family']=='bag':base=replace(base,n_estimators=1,learning_rate=1.,row_fraction=1.)
|
|
587
|
+
allowed={'l2','min_samples_leaf'}|({'learning_rate'} if identity['family']=='boost' else set())
|
|
588
|
+
if any(asdict(model.base_params)[k]!=v for k,v in asdict(base).items() if k not in allowed):
|
|
589
|
+
raise ValueError('Numerical tuning changed a fixed parameter')
|
|
590
|
+
self.check(); a=self.prepared; task=a['task']; family=identity['family']
|
|
591
|
+
if type(model) is not NATIVE[task] or model.backend!='pure' or model.model_ is not None:
|
|
592
|
+
raise ValueError('Fresh exact native estimator required')
|
|
593
|
+
p=asdict(model.base_params); c=asdict(model.config)
|
|
594
|
+
genome=model.base_genome.to_dict()
|
|
595
|
+
train={'role':'TRAIN','ids':identity['TRAIN_ids'],
|
|
596
|
+
'X':identity['actual_TRAIN_X'],'y':identity['actual_TRAIN_y']}
|
|
597
|
+
indices=identity['ordered_sample']
|
|
598
|
+
if X!=[train['X'][i] for i in indices] or y!=[train['y'][i] for i in indices] or p!=identity['params']:
|
|
599
|
+
raise ValueError('Actual sampled data/parameters differ')
|
|
600
|
+
prep={**identity['preprocessing'],'fit_role':'TRAIN','encoded_classes':list(range(len(a['classes'])))}
|
|
601
|
+
component_protocol={k:self.plan['protocol'][k] for k in ('path','sha256')}
|
|
602
|
+
context=dict(protocol=component_protocol,split_sha256=digest(self.source['partitions']),
|
|
603
|
+
preprocessing=prep,task=task,family=family,structure=genome,params=p,config=c,
|
|
604
|
+
objective={'binary':'binary_logloss','multiclass':'ovr','regression':'squared_error'}[task],
|
|
605
|
+
seed=c['seed'],backend='pure',dependencies={'runtime':self.engine.identity,'mode':self.mode},
|
|
606
|
+
sources=cache.native_sources())
|
|
607
|
+
request=cache.learning_request(train,indices,**context)
|
|
608
|
+
form={'family':family,'members':1,'stages':p['n_estimators'],'classes':len(a['classes']) or 1,'genome_sha256':digest(genome)}
|
|
609
|
+
from .admission import CURRENT
|
|
610
|
+
current=CURRENT.get()
|
|
611
|
+
if current is None or current[0] is not self.engine or current[1]['operation']!='inspect':
|
|
612
|
+
raise ValueError('Missing exact parent software admission')
|
|
613
|
+
if True:
|
|
614
|
+
self.logical+=1
|
|
615
|
+
if self.logical>self.plan['max_requests']: raise RuntimeError('Logical opportunity budget exhausted')
|
|
616
|
+
with self.store.claim(request,logical_id='component:'+str(self.logical)) as lease:
|
|
617
|
+
if lease.hit:
|
|
618
|
+
self._capture_oof_component(request,lease)
|
|
619
|
+
return validate_model(artifact_path(lease.result['artifact']['path']),request.value())
|
|
620
|
+
if self.mode in ('PREFIT','LIVE','PUBLIC_USER'):
|
|
621
|
+
self.component=(model,copy.deepcopy(X),copy.deepcopy(y),request)
|
|
622
|
+
try:
|
|
623
|
+
native=model.fit(X,y,cancel=lambda:(cancel() or False))
|
|
624
|
+
path=artifact_path(lease.folder/'model.json');native.save_model(path)
|
|
625
|
+
validated=validate_model(path,request.value())
|
|
626
|
+
if self.mode=='PUBLIC_USER':
|
|
627
|
+
self._user_integrity()
|
|
628
|
+
if sum(p.stat().st_size for p in self.root.rglob('*') if p.is_file())>self._user_limits['artifact_bytes']:
|
|
629
|
+
raise RuntimeError('Ordinary-user artifact budget exhausted')
|
|
630
|
+
lease.commit(path,expected_sha256=sha(path),proof=[component_protocol])
|
|
631
|
+
self._capture_oof_component(request,lease)
|
|
632
|
+
return validated
|
|
633
|
+
finally:self.component=None
|
|
634
|
+
self.fixture_calls+=1
|
|
635
|
+
payload=self.fixture(copy.deepcopy(request.value()),copy.deepcopy(X),copy.deepcopy(y))
|
|
636
|
+
path=artifact_path(lease.folder/'model.json')
|
|
637
|
+
cache._write(path,payload)
|
|
638
|
+
native=validate_model(path,request.value())
|
|
639
|
+
lease.commit(path,expected_sha256=sha(path),proof=[component_protocol])
|
|
640
|
+
self._capture_oof_component(request,lease)
|
|
641
|
+
self.event('FIXTURE_COMPLETED',key=request.key,physical_fits=0)
|
|
642
|
+
return native
|
|
643
|
+
|
|
644
|
+
def native_boundary(self, original, model, X, y, kwargs):
|
|
645
|
+
self._learning_open()
|
|
646
|
+
self.check()
|
|
647
|
+
if self.mode=='PUBLIC_USER':
|
|
648
|
+
self._user_integrity()
|
|
649
|
+
if self.component is None:raise PermissionError('No active user component lease')
|
|
650
|
+
want,xx,yy,request=self.component;v=request.value()
|
|
651
|
+
if model is not want or X!=xx or y!=yy or set(kwargs)!={'cancel'} or not callable(kwargs['cancel']):
|
|
652
|
+
raise ValueError('Actual user native invocation differs')
|
|
653
|
+
if asdict(model.base_params)!=v['params'] or asdict(model.config)!=v['config'] or model.base_genome.to_dict()!=v['structure']:
|
|
654
|
+
raise ValueError('Actual user native recipe changed')
|
|
655
|
+
scalar=v['params']['n_estimators']*(len(v['preprocessing']['encoded_classes']) if v['task']=='multiclass' else 1)
|
|
656
|
+
if self._user_started+1>self._user_limits['calls'] or self._user_trees+scalar>self._user_limits['scalar_trees']:
|
|
657
|
+
raise RuntimeError('Ordinary-user cumulative native budget exhausted')
|
|
658
|
+
r=self._user_learning_request('fit',v['task'],v['family'],model.base_params,
|
|
659
|
+
len(v['preprocessing']['encoded_classes']),v['structure'])
|
|
660
|
+
# Repeated equal parameters still need distinct physical component
|
|
661
|
+
# identities. Cache hits never reach this reservation boundary.
|
|
662
|
+
r['component_key']=request.key
|
|
663
|
+
r['id']=digest({k:x for k,x in r.items() if k!='id'})
|
|
664
|
+
if self.public_scope is not None:self.public_scope.reserve(self,operation='fit',trees=scalar,key=request.key)
|
|
665
|
+
with self.engine.user_learning(r):
|
|
666
|
+
self._user_started+=1;self._user_trees+=scalar;self.physical+=1
|
|
667
|
+
self.event('NATIVE_FIT_STARTED',key=request.key,scalar_trees=scalar,physical_fits=1)
|
|
668
|
+
old=getattr(self,'_component_deadline',None)
|
|
669
|
+
self._component_deadline=min(time.monotonic()+60,self.started+self.plan['seconds'])
|
|
670
|
+
try:
|
|
671
|
+
fitted=original(model,X,y,**kwargs)
|
|
672
|
+
self._user_integrity();self._user_completed+=1
|
|
673
|
+
self.event('NATIVE_FIT_COMPLETED',key=request.key,scalar_trees=scalar,physical_fits=1)
|
|
674
|
+
return fitted
|
|
675
|
+
except BaseException as e:
|
|
676
|
+
self.event('NATIVE_FIT_INCOMPLETE',key=request.key,error=type(e).__name__)
|
|
677
|
+
raise
|
|
678
|
+
finally:self._component_deadline=old
|
|
679
|
+
if self.mode!='PREFIT' or self.component is None:
|
|
680
|
+
raise AdmissionError({'status':'BLOCKED','reason':'NO_CURRENT_LIVE_NATIVE_AUTHORITY'})
|
|
681
|
+
want,xx,yy,request=self.component
|
|
682
|
+
v=request.value()
|
|
683
|
+
if model is not want or X!=xx or y!=yy or set(kwargs)!={'cancel'} or not callable(kwargs['cancel']):
|
|
684
|
+
raise ValueError('Actual pre-fit invocation differs')
|
|
685
|
+
if asdict(model.base_params)!=v['params'] or asdict(model.config)!=v['config']:
|
|
686
|
+
raise ValueError('Changed actual pre-fit parameters/config')
|
|
687
|
+
self.prefit+=1;self.event('NATIVE_PREFIT_BOUNDARY',key=request.key,physical_fits=0)
|
|
688
|
+
raise PreFitBoundary('STOP_BEFORE_ORIGINAL_NATIVE_FIT')
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def native_fit_entry(original, cls):
|
|
692
|
+
"""M031 cooperative pre-fit wrapper, with exact current v2 session type."""
|
|
693
|
+
def guarded(model,X,y,**kwargs):
|
|
694
|
+
session=require_session()
|
|
695
|
+
if type(model) is not cls: raise ValueError('Changed native class')
|
|
696
|
+
return session.native_boundary(original,model,X,y,kwargs)
|
|
697
|
+
return guarded
|