fxbx 0.7.0__py3-none-any.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.
fxbex/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """Independent FXBEX package builder and installed-adapter runtime."""
2
+ __version__ = "0.7.0"
3
+
4
+ from .llm import LLM
5
+ from .model import Model, YOLO, ObjectDetection, ImageGeneration
6
+ __all__ = ['Model', 'LLM', 'YOLO', 'ObjectDetection', 'ImageGeneration','hug','describe','example_inputs','as_torch']
7
+
8
+ from .schema import describe, example_inputs
9
+
10
+ from .hug import hug
11
+
12
+
13
+ def as_torch(package, *, execution_policy='npu_only', inference_only=True, output_device='cpu'):
14
+ """Opt-in immutable nn.Module wrapper; importing FXBEX alone does not import torch."""
15
+ from .torch_api import CompiledModule
16
+ return CompiledModule(package,execution_policy=execution_policy,inference_only=inference_only,output_device=output_device)
fxbex/__main__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .cli import main
2
+ raise SystemExit(main())
fxbex/buildbundle.py ADDED
@@ -0,0 +1,54 @@
1
+ """TPLMachine delivery receipt bound to all execution-relevant source contents."""
2
+ import hashlib
3
+ import json
4
+ from pathlib import Path
5
+ from .package import PackageError
6
+
7
+ NAME='build_bundle.json'
8
+ def file_sha(path):
9
+ h=hashlib.sha256()
10
+ with path.open('rb') as stream:
11
+ for block in iter(lambda:stream.read(1024*1024),b''):h.update(block)
12
+ return h.hexdigest()
13
+ def sha(data):return hashlib.sha256(data).hexdigest()
14
+ def semantic_manifest(manifest):
15
+ return {k:manifest.get(k) for k in ('model','runtime','compatibility','io_contract','memory','state_contract')}
16
+ def validate(root,manifest):
17
+ root=Path(root);path=root/NAME
18
+ if not path.exists():return None
19
+ if path.stat().st_size>16*1024**2:raise PackageError('Oversized BuildBundle')
20
+ bundle=json.loads(path.read_text());files=bundle.get('content',{})
21
+ if bundle.get('schema')!='tplmachine.build-bundle.v1' or not files:
22
+ raise PackageError('Invalid BuildBundle')
23
+ canonical=lambda x:json.dumps(x,sort_keys=True,separators=(',',':'),allow_nan=False).encode()
24
+ if sha(canonical(semantic_manifest(manifest)))!=bundle.get('manifest_semantics_sha256'):
25
+ raise PackageError('BuildBundle manifest semantics changed')
26
+ actual={str(p.relative_to(root)):file_sha(p) for p in root.rglob('*') if p.is_file() and str(p.relative_to(root)) not in (NAME,'manifest.json')}
27
+ if actual!=files:raise PackageError('BuildBundle content changed or missing files')
28
+ acceptance=bundle.get('acceptance',{})
29
+ expected=sha(canonical({'files':files,'manifest':bundle['manifest_semantics_sha256']}))
30
+ if bundle.get('content_id')!=expected or acceptance.get('content_id')!=expected:
31
+ raise PackageError('BuildBundle acceptance belongs to different contents')
32
+ if (bundle.get('status')!='validated' or acceptance.get('fresh_process_reload') is not True
33
+ or acceptance.get('numerical_passed') is not True or acceptance.get('completion_confirmed') is not True
34
+ or acceptance.get('state_validation') not in ('passed','not_applicable')
35
+ or type(acceptance.get('npu_tasks')) is not int or acceptance['npu_tasks']<1
36
+ or any(type(acceptance.get(k)) is not int or acceptance[k]!=0 for k in ('cpu_routes','unknown_graphs','compiler_calls'))):
37
+ raise PackageError('BuildBundle has not passed independent execution acceptance')
38
+ stateful=manifest.get('io_contract',{}).get('method') in ('chat','generate') or manifest.get('state_contract') not in (None,{}, {'kind':'stateless'})
39
+ if stateful and acceptance['state_validation']!='passed':
40
+ raise PackageError('BuildBundle stateful model requires state validation')
41
+ return {'content_id':expected,'status':'validated','current_request_npu_execution':'unknown'}
42
+
43
+
44
+ def prepare_source(root):
45
+ """Freeze the same public metadata that package.build will generate before binding."""
46
+ root=Path(root);index=json.loads((root/'weights_index.json').read_text())
47
+ if any('external_path' in w for w in index['weights']):
48
+ raise PackageError('BuildBundle requires already internalized weights')
49
+ (root/'weights_index.json').write_bytes(json.dumps(index,sort_keys=True).encode())
50
+ manifest=json.loads((root/'manifest.json').read_text())
51
+ if manifest['runtime']['adapter']=='runnable_dag':
52
+ from .memory import analyze
53
+ memory=analyze(root)
54
+ (root/'memory_plan.json').write_bytes(json.dumps(memory,sort_keys=True).encode())
fxbex/bundled.py ADDED
@@ -0,0 +1,45 @@
1
+ """Explicit trusted Python runtime bundles; not a sandbox for untrusted packages."""
2
+ from contextlib import contextmanager
3
+ import importlib
4
+ import re
5
+ import sys
6
+ from .package import PackageError
7
+
8
+
9
+ def specification(manifest):
10
+ spec=manifest['runtime'].get('bundled_python')
11
+ if spec is None:return None
12
+ if not isinstance(spec,dict) or spec.get('root')!='python':raise PackageError('Expected bundled_python root=python')
13
+ entry=spec.get('entrypoint','')
14
+ if not isinstance(entry,str) or not re.fullmatch(r'[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*',entry):
15
+ raise PackageError('Invalid bundled adapter entrypoint')
16
+ packages=spec.get('packages')
17
+ if not isinstance(packages,list) or not packages or any(not isinstance(p,str) or not re.fullmatch(r'[A-Za-z_]\w*',p) for p in packages):
18
+ raise PackageError('Invalid bundled top-level packages')
19
+ if len(set(packages))!=len(packages) or entry.split('.')[0].split(':')[0] not in packages or 'fxbex' in packages:
20
+ raise PackageError('Invalid bundled package ownership')
21
+ for p in packages:
22
+ if f'python/{p}/__init__.py' not in manifest['files']:raise PackageError(f'Missing bundled package {p}')
23
+ module=entry.split(':')[0].replace('.','/')
24
+ if f'python/{module}.py' not in manifest['files']:raise PackageError('Missing bundled entrypoint source')
25
+ return spec
26
+
27
+
28
+ @contextmanager
29
+ def activate(root,manifest):
30
+ spec=specification(manifest)
31
+ if spec is None:
32
+ yield None;return
33
+ names=spec['packages']
34
+ def owned(n):return any(n==p or n.startswith(p+'.') for p in names)
35
+ if any(owned(n) for n in sys.modules):
36
+ raise PackageError('Bundled runtime conflicts with imported modules; use a fresh FXBEX worker')
37
+ directory=str(root/'python');sys.path.insert(0,directory)
38
+ try:
39
+ module,attribute=spec['entrypoint'].split(':')
40
+ yield getattr(importlib.import_module(module),attribute)
41
+ finally:
42
+ if directory in sys.path:sys.path.remove(directory)
43
+ for n in list(sys.modules):
44
+ if owned(n):del sys.modules[n]
45
+ importlib.invalidate_caches()
fxbex/cli.py ADDED
@@ -0,0 +1,245 @@
1
+ """CLI for local FXB_EX packages; SDK is imported only for runtime operations."""
2
+ import argparse
3
+ import json
4
+ from pathlib import Path
5
+ import sys
6
+ import threading
7
+ import time
8
+ from .package import build, inspect
9
+ from .validation import validate
10
+
11
+
12
+ def _runtime_call(callback, log_path, verbose=False):
13
+ """Keep framework diagnostics separate from answers and JSON results."""
14
+ if verbose:return callback()
15
+ import contextlib
16
+ with log_path.open('a') as log, contextlib.redirect_stdout(log), contextlib.redirect_stderr(log):
17
+ return callback()
18
+
19
+
20
+ def _load_with_progress(callback, stream, interval=5):
21
+ """Report elapsed time without inventing a completion percentage."""
22
+ stopped = threading.Event()
23
+ started = time.monotonic()
24
+ interactive = stream.isatty()
25
+ displayed = False
26
+ def report():
27
+ nonlocal displayed
28
+ while not stopped.wait(interval):
29
+ message = f'Model loading... {int(time.monotonic() - started)}s elapsed'
30
+ print(('\r' if interactive else '') + message, end='' if interactive else '\n', file=stream, flush=True)
31
+ displayed = True
32
+ thread = threading.Thread(target=report, daemon=True)
33
+ thread.start()
34
+ try:
35
+ return callback()
36
+ finally:
37
+ stopped.set()
38
+ thread.join()
39
+ if interactive and displayed:
40
+ print(file=stream, flush=True)
41
+
42
+
43
+ def _main(argv=None):
44
+ parser = argparse.ArgumentParser(
45
+ prog='fxbx', description='Build, inspect and run compiled FXB_EX model packages.',
46
+ formatter_class=argparse.RawDescriptionHelpFormatter,
47
+ epilog="""Examples:
48
+ fxbx build ./compiled_model -o model.fxbex
49
+ fxbx inspect model.fxbex
50
+ fxbx validate model.fxbex --integrity-only
51
+ fxbx validate model.fxbex --weights-dir ./weights
52
+ fxbx run model.fxbex --input inputs.json -o result.json
53
+ fxbx chat model.fxbex
54
+ fxbx serve model.fxbex --port 8000
55
+ fxbx doctor
56
+
57
+ Validation checks static readiness; it does not run inference or prove NPU execution.
58
+ Model-specific exporters and capture commands belong to their adapter plugins.""")
59
+ sub = parser.add_subparsers(dest='action', required=True)
60
+ from . import __version__
61
+ parser.add_argument('--version',action='version',version='%(prog)s '+__version__)
62
+ p = sub.add_parser('build', help='Pack a compiled directory with checksums (no compilation)')
63
+ p.add_argument('source', help='Compiled directory containing manifest and required JSON configurations')
64
+ p.add_argument('-o', '--output', required=True, help='New .fxbex file; existing files are never overwritten')
65
+ p.add_argument('--source-model',help='Original HF model ID or HTTPS model URL')
66
+ p.add_argument('--source-revision',help='Original checkpoint revision (prefer immutable commit)')
67
+ p.add_argument('--standalone', action='store_true', help='Copy referenced external weights into the archive')
68
+ p = sub.add_parser('inspect', help='Verify integrity and display the manifest; no SDK needed')
69
+ p.add_argument('package', help='Path to a .fxbex archive')
70
+ p.add_argument('--memory', action='store_true', help='Show logical memory/lifetime plan without loading the NPU')
71
+ p = sub.add_parser('validate', help='Check integrity, SDK, adapter, weights and device availability without inference')
72
+ p.add_argument('package', help='Path to a .fxbex archive')
73
+ p.add_argument('--integrity-only', action='store_true', help='Verify archive only; skip SDK, weights and device checks')
74
+ p.add_argument('--weights-dir', help='Override the external checkpoint/weights directory')
75
+ for name in ('describe','sample'):
76
+ p=sub.add_parser(name,help='Show I/O contract or example input without running the model')
77
+ p.add_argument('package')
78
+ sub.add_parser('doctor', help='Report installed SDK versions and RNGD device status')
79
+ p = sub.add_parser('ui', help='Launch the local package interface')
80
+ p.add_argument('--port', type=int, default=7861, help='HTTP port (default: 7861)')
81
+ for name, help_text in (
82
+ ('run', 'Run JSON inputs or start interactive chat for a chat adapter'),
83
+ ('chat', 'Chat interactively, or process a messages JSON file'),
84
+ ('predict', 'Run a tensor DAG with named JSON inputs'),
85
+ ('serve', 'Serve the loaded package over local HTTP'),
86
+ ):
87
+ p = sub.add_parser(name, help=help_text, description=help_text)
88
+ p.add_argument('package', help='Path to a .fxbex archive')
89
+ p.add_argument('--verbose',action='store_true',help='Show internal SDK/compiler diagnostics on the terminal')
90
+ p.add_argument('--report-dir', help='Directory for runtime logs and evidence')
91
+ p.add_argument('--weights-dir', help='Override external checkpoint/weights directory')
92
+ if name != 'serve':
93
+ p.add_argument('--input', help='JSON file: named tensors for predict, or {"messages": [...]} for chat')
94
+ p.add_argument('-o', '--output', help='Write response JSON to this file')
95
+ p.add_argument('--max-new-tokens', type=int, default=32, help='Chat token limit (default: 32)')
96
+ if name == 'run':
97
+ p.add_argument('--sample', action='store_true', help='Run the packaged example once, including for chat models')
98
+ p.add_argument('--image', help='Use an image file instead of the packaged image sample')
99
+ else:
100
+ p.add_argument('--port', type=int, default=8000, help='HTTP port (default: 8000)')
101
+ p=sub.add_parser('fetch', help='Download a catalog release into the shared FXBEX cache')
102
+ p.add_argument('package')
103
+ p.add_argument('--refresh', action='store_true', help='Recheck catalog and force a fresh HF download')
104
+ p=sub.add_parser('cache', help='List or clear FXBEX-managed model downloads only')
105
+ p.add_argument('operation', choices=['list','clear'])
106
+ p.add_argument('--yes', action='store_true', help='Confirm deletion of managed model downloads')
107
+ args=parser.parse_args(argv)
108
+ if args.action=='cache':
109
+ from .settings import cache_root
110
+ root=cache_root()/'models'
111
+ files=sorted(root.rglob('*.fxbex')) if root.exists() else []
112
+ if args.operation=='clear':
113
+ if not args.yes:raise ValueError('Use cache clear --yes to delete FXBEX-managed downloads')
114
+ import shutil
115
+ if root.is_symlink():raise ValueError('Refusing to clear a symlinked cache root')
116
+ if root.exists():shutil.rmtree(root)
117
+ print(json.dumps({'cache':str(root),'files':[str(p) for p in files],'cleared':args.operation=='clear'}))
118
+ return 0
119
+ if args.action=='fetch':
120
+ from .hub import resolve_package
121
+ print(json.dumps({'package':str(resolve_package(args.package,refresh=args.refresh))}))
122
+ return 0
123
+ if args.action in ('run','chat','predict','serve','doctor') or (args.action=='validate' and not args.integrity_only):
124
+ from .settings import runtime_python
125
+ interpreter=runtime_python()
126
+ if Path(interpreter)!=Path(sys.executable):
127
+ import subprocess
128
+ print('Using SDK Python: '+interpreter,file=sys.stderr,flush=True)
129
+ code=subprocess.call([interpreter,'-m','fxbx',*(sys.argv[1:] if argv is None else map(str,argv))])
130
+ return code if code>=0 else 128-code
131
+ if args.action=='chat' and not args.input:
132
+ print('Welcome to FXBEX Chat!\nModel: '+args.package+
133
+ '\n모델을 준비하고 있습니다. 적재가 완료되면 >>> 뒤에 질문을 입력하세요.'+
134
+ '\n/reset: 대화 초기화 · /quit: 종료 및 모델 해제\n',file=sys.stderr,flush=True)
135
+ if args.action in ('run','chat','predict','serve','describe','sample','inspect','validate'):
136
+ from .hub import resolve_package
137
+ args.package=str(resolve_package(args.package))
138
+ sample_contract=None
139
+ if args.action=='run' and args.image and args.input:
140
+ raise ValueError('Use --image or --input, not both')
141
+ if args.action=='run' and not args.input:
142
+ from .schema import describe
143
+ sample_contract=describe(args.package)
144
+ if args.image and (sample_contract['method']!='predict' or 'image' not in sample_contract['inputs']):
145
+ raise ValueError('This package does not declare an image input; inspect its input contract')
146
+ if args.image and not Path(args.image).is_file():
147
+ raise ValueError('Image file does not exist')
148
+ if sample_contract['method']=='chat' and not args.sample:
149
+ sample_contract=None
150
+ if args.action=='build':result={'package':str(build(args.source,args.output,standalone=args.standalone,source_url=args.source_model,source_revision=args.source_revision))}
151
+ elif args.action == 'inspect':
152
+ m=inspect(args.package)
153
+ result={'model':m['model'],'runtime':m['runtime'],'compatibility':m['compatibility'],
154
+ 'standalone':m['standalone'],'bundled_python':bool(m['runtime'].get('bundled_python')),
155
+ 'delivery':m.get('delivery'), 'provenance':m.get('provenance',{'status':'unknown'}), 'files':len(m['files']),
156
+ 'artifact_bytes':sum(x['size'] for x in m['files'].values()),'integrity_verified':True}
157
+ if args.memory:
158
+ from .memory import inspect_archive, summarize
159
+ result['memory_plan'] = summarize(inspect_archive(args.package))
160
+ elif args.action == 'validate':
161
+ result = validate(args.package, integrity_only=args.integrity_only, weights_dir=args.weights_dir)
162
+ elif args.action in ('describe','sample'):
163
+ from .schema import describe,example_inputs
164
+ result=describe(args.package) if args.action=='describe' else example_inputs(args.package)
165
+ elif args.action=='doctor':
166
+ from .runtime import doctor
167
+ result=doctor()
168
+ elif args.action=='ui':
169
+ from .ui import launch
170
+ launch(args.port);return
171
+ else:
172
+ from .runtime import Session
173
+ # SDK/compiler diagnostics may print; keep JSON results on stdout.
174
+ import contextlib
175
+ if args.action == 'predict' and not args.input:
176
+ raise ValueError('--input JSON file is required for tensor prediction')
177
+ from .runtime import package_compatibility
178
+ session=Session(args.package,report_dir=args.report_dir,weights_dir=args.weights_dir)
179
+ log_path=session.report_dir/'runtime.log'
180
+ print(f'Loading package; diagnostics: {log_path}',file=sys.stderr,flush=True)
181
+ def invoke(callback):return _runtime_call(callback,log_path,args.verbose)
182
+ progress_stream = sys.stderr
183
+ _load_with_progress(lambda: invoke(session.__enter__), progress_stream)
184
+ try:
185
+ for message in package_compatibility(session.manifest):
186
+ print('Warning: '+message,file=sys.stderr,flush=True)
187
+ print(f'NPU package ready; reports: {session.report_dir}',file=sys.stderr,flush=True)
188
+ from .execution import memory_text
189
+ print('Memory: '+memory_text(getattr(session,'memory',None)),file=sys.stderr,flush=True)
190
+ if args.action=='serve':
191
+ from .server import serve
192
+ serve(session,args.port,invoke=invoke);return
193
+ if sample_contract is not None:
194
+ from .schema import resolve_sample
195
+ data=resolve_sample(sample_contract,session.root,session.manifest)
196
+ if args.image:
197
+ data['image']=str(Path(args.image).resolve())
198
+ if sample_contract['method']=='chat':
199
+ result=invoke(lambda: session.chat(data['messages'],data.get('max_new_tokens',args.max_new_tokens)))
200
+ else:result=invoke(lambda: session.predict(data))
201
+ elif args.action=='predict' or (args.action=='run' and session.manifest['runtime']['adapter']=='runnable_dag'):
202
+ if not args.input:raise ValueError('--input JSON file is required for tensor prediction')
203
+ result=invoke(lambda: session.predict(json.loads(Path(args.input).read_text())))
204
+ elif args.input:
205
+ data=json.loads(Path(args.input).read_text())
206
+ result=invoke(lambda: session.chat(data['messages'],data.get('max_new_tokens',args.max_new_tokens)))
207
+ else:
208
+ messages=[]
209
+ print('모델 적재 완료. 대화를 시작하세요. (/reset: 초기화 · /quit: 종료)',flush=True)
210
+ while True:
211
+ try:prompt=input('>>> ')
212
+ except EOFError:break
213
+ if prompt.strip()=='/quit':break
214
+ if prompt.strip()=='/reset':messages=[];continue
215
+ if not prompt.strip():continue
216
+ candidate=messages+[{'role':'user','content':prompt}]
217
+ result=invoke(lambda: session.chat(candidate,args.max_new_tokens))
218
+ messages=candidate+[{'role':'assistant','content':result['text']}]
219
+ print(result['text'],flush=True)
220
+ return
221
+ if isinstance(result,dict):result.setdefault('memory',getattr(session,'memory',None))
222
+ finally:invoke(lambda: session.__exit__(None,None,None))
223
+ result['report_dir']=str(session.report_dir)
224
+ result_path=Path(args.output).resolve() if args.output else session.report_dir/'result.json'
225
+ result['result_file']=str(result_path)
226
+ result_path.write_text(json.dumps(result,ensure_ascii=False,indent=2))
227
+ if not args.verbose:
228
+ # Full raw diagnostics remain in result.json and runtime.log.
229
+ result={key:value for key,value in result.items() if key not in
230
+ ('memory','compiler_calls','replay_hits','npu_tasks','validated_audit','current_request_traced')}
231
+ print(json.dumps(result,ensure_ascii=False,indent=2))
232
+ return 0 if result.get('valid', True) else 1
233
+
234
+ def main(argv=None):
235
+ try:
236
+ return _main(argv) or 0
237
+ except Exception as exc:
238
+ print(f'fxbx: {exc}', file=sys.stderr)
239
+ return 1
240
+ except KeyboardInterrupt:
241
+ return 130
242
+
243
+
244
+ if __name__ == '__main__':
245
+ sys.exit(main())
fxbex/execution.py ADDED
@@ -0,0 +1,113 @@
1
+ """Small execution receipts, not a replacement for official NPU trace verification."""
2
+ import hashlib
3
+ import json
4
+ import time
5
+ from pathlib import Path
6
+ import subprocess
7
+
8
+ def memory_text(snapshot):
9
+ snapshot=snapshot or {}
10
+ logical=snapshot.get('model_logical_memory',{})
11
+ values=[]
12
+ for key,label in [('weight_logical_bytes','weights'),('kv_logical_bytes','KV'),('recurrent_logical_bytes','recurrent')]:
13
+ if key in logical:values.append(f"{label}={logical[key]/1024**3:.3f} GiB")
14
+ execution=logical.get('execution') or {}
15
+ if 'peak_live_tensor_logical_bytes' in execution:
16
+ values.append(f"live tensor peak={execution['peak_live_tensor_logical_bytes']/1024**2:.2f} MiB")
17
+ values.append(f"released bindings={execution.get('released_tensor_bindings',0)}")
18
+ return 'physical HBM unmeasured; SMI zero is inconclusive. Logical NPU tensors: '+(', '.join(values) or 'not reported')
19
+
20
+
21
+
22
+ class MemoryTelemetry:
23
+ """Best-effort SMI memory snapshots; never an allocator substitute."""
24
+ def __init__(self, report_dir, interval_s=5):
25
+ self.path = Path(report_dir) / 'memory.jsonl'
26
+ self.last = None
27
+ self.interval_s = interval_s
28
+ self.last_sample_monotonic = None
29
+
30
+ def sample(self, phase, *, force=False):
31
+ now = time.monotonic()
32
+ boundary = phase in ('before_load', 'after_load', 'after_unload')
33
+ if (self.last is not None and not boundary and not force
34
+ and now-self.last_sample_monotonic < self.interval_s):
35
+ return dict(self.last, requested_phase=phase, cached_snapshot=True,
36
+ sample_age_s=now-self.last_sample_monotonic)
37
+ row = {'schema': 1, 'phase': phase, 'time_unix': time.time(),
38
+ 'source': 'furiosa-smi status --format json',
39
+ 'physical_allocated_bytes':None,
40
+ 'note':'SMI-reported counters are not allocator proof; zero does not establish zero HBM use.'}
41
+ try:
42
+ result = subprocess.run(['furiosa-smi', 'status', '--format', 'json'],
43
+ capture_output=True, text=True, timeout=5, check=True)
44
+ row['devices'] = json.loads(result.stdout)
45
+ except (OSError, subprocess.SubprocessError, json.JSONDecodeError) as exc:
46
+ row['error_type'] = type(exc).__name__
47
+ self.last = row
48
+ self.last_sample_monotonic = now
49
+ try:
50
+ with self.path.open('a') as stream:
51
+ stream.write(json.dumps(row, allow_nan=False) + '\n')
52
+ except (OSError,ValueError,TypeError):
53
+ # A diagnostic must never hide model execution or cleanup failures.
54
+ pass
55
+ return row
56
+
57
+
58
+ def manifest_identity(manifest):
59
+ # The validated manifest already contains hashes of packaged artifacts.
60
+ # Hashing it avoids rereading multi-GB weights for every inference.
61
+ data = json.dumps(manifest, sort_keys=True, separators=(',', ':'), allow_nan=False)
62
+ return hashlib.sha256(data.encode()).hexdigest()
63
+
64
+
65
+ class ExecutionRecorder:
66
+ def __init__(self, report_dir, manifest):
67
+ self.path = Path(report_dir) / 'executions.jsonl'
68
+ self.identity = manifest_identity(manifest)
69
+ self.sequence = 0
70
+ self.last = None
71
+ self.backend=manifest.get('optimization',{}).get('backend',
72
+ 'torch_runnable' if manifest.get('runtime',{}).get('adapter') in ('qwen35_replay','runnable_dag') else 'unknown')
73
+
74
+ def call(self, operation, callback):
75
+ self.sequence += 1
76
+ started = time.perf_counter()
77
+ row = {'schema': 1, 'sequence': self.sequence, 'operation': operation,
78
+ 'manifest_sha256': self.identity, 'status': 'failed',
79
+ 'timing_scope': 'host call including dispatch/transfers/waits; completion is adapter-defined',
80
+ 'npu_verdict': 'unknown', 'support_status': 'not_established',
81
+ 'optimization_status': 'not_measured', 'adapter_reported': {}}
82
+ row.update(backend=self.backend,measurement_scope='adapter_defined_return',
83
+ unit='seconds',clock_domain='host_monotonic',completion_confirmed=None,
84
+ profiling_enabled=None)
85
+ try:
86
+ result = callback()
87
+ row['status'] = 'succeeded'
88
+ if isinstance(result, dict):
89
+ # Never save prompts, messages, tensor outputs or arbitrary adapter data.
90
+ for key in ('compiler_calls', 'npu_tasks', 'current_request_traced'):
91
+ value = result.get(key)
92
+ if type(value) in (int, bool) or value is None:
93
+ row['adapter_reported'][key] = value
94
+ for key in ('validated_audit',):
95
+ value = result.get(key)
96
+ if isinstance(value, str) and len(value) <= 4096:
97
+ row['adapter_reported'][key] = value
98
+ return result
99
+ except BaseException as exc:
100
+ row['error_type'] = type(exc).__name__
101
+ raise
102
+ finally:
103
+ row['wall_seconds'] = time.perf_counter() - started
104
+ row['report_path'] = str(self.path)
105
+ self.last = row
106
+ row['persisted'] = True
107
+ try:
108
+ with self.path.open('a') as stream:
109
+ stream.write(json.dumps(row, allow_nan=False) + '\n')
110
+ except OSError as exc:
111
+ # Telemetry failure must not hide the original model exception.
112
+ row['persisted'] = False
113
+ row['recording_error_type'] = type(exc).__name__