mlbricks-studio 1.0.0b1__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.
mlb_studio/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .builder import Builder, BuilderWidget
2
+ from .graph import new_project
3
+ from .version import __version__
4
+
5
+ __all__ = ["Builder", "BuilderWidget", "new_project", "__version__"]
@@ -0,0 +1,347 @@
1
+ from __future__ import annotations
2
+
3
+ """Declarative runtime contracts for MLBricks components.
4
+
5
+ Studio is an orchestrator: contracts describe how a visual node maps to the
6
+ original MLBricks constructor and forward API. The graph executor itself does
7
+ not need per-component branches.
8
+ """
9
+
10
+ from copy import deepcopy
11
+ from dataclasses import dataclass
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any, Mapping
15
+
16
+ from .import_pool import IMPORT_POOL
17
+
18
+ _SCHEMA_PATH = Path(__file__).with_name("mlbricks_api_schema.json")
19
+ _SCHEMA_CACHE: dict[str, Any] | None = None
20
+
21
+
22
+ def _schema() -> dict[str, Any]:
23
+ global _SCHEMA_CACHE
24
+ if _SCHEMA_CACHE is None:
25
+ try:
26
+ _SCHEMA_CACHE = json.loads(_SCHEMA_PATH.read_text(encoding="utf-8"))
27
+ except Exception:
28
+ _SCHEMA_CACHE = {}
29
+ return _SCHEMA_CACHE or {}
30
+
31
+
32
+ def _bool(value: Any) -> bool:
33
+ if isinstance(value, bool):
34
+ return value
35
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
36
+
37
+
38
+ def _coerce(value: Any, annotation: str | None) -> Any:
39
+ kind = str(annotation or "").lower()
40
+ if value is None:
41
+ return None
42
+ if "bool" in kind:
43
+ return _bool(value)
44
+ if "int" in kind:
45
+ return int(float(value))
46
+ if "float" in kind:
47
+ return float(value)
48
+ return value
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class APIComponentContract:
53
+ """Mapping between one Studio node and an original MLBricks API.
54
+
55
+ ``input_ports`` maps Studio input-port names to forward argument names.
56
+ ``output_ports`` maps Studio output-port names to result positions/keys.
57
+ For the common one-input/one-output case no custom code is required.
58
+ """
59
+
60
+ component_type: str
61
+ import_key: str
62
+ input_ports: Mapping[str, str]
63
+ output_ports: Mapping[str, Any]
64
+ parameter_aliases: Mapping[str, tuple[str, ...]] = None
65
+ runtime_sources: Mapping[str, str] = None
66
+ input_initializers: Mapping[str, Mapping[str, Any]] = None
67
+
68
+ def _parameter_value(self, key: str, spec: dict[str, Any], params: dict[str, Any], runtime: dict[str, Any]):
69
+ aliases = (self.parameter_aliases or {}).get(key, ())
70
+ candidates = (key, *aliases)
71
+ for candidate in candidates:
72
+ if candidate in params and params[candidate] not in (None, ""):
73
+ return params[candidate], True
74
+ runtime_key = (self.runtime_sources or {}).get(key)
75
+ if runtime_key and runtime.get(runtime_key) not in (None, ""):
76
+ return runtime[runtime_key], True
77
+ if spec.get("value") is not None:
78
+ return spec.get("value"), True
79
+ if spec.get("default") is not None:
80
+ return spec.get("default"), True
81
+ return None, False
82
+
83
+ def constructor_kwargs(self, node: dict[str, Any], runtime: dict[str, Any]) -> dict[str, Any]:
84
+ params = deepcopy(node.get("params") or {})
85
+ component_schema = (_schema().get("components") or _schema()).get(self.component_type) or {}
86
+ kwargs: dict[str, Any] = {}
87
+ for spec in component_schema.get("parameters") or []:
88
+ key = str(spec.get("key") or spec.get("name") or "").strip()
89
+ if not key:
90
+ continue
91
+ value, present = self._parameter_value(key, spec, params, runtime)
92
+ if not present:
93
+ if spec.get("required"):
94
+ raise ValueError(f"{node.get('name', self.component_type)} requires {key}.")
95
+ continue
96
+ # UI/runtime values override constructor defaults, but 'auto' backend
97
+ # is intentionally preserved because it is part of the original API.
98
+ kwargs[key] = _coerce(value, spec.get("annotation") or spec.get("type"))
99
+ return kwargs
100
+
101
+ def instantiate(self, node: dict[str, Any], runtime: dict[str, Any]):
102
+ cls = IMPORT_POOL.resolve_component(self.import_key)
103
+ return cls(**self.constructor_kwargs(node, runtime))
104
+
105
+
106
+ def initialize_input(self, port: str, inputs: Mapping[str, Any], node: Mapping[str, Any], module):
107
+ """Create a declarative default for an unconnected logical input.
108
+
109
+ This keeps first-depth initialization in the component contract instead
110
+ of adding component-specific branches to the graph executor. Supported
111
+ initializers are intentionally small and tensor-shape driven.
112
+ """
113
+ spec = dict((self.input_initializers or {}).get(port) or {})
114
+ if not spec:
115
+ return None, False
116
+
117
+ source_name = str(spec.get("source") or "")
118
+ source = inputs.get(source_name)
119
+ if source is None:
120
+ return None, False
121
+
122
+ kind = str(spec.get("kind") or "").lower()
123
+ if kind == "zeros_like":
124
+ return source.new_zeros(source.shape), True
125
+
126
+ if kind == "module_method":
127
+ method_name = str(spec.get("method") or "")
128
+ method = getattr(module, method_name, None)
129
+ if not callable(method):
130
+ return None, False
131
+ return method(source), True
132
+
133
+ if kind == "zeros_feature":
134
+ width = None
135
+ width_attr = str(spec.get("width_attr") or "")
136
+ if width_attr and hasattr(module, width_attr):
137
+ width = getattr(module, width_attr)
138
+ if width in (None, ""):
139
+ width_param = str(spec.get("width_param") or "")
140
+ if width_param:
141
+ width = (node.get("params") or {}).get(width_param)
142
+ if width in (None, ""):
143
+ return None, False
144
+ shape = (*source.shape[:-1], int(width))
145
+ return source.new_zeros(shape), True
146
+
147
+ raise ValueError(f"Unsupported input initializer kind {kind!r} for {self.component_type}.{port}.")
148
+
149
+ def execute(self, module, inputs: Mapping[str, Any]):
150
+ kwargs = {
151
+ api_arg: inputs[port]
152
+ for port, api_arg in self.input_ports.items()
153
+ if port in inputs
154
+ }
155
+ result = module(**kwargs)
156
+ if len(self.output_ports) == 1 and "main" in self.output_ports:
157
+ selector = self.output_ports["main"]
158
+ if selector is None:
159
+ return {"main": result}
160
+ outputs: dict[str, Any] = {}
161
+ for port, selector in self.output_ports.items():
162
+ if selector is None:
163
+ outputs[port] = result
164
+ elif isinstance(selector, int):
165
+ outputs[port] = result[selector]
166
+ else:
167
+ outputs[port] = result[selector] if isinstance(result, dict) else getattr(result, str(selector))
168
+ return outputs
169
+
170
+
171
+ class VisionClassificationContract(APIComponentContract):
172
+ # TensorGraph contract for image-classification vision engines.
173
+ _CLASSIFIER_ENGINES = {"serpentine", "vit", "visiontransformer", "cnn"}
174
+
175
+ def constructor_kwargs(self, node, runtime):
176
+ kwargs = super().constructor_kwargs(node, runtime)
177
+ engine = str(kwargs.get("engine") or "Serpentine").strip()
178
+ key = engine.lower().replace("-", "").replace("_", "").replace(" ", "")
179
+ if key not in self._CLASSIFIER_ENGINES:
180
+ raise ValueError(
181
+ f"{node.get('name', self.component_type)} engine={engine!r} is "
182
+ "not an image-classification TensorGraph mode. Studio currently "
183
+ "supports Serpentine, ViT/VisionTransformer and CNN here; "
184
+ "Diffusion requires timesteps and AR requires visual token IDs."
185
+ )
186
+ return kwargs
187
+
188
+
189
+ class APIComponentRegistry:
190
+ def __init__(self):
191
+ self._contracts: dict[str, APIComponentContract] = {}
192
+
193
+ def register(self, contract: APIComponentContract) -> APIComponentContract:
194
+ self._contracts[contract.component_type] = contract
195
+ return contract
196
+
197
+ def get(self, component_type: str | None) -> APIComponentContract | None:
198
+ return self._contracts.get(str(component_type or ""))
199
+
200
+ def __contains__(self, component_type: str) -> bool:
201
+ return component_type in self._contracts
202
+
203
+
204
+ API_COMPONENTS = APIComponentRegistry()
205
+
206
+ # RoPE is a standard one-input/one-output MLBricks module. The original API
207
+ # expects a Q/K-like tensor [B,H,T,D]; start_pos defaults to zero in MLBricks,
208
+ # so Studio can use the universal execution path without a TensorGraph branch.
209
+ API_COMPONENTS.register(APIComponentContract(
210
+ component_type="rope",
211
+ import_key="rope",
212
+ input_ports={"main": "x"},
213
+ output_ports={"main": None},
214
+ ))
215
+
216
+ # First proof of the universal path. No BOLT-specific branch is required in
217
+ # TensorGraph: this declaration maps the visual node to Bolt's original API.
218
+ API_COMPONENTS.register(APIComponentContract(
219
+ component_type="bolt",
220
+ import_key="bolt",
221
+ input_ports={"main": "x"},
222
+ output_ports={"main": None},
223
+ parameter_aliases={
224
+ "d_model": ("dim", "hidden_size"),
225
+ "num_heads": ("heads", "head"),
226
+ "backend": ("kernel",),
227
+ },
228
+ runtime_sources={
229
+ "d_model": "model_dim",
230
+ "num_heads": "heads",
231
+ "backend": "backend",
232
+ },
233
+ ))
234
+
235
+ # Multi-input proof: the existing Studio Main/Skip lanes map directly to the
236
+ # original ResController API. Main carries the update tensor; Skip carries
237
+ # the residual stream. TensorGraph does not need a ResController branch.
238
+ API_COMPONENTS.register(APIComponentContract(
239
+ component_type="rescontroller",
240
+ import_key="rescontroller",
241
+ input_ports={"main": "update", "skip": "residual"},
242
+ output_ports={"main": None},
243
+ runtime_sources={
244
+ "backend": "backend",
245
+ },
246
+ ))
247
+ # Stateful multi-input/multi-output proof: SAFFN exposes its original forward
248
+ # signature as named graph ports. The executor maps API return tuple[0] to
249
+ # ``main`` and tuple[1] to ``state``; no SAFFN branch is required in TensorGraph.
250
+ API_COMPONENTS.register(APIComponentContract(
251
+ component_type="saffn",
252
+ import_key="saffn",
253
+ input_ports={
254
+ "x": "x",
255
+ "esa_update": "esa_update",
256
+ "previous_esa": "previous_esa",
257
+ "previous_state": "previous_state",
258
+ },
259
+ output_ports={"main": 0, "state": 1},
260
+ parameter_aliases={
261
+ "d_model": ("dim", "hidden_size"),
262
+ "depth_embedding_dim": ("depth_dim",),
263
+ },
264
+ runtime_sources={
265
+ "d_model": "model_dim",
266
+ "backend": "backend",
267
+ },
268
+ input_initializers={
269
+ # At physical depth 0 there is no earlier signal/state. Studio supplies
270
+ # correctly shaped zeros only when these sockets are left unconnected.
271
+ "previous_esa": {"kind": "zeros_like", "source": "esa_update"},
272
+ "previous_state": {
273
+ # Use the original MLBricks helper so Studio does not duplicate
274
+ # StateAwareFFN's state-shape policy.
275
+ "kind": "module_method",
276
+ "source": "x",
277
+ "method": "initial_state",
278
+ },
279
+ },
280
+ ))
281
+
282
+ # VirtualStateAwareFFN preserves the exact StateAwareFFN forward contract and
283
+ # adds internal virtual refinements. It therefore reuses the same named graph
284
+ # wiring and first-depth initialization policy without a compiler branch.
285
+ API_COMPONENTS.register(APIComponentContract(
286
+ component_type="virtual_saffn",
287
+ import_key="virtual_saffn",
288
+ input_ports={
289
+ "x": "x",
290
+ "esa_update": "esa_update",
291
+ "previous_esa": "previous_esa",
292
+ "previous_state": "previous_state",
293
+ },
294
+ output_ports={"main": 0, "state": 1},
295
+ parameter_aliases={
296
+ "d_model": ("dim", "hidden_size"),
297
+ "depth_embedding_dim": ("depth_dim",),
298
+ },
299
+ runtime_sources={
300
+ "d_model": "model_dim",
301
+ "backend": "backend",
302
+ },
303
+ input_initializers={
304
+ "previous_esa": {"kind": "zeros_like", "source": "esa_update"},
305
+ "previous_state": {
306
+ "kind": "module_method",
307
+ "source": "x",
308
+ "method": "initial_state",
309
+ },
310
+ },
311
+ ))
312
+
313
+ # MicroVirtualFFN is a standard one-input/one-output original MLBricks API.
314
+ # The default forward pass validates one configured refinement pass; callers
315
+ # can still use the full ``refine`` API through a custom API Function node.
316
+ API_COMPONENTS.register(APIComponentContract(
317
+ component_type="micro_ffn",
318
+ import_key="micro_ffn",
319
+ input_ports={"main": "x"},
320
+ output_ports={"main": None},
321
+ parameter_aliases={
322
+ "d_model": ("dim", "hidden_size"),
323
+ },
324
+ runtime_sources={
325
+ "d_model": "model_dim",
326
+ "backend": "backend",
327
+ },
328
+ ))
329
+
330
+ # Vision classification path. Both current MLBricks Kit 1.0.0b2 families take
331
+ # raw image tensors [B,C,H,W] for Serpentine / ViT / CNN and return class logits.
332
+ API_COMPONENTS.register(VisionClassificationContract(
333
+ component_type="vesa",
334
+ import_key="vesa",
335
+ input_ports={"main": "images"},
336
+ output_ports={"main": None},
337
+ runtime_sources={"backend": "backend"},
338
+ ))
339
+
340
+ API_COMPONENTS.register(VisionClassificationContract(
341
+ component_type="visualbolt",
342
+ import_key="visualbolt",
343
+ input_ports={"main": "images"},
344
+ output_ports={"main": None},
345
+ runtime_sources={"backend": "backend"},
346
+ ))
347
+
@@ -0,0 +1,229 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import json
5
+ from copy import deepcopy
6
+ from pathlib import Path
7
+ from typing import Any, Iterable
8
+
9
+ from .import_pool import (
10
+ COMPONENT_IMPORTS,
11
+ CONFIG_KEYS,
12
+ IMPORT_POOL,
13
+ )
14
+
15
+ _SCHEMA_PATH = Path(__file__).with_name("mlbricks_api_schema.json")
16
+
17
+
18
+ # These components intentionally keep the richer source-derived Builder schema
19
+ # instead of replacing it with a plain constructor signature at runtime.
20
+ # SOUP accepts scalar-or-per-layer sequences/config mappings, while the primary
21
+ # ElasticBit 4-32 UI represents ElasticBit.RuntimeMatrix.from_auto.
22
+ SOURCE_DEFINED_FIELDS = {"soup", "elasticbit_runtime", "lm_head"}
23
+
24
+ CHOICES = {
25
+ "backend": ["auto", "native", "pytorch"],
26
+ "precision": ["fp32", "fp16", "bf16"],
27
+ "activation": ["gelu", "gelu_tanh", "relu", "silu", "swish", "tanh"],
28
+ "device": ["auto", "cpu", "cuda", "None"],
29
+ "engine": ["Serpentine", "ViT", "CNN", "Diffusion", "AR"],
30
+ "ffn": ["standard", "ffnbrick", "virtual_ffnbrick", "micro_ffnbrick"],
31
+ "residual": ["standard", "rescontroller"],
32
+ "norm": ["rmsnorm", "layernorm"],
33
+ }
34
+
35
+ COMPONENT_CHOICES = {
36
+ "bolt": {
37
+ "position": ["none", "rope"],
38
+ },
39
+ "vesa": {
40
+ "position": ["auto", "none", "2d_sincos", "learned"],
41
+ "scan": ["cross", "horizontal", "vertical", "raster"],
42
+ },
43
+ "visualbolt": {
44
+ "position": ["auto", "none", "2d_sincos", "learned"],
45
+ "scan": ["cross", "horizontal", "vertical", "raster"],
46
+ },
47
+ }
48
+
49
+
50
+ def _fallback_schema():
51
+ payload = json.loads(_SCHEMA_PATH.read_text(encoding="utf-8"))
52
+ return deepcopy(payload["components"])
53
+
54
+
55
+ def _safe_default(value: Any):
56
+ if value is inspect._empty:
57
+ return None
58
+ if value is None or isinstance(value, (str, int, float, bool)):
59
+ return value
60
+ return str(value)
61
+
62
+
63
+ def _field_type(name, annotation, default, choices=None):
64
+ choices = CHOICES if choices is None else choices
65
+ if name in choices:
66
+ return "select"
67
+ if isinstance(default, bool):
68
+ return "bool"
69
+ if isinstance(default, (int, float)):
70
+ return "number"
71
+ text = "" if annotation is inspect._empty else str(annotation).lower()
72
+ if "bool" in text:
73
+ return "bool"
74
+ if "int" in text or "float" in text:
75
+ return "number"
76
+ return "text"
77
+
78
+
79
+ def _fields(obj, *, choices=None):
80
+ choices = CHOICES if choices is None else choices
81
+ sig = inspect.signature(obj)
82
+ out = []
83
+ for name, p in sig.parameters.items():
84
+ if name in {"self", "args", "kwargs"}:
85
+ continue
86
+ default = _safe_default(p.default)
87
+ item = {
88
+ "key": name,
89
+ "label": name.replace("_", " ").title(),
90
+ "type": _field_type(name, p.annotation, default, choices),
91
+ "required": p.default is inspect._empty,
92
+ "value": default,
93
+ "annotation": "" if p.annotation is inspect._empty else str(p.annotation),
94
+ }
95
+ if name in choices:
96
+ item["options"] = choices[name]
97
+ out.append(item)
98
+ return sig, out
99
+
100
+
101
+ def _canonical_metadata(component_type: str, fallback: dict[str, Any]) -> dict[str, Any]:
102
+ info = IMPORT_POOL.import_info(component_type)
103
+ canonical_path = info.get("canonical_path")
104
+ public_name = info.get("canonical_symbol") or fallback.get("public_name")
105
+ payload = {
106
+ **fallback,
107
+ "available": True,
108
+ "runtime_available": None,
109
+ "source": "MLBricks source schema + lazy import pool",
110
+ "public_name": public_name,
111
+ "import_path": canonical_path or fallback.get("import_path"),
112
+ "import_module": info.get("canonical_module"),
113
+ "import_symbol": info.get("canonical_symbol"),
114
+ "import_candidates": [candidate.path for candidate in COMPONENT_IMPORTS.get(component_type, ())],
115
+ "loaded": bool(info.get("loaded")),
116
+ "resolved_from": info.get("resolved_from"),
117
+ "runtime_error": info.get("error"),
118
+ }
119
+ if info.get("config_path"):
120
+ config = deepcopy(payload.get("config_api") or {})
121
+ config.update({
122
+ "public_name": info.get("config_symbol"),
123
+ "import_path": info.get("config_path"),
124
+ "import_module": info.get("config_module"),
125
+ "import_symbol": info.get("config_symbol"),
126
+ })
127
+ payload["config_api"] = config
128
+ return payload
129
+
130
+
131
+ def _inspect_one(component_type: str, fallback: dict[str, Any]) -> dict[str, Any]:
132
+ result = _canonical_metadata(component_type, fallback)
133
+ try:
134
+ obj = IMPORT_POOL.resolve_component(component_type)
135
+ component_choices = {**CHOICES, **COMPONENT_CHOICES.get(component_type, {})}
136
+ sig, inspected_fields = _fields(obj, choices=component_choices)
137
+ source_defined = component_type in SOURCE_DEFINED_FIELDS
138
+ fields = deepcopy(fallback.get("parameters", [])) if source_defined else inspected_fields
139
+
140
+ config_info = result.get("config_api")
141
+ if component_type in CONFIG_KEYS:
142
+ cfg_obj = IMPORT_POOL.resolve_config(component_type)
143
+ cfg_choices = {**CHOICES, **COMPONENT_CHOICES.get(component_type, {})}
144
+ cfg_sig, cfg_fields = _fields(cfg_obj, choices=cfg_choices)
145
+ if len(cfg_fields) > 1:
146
+ fields = cfg_fields
147
+ config_info = {
148
+ **(config_info or {}),
149
+ "public_name": cfg_obj.__name__,
150
+ "signature": f"{cfg_obj.__name__}{cfg_sig}",
151
+ "parameters": cfg_fields,
152
+ }
153
+
154
+ doc = inspect.getdoc(obj) or ""
155
+ runtime_available = True
156
+ runtime_error = None
157
+ if component_type == "elasticbit_runtime":
158
+ checker = getattr(obj, "native_runtime_available", None)
159
+ if callable(checker):
160
+ try:
161
+ runtime_available = bool(checker())
162
+ except Exception as exc:
163
+ runtime_available = False
164
+ runtime_error = f"{type(exc).__name__}: {exc}"
165
+ if not runtime_available and runtime_error is None:
166
+ runtime_error = "ElasticBit runtime is not built in this environment."
167
+
168
+ info = IMPORT_POOL.import_info(component_type)
169
+ return {
170
+ **result,
171
+ "available": True,
172
+ "runtime_available": runtime_available,
173
+ "source": "runtime inspection + MLBricks source schema" if source_defined else "runtime inspection",
174
+ "public_name": fallback.get("public_name", obj.__name__) if source_defined else obj.__name__,
175
+ "signature": fallback.get("signature", f"{obj.__name__}{sig}") if source_defined else f"{obj.__name__}{sig}",
176
+ "description": fallback.get("description", "") if source_defined else (doc.splitlines()[0] if doc else fallback.get("description", "")),
177
+ "parameters": fields if fields else fallback.get("parameters", []),
178
+ "config_api": config_info,
179
+ "runtime_error": runtime_error,
180
+ "loaded": True,
181
+ "resolved_from": info.get("resolved_from"),
182
+ }
183
+ except Exception as exc:
184
+ info = IMPORT_POOL.import_info(component_type)
185
+ return {
186
+ **result,
187
+ "available": True,
188
+ "runtime_available": False,
189
+ "loaded": False,
190
+ "source": "MLBricks source schema + lazy import pool",
191
+ "runtime_error": f"{type(exc).__name__}: {exc}",
192
+ "resolved_from": info.get("resolved_from"),
193
+ }
194
+
195
+
196
+ def discover_mlbricks_api(
197
+ component_types: Iterable[str] | None = None,
198
+ *,
199
+ eager: bool = False,
200
+ ):
201
+ """Return Builder API metadata without requiring eager MLBricks imports.
202
+
203
+ By default, the supplied source-derived schema is used and canonical import
204
+ routes are attached to each component. If ``eager=True`` (or a specific
205
+ ``component_types`` iterable is supplied), only those requested components
206
+ are imported and inspected. This is the import-pool behavior used by the
207
+ Builder UI: adding a component can warm just that component's API instead of
208
+ importing the whole MLBricks package.
209
+ """
210
+ result = _fallback_schema()
211
+ requested = set(str(x) for x in (component_types or ()))
212
+ inspect_all = bool(eager and component_types is None)
213
+
214
+ for component_type in COMPONENT_IMPORTS:
215
+ fallback = result.get(component_type, {})
216
+ if inspect_all or component_type in requested:
217
+ result[component_type] = _inspect_one(component_type, fallback)
218
+ else:
219
+ result[component_type] = _canonical_metadata(component_type, fallback)
220
+
221
+ return result
222
+
223
+
224
+ def refresh_component_api(component_type: str) -> dict[str, Any] | None:
225
+ component_type = str(component_type)
226
+ if component_type not in COMPONENT_IMPORTS:
227
+ return None
228
+ fallback = _fallback_schema().get(component_type, {})
229
+ return _inspect_one(component_type, fallback)