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 +16 -0
- fxbex/__main__.py +2 -0
- fxbex/buildbundle.py +54 -0
- fxbex/bundled.py +45 -0
- fxbex/cli.py +245 -0
- fxbex/execution.py +113 -0
- fxbex/graph_fxb.py +350 -0
- fxbex/hub.py +115 -0
- fxbex/hug.py +268 -0
- fxbex/hug_engine.py +58 -0
- fxbex/llm.py +26 -0
- fxbex/memory.py +286 -0
- fxbex/model.py +48 -0
- fxbex/model_export.py +94 -0
- fxbex/native_fxb.py +177 -0
- fxbex/optimization.py +59 -0
- fxbex/package.py +288 -0
- fxbex/provenance.py +18 -0
- fxbex/replay.py +221 -0
- fxbex/runtime.py +289 -0
- fxbex/schema.py +36 -0
- fxbex/server.py +44 -0
- fxbex/service.py +113 -0
- fxbex/settings.py +58 -0
- fxbex/torch_api.py +59 -0
- fxbex/ui.py +105 -0
- fxbex/validation.py +135 -0
- fxbex/worker.py +59 -0
- fxbx/__init__.py +3 -0
- fxbx/__main__.py +2 -0
- fxbx-0.7.0.dist-info/METADATA +565 -0
- fxbx-0.7.0.dist-info/RECORD +36 -0
- fxbx-0.7.0.dist-info/WHEEL +5 -0
- fxbx-0.7.0.dist-info/entry_points.txt +2 -0
- fxbx-0.7.0.dist-info/licenses/LICENSE +201 -0
- fxbx-0.7.0.dist-info/top_level.txt +2 -0
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
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__
|