diff --git a/CHANGELOG.md b/CHANGELOG.md index 67d4db56..a7209bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.44.0 (2026-07-21) + +- **pgw#617: hierarchical slot bindings (th#980 companion).** + `RunJob.ModelBinding` gains `components` (field 5, component name -> + canonical tensorhub ref). The worker loads the base composition and + substitutes each named component from its OWN materialized snapshot via + the gw#479 `components=` from_pretrained injection (load-then-substitute). + The composition (base + sorted component refs) is the instance/residency + identity — a component-only rebind derives a new instance and reconcile + reloads it; flat bindings (empty map) are byte-identical to 0.43.x. + Unknown component names, non-CAS override refs, and overrides on + self-loading (str/Path) slots refuse typed (`ComponentSubstitutionError`) + at setup, never mid-denoise. Override refs join job pins, held refs/ + digests, and compile-cell binding facts. +- **pgw#617: `selected_by=` slots may omit `default_checkpoint`.** Deploy- + time bindings (th#980) seed the hub mapping now, so the registry's + author-time requirement is dropped (mirror of tensorhub's relaxed + registration rule). Unblocks the ie#524 de-hardcode sweep of + request-branching endpoints (wan-2.2, sdxl slot-model, z-image). + ## 0.43.1 (2026-07-21) - **gw#608 CLOSED: self-mint arm is transactional over the process-global diff --git a/proto/CONTRACT.md b/proto/CONTRACT.md index ab956fcb..95a7ef69 100644 --- a/proto/CONTRACT.md +++ b/proto/CONTRACT.md @@ -450,6 +450,7 @@ the endpoint's declared `Resources` + probed free VRAM, never the wire. | `ref` | O resolver | W `ensure_local` + injection | canonical ref string | | `loras` | O `_models` override gate (AllowLora) + BYOM ingest (th#585) | W per-request adapter overlay (gw#393) + adapter residency (gw#399) | LoRA overlays riding this slot's base model; attached as unfused named adapters that stay resident on the pipeline, ACTIVE only for jobs that name them (explicit activation — adapter-free jobs always run with adapters disabled). Purely W-side: O never routes on adapter residency. Empty for adapter-free jobs; workers predating the field ignore it | | `inference_defaults` | O repo-metadata store (th#767c: PUT-time validated against the family's exported JSON Schema) | W `ctx.slots[slot].defaults` resolution chain (pgw#520) | JSON object, an instance of the slot's family vocabulary struct (`gen_worker.families.FamilyDefaults` subclass). Empty when the resolved repo carries no metadata — W merges by falling back to the endpoint's code-declared `Slot(fallback=...)` preset; empty AND no fallback ⇒ `ctx.slots[slot]` raises when the handler reads it. Additive: workers predating the field ignore it (Slot resolution falls back to the code preset unconditionally) | +| `components` | O deploy-time binding config (th#980: per-component validated against the pipeline family's component schema) | W load-then-substitute (pgw#617): base pipeline `from_pretrained` with each named component loaded from its OWN materialized snapshot and injected via the `components=` kwarg (gw#479 mechanism) | component name → canonical tensorhub ref (same grammar as `ref`); snapshots ride `RunJob.snapshots`/`DesiredResidency` exactly like base refs. The composition (base + sorted overrides) IS the instance/residency identity — a component-only rebind derives a new instance. Empty map = flat binding, byte-identical to pre-#617. Unknown component name = typed setup refusal (`ComponentSubstitutionError`). Additive: old workers ignore it | ### LoraOverlay (embedded in ModelBinding.loras) diff --git a/proto/worker_scheduler.proto b/proto/worker_scheduler.proto index 4d1023b0..9100ecff 100644 --- a/proto/worker_scheduler.proto +++ b/proto/worker_scheduler.proto @@ -417,6 +417,13 @@ message ModelBinding { // (precedence: explicit payload value > this > endpoint fallback preset). // Additive: old workers ignore this field. string inference_defaults = 4; + // th#980 hierarchical slots: per-component repo substitutions on the base + // composition, component name -> canonical tensorhub ref (normal form, + // same grammar as `ref`). The worker loads the base composition then + // substitutes each named component from its materialized snapshot + // (pgw#617). Snapshots for these refs ride RunJob.snapshots / + // DesiredResidency exactly like base refs. Additive: old workers ignore. + map components = 5; } // One LoRA adapter overlay: a resolved tensorhub ref + its adapter weight. diff --git a/pyproject.toml b/pyproject.toml index 193f2aef..84cad4cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gen-worker" -version = "0.43.1" +version = "0.44.0" description = "A library used to build custom functions in Cozy Creator's serverless function platform." readme = "README.md" license = "MIT" diff --git a/src/gen_worker/api/binding.py b/src/gen_worker/api/binding.py index ff8cde10..0bad1ba0 100644 --- a/src/gen_worker/api/binding.py +++ b/src/gen_worker/api/binding.py @@ -86,6 +86,12 @@ class ModelRef(msgspec.Struct, frozen=True): version: str = "" files: tuple[str, ...] = () components: tuple[str, ...] = () + # pgw#617 hierarchical bindings (tensorhub only): sorted (component name, + # canonical ref) substitutions on the base composition — stamped from + # ModelBinding.components at dispatch, never declared by endpoint code. + # Part of the binding's identity: a component-only rebind derives a new + # instance/residency identity. + component_overrides: tuple[tuple[str, str], ...] = () def __post_init__(self) -> None: # msgspec.structs.force_setattr, NOT object.__setattr__: the latter @@ -103,8 +109,17 @@ def __post_init__(self) -> None: force(self, "version", _clean(self.version)) force(self, "files", tuple(_clean(p) for p in self.files if _clean(p))) force(self, "components", tuple(_clean(p) for p in self.components if _clean(p))) + force(self, "component_overrides", tuple(sorted( + (_clean(k), _clean(v)) + for k, v in self.component_overrides if _clean(k) and _clean(v) + ))) force(self, "storage_dtype", _clean_storage_dtype(self.storage_dtype)) + if self.component_overrides and self.source != "tensorhub": + raise ValueError( + "component_overrides= is tensorhub-only (pgw#617: refs are " + "tensorhub-CAS, mirror-first)" + ) if self.source == "tensorhub": if not self.tag: msgspec.structs.force_setattr(self, "tag", "latest") diff --git a/src/gen_worker/api/errors.py b/src/gen_worker/api/errors.py index a0f5269f..09184859 100644 --- a/src/gen_worker/api/errors.py +++ b/src/gen_worker/api/errors.py @@ -103,6 +103,30 @@ def __init__( ) +class ComponentSubstitutionError(WorkerError): + """A dispatched component substitution cannot apply to the slot's base + composition (pgw#617/th#980 hierarchical bindings). + + Named-axis refusal: the message carries function, slot, component name + and the base's known component vocabulary (or the failing override ref). + Raised at setup/injection time, never mid-denoise. + """ + + def __init__( + self, function: str, slot: str, component: str, *, detail: str = "", + ) -> None: + self.function = str(function or "") + self.slot = str(slot or "") + self.component = str(component or "") + msg = ( + f"{self.function!r} slot {self.slot!r}: component substitution " + f"{self.component!r} cannot apply" + ) + if detail: + msg = f"{msg}: {detail}" + super().__init__(msg) + + class RefCompatibilitySurprise(ValidationError): """Post-download runtime mismatch on a caller-supplied PAYLOAD_REF. diff --git a/src/gen_worker/executor.py b/src/gen_worker/executor.py index f6e2949d..74123e96 100644 --- a/src/gen_worker/executor.py +++ b/src/gen_worker/executor.py @@ -34,6 +34,7 @@ from .api.errors import ( ArtifactTransferError, CanceledError, + ComponentSubstitutionError, ModelSlotIdentityError, RetryableError, ValidationError, @@ -294,6 +295,29 @@ def _hub_binding_for_wire_ref(ref: str) -> ModelRef: ) +def _component_overrides(binding: Any) -> Tuple[Tuple[str, str], ...]: + """(component, canonical ref) substitutions the binding carries (pgw#617).""" + return tuple(getattr(binding, "component_overrides", ()) or ()) + + +def _binding_wire_refs(binding: Any) -> List[str]: + """The base wire ref plus every component-override ref (pgw#617): the + full set of refs materializing this binding pins/downloads.""" + return [wire_ref(binding), *(ref for _, ref in _component_overrides(binding))] + + +def _alias_binding_matches(alias: "EndpointSpec", slot_key: str, ref: str) -> bool: + """Does ``alias`` hold this load-time binding fact? ``slot_key`` is a + slot name or ``.`` override key (pgw#617).""" + base, _, comp = slot_key.partition(".") + binding = alias.models.get(base) + if binding is None: + return False + if not comp: + return wire_ref(binding).strip() == ref + return (comp, ref) in _component_overrides(binding) + + def _map_exception(exc: BaseException) -> Tuple["pb.JobStatus", str]: """-> (JobStatus, safe_message).""" if isinstance(exc, (CanceledError, asyncio.CancelledError)): @@ -2499,7 +2523,9 @@ def _install_compile_targets( # Production injection supplies object-scoped slot ownership. Keep # bare objects accepted for focused unit construction only, deriving # their ownership from the record's already-frozen held bindings. - all_slots = {slot for slot, _ref, _digest in rec.held_bindings} + all_slots = { + slot.partition(".")[0] for slot, _ref, _digest in rec.held_bindings + } candidates = [ item if isinstance(item, _CompileObjectCandidate) else _CompileObjectCandidate(item, set(all_slots)) @@ -2522,7 +2548,7 @@ def _install_compile_targets( continue bindings = tuple(sorted( binding for binding in rec.held_bindings - if binding[0] in candidate.slots + if binding[0].partition(".")[0] in candidate.slots )) bindings_valid = bool(bindings) and all( slot.strip() and ref.strip() and digest.strip() @@ -2575,8 +2601,7 @@ def _install_compile_targets( except Exception: continue if any( - slot not in alias.models - or wire_ref(alias.models[slot]).strip() != ref + not _alias_binding_matches(alias, slot, ref) for slot, ref, _digest in bindings ): continue @@ -3009,10 +3034,42 @@ def _effective_spec(self, spec: EndpointSpec, run: "pb.RunJob") -> EndpointSpec: run_refs = { b.slot: b.ref.strip() for b in run.models if b.slot and b.ref.strip() } + # pgw#617 hierarchical bindings: per-component substitutions ride the + # dispatch binding and become part of the derived spec's identity — + # a component-only rebind derives a NEW instance (reload), a flat + # binding (empty map) is byte-identical to the pre-#617 path. + run_comps: Dict[str, Dict[str, str]] = {} + for b in run.models: + comps = { + str(k).strip(): str(v).strip() + for k, v in b.components.items() + if str(k).strip() and str(v).strip() + } + if not comps: + continue + if b.slot not in spec.slots: + logger.warning( + "component substitutions on %s slot %r ignored: not a " + "declared Slot", spec.name, b.slot) + continue + run_comps[b.slot] = comps effective = dict(spec.models) for slot in spec.slots: - effective[slot] = self._slot_dispatch_binding( + binding = self._slot_dispatch_binding( spec, slot, run_refs.get(slot, "")) + slot_comps = run_comps.get(slot) or {} + if slot_comps: + for comp, comp_ref in slot_comps.items(): + try: + self._hub_binding(comp_ref) + except ValueError: + raise ComponentSubstitutionError( + spec.name, slot, comp, + detail=f"override ref {comp_ref!r} is not a " + "tensorhub-CAS ref") from None + binding = msgspec.structs.replace( + binding, component_overrides=tuple(sorted(slot_comps.items()))) + effective[slot] = binding if effective == spec.models: return spec return dc_replace(spec, models=effective) @@ -3219,7 +3276,8 @@ def _job_pin_refs(self, spec: EndpointSpec, slots: List[str]) -> List[str]: lane_refs = rec.lane_refs if rec is not None else set() return [ r for s in slots - if (r := wire_ref(spec.models[s])) not in lane_refs + for r in _binding_wire_refs(spec.models[s]) + if r not in lane_refs ] def _class_record(self, spec: EndpointSpec) -> _ClassRecord: @@ -3271,8 +3329,11 @@ async def ensure_setup( rec = self._class_record(spec) async with rec.lock: if rec.ready and not rec.stale: - for slot in self._setup_slots(spec): - ref = wire_ref(spec.models[slot]) + setup_refs = [ + r for slot in self._setup_slots(spec) + for r in _binding_wire_refs(spec.models[slot]) + ] + for ref in setup_refs: wanted = self.store.snapshot_digest( ref, (snapshots or {}).get(ref) ) @@ -3748,6 +3809,10 @@ async def _setup_locked( } slot_identities: Dict[str, _ResidencyIdentity] = {} paths: Dict[str, str] = {} + # pgw#617: component-override materializations, slot -> comp -> local + # path, plus per-override-ref snapshot digests (identity facts). + component_paths: Dict[str, Dict[str, str]] = {} + override_digests: Dict[str, str] = {} for slot in setup_slots: binding = spec.models[slot] ref = slot_refs[slot] @@ -3756,6 +3821,20 @@ async def _setup_locked( ref, snap, binding=binding) paths[slot] = str(materialized.path) slot_identities[slot] = materialized.identity + for comp, comp_ref in _component_overrides(binding): + try: + comp_binding = self._hub_binding(comp_ref) + except ValueError: + raise ComponentSubstitutionError( + spec.name, slot, comp, + detail=f"override ref {comp_ref!r} is not a " + "tensorhub-CAS ref") from None + comp_mat = await self.store._materialize_local( + comp_ref, (snapshots or {}).get(comp_ref), + binding=comp_binding) + component_paths.setdefault(slot, {})[comp] = str(comp_mat.path) + if comp_mat.identity[0]: + override_digests[comp_ref] = comp_mat.identity[0] compile_selection = await self._fetch_compile_snapshot(spec, snapshots) compile_artifact = compile_selection.path if compile_selection else None # Loads serialize: concurrent setups would cross-contaminate each @@ -3772,19 +3851,40 @@ async def _setup_locked( # exception or cancellation can now tear down the exact instance # and exact resolved refs instead of losing them in stack locals. rec.instance = instance - rec.held_refs = sorted(set(slot_refs.values())) + rec.held_refs = sorted( + set(slot_refs.values()) | set(override_digests) + | { + comp_ref + for slot in setup_slots + for _, comp_ref in _component_overrides(spec.models[slot]) + } + ) rec.held_snapshot_digests = { slot_refs[slot]: identity[0] for slot, identity in slot_identities.items() if slot in slot_refs and identity[0] } + rec.held_snapshot_digests.update(override_digests) + # Override triples key as "." — part of the + # composition's identity (compile-cell applicability, pgw#617). rec.held_bindings = sorted( - ( - slot, - ref, - rec.held_snapshot_digests.get(ref, ""), - ) - for slot, ref in slot_refs.items() + [ + ( + slot, + ref, + rec.held_snapshot_digests.get(ref, ""), + ) + for slot, ref in slot_refs.items() + ] + + [ + ( + f"{slot}.{comp}", + comp_ref, + rec.held_snapshot_digests.get(comp_ref, ""), + ) + for slot in setup_slots + for comp, comp_ref in _component_overrides(spec.models[slot]) + ] ) setup = getattr(instance, "setup", None) inj = _InjectionResult(kwargs={}, loaded={}) @@ -3798,7 +3898,8 @@ async def _setup_locked( spec, setup, paths, server=rec.server, compile_selection=compile_selection, snapshots=snapshots, - slot_identities=slot_identities) + slot_identities=slot_identities, + component_paths=component_paths) rec.shared_keys.extend(inj.shared_keys) # pgw#517: a self-loading (str/Path-slot) endpoint builds its # own pipeline inside setup() and the executor never sees it @@ -5051,6 +5152,7 @@ async def _injection_kwargs( compile_selection: Optional[_CompileArtifactSelection] = None, snapshots: Optional[Dict[str, pb.Snapshot]] = None, slot_identities: Optional[Dict[str, _ResidencyIdentity]] = None, + component_paths: Optional[Dict[str, Dict[str, str]]] = None, ) -> "_InjectionResult": """Typed injection: each slot receives exactly what its ``setup`` annotation says — a ``str``/``Path`` local path, or a constructed @@ -5084,6 +5186,17 @@ async def _injection_kwargs( kwargs[pname] = server for slot, path in paths.items(): ann = hints.get(slot) + overrides = dict((component_paths or {}).get(slot) or {}) + if overrides and not ( + isinstance(ann, type) + and callable(getattr(ann, "from_pretrained", None)) + ): + # pgw#617: substitution requires a worker-loaded pipeline + # slot; a self-loading str/Path slot never sees components. + raise ComponentSubstitutionError( + spec.name, slot, sorted(overrides)[0], + detail="slot is not worker-loaded (no pipeline-class " + "annotation); components cannot substitute") if ann is None or ann is str: kwargs[slot] = path elif ann is Path: @@ -5108,6 +5221,28 @@ async def _injection_kwargs( slot_share = {} res = self.store.residency injected: Dict[str, Any] = {} + if overrides: + # pgw#617 load-then-substitute: each override component + # loads from its OWN materialized tree and rides the + # same from_pretrained components= injection as gw#479 + # shared modules. Unknown names refuse typed at setup. + valid = self._model_index_components(path) + unknown = sorted(set(overrides) - valid) + if unknown: + raise ComponentSubstitutionError( + spec.name, slot, unknown[0], + detail=f"base composition {ref!r} declares " + f"components {sorted(valid)}") + for comp in overrides: + # An overridden component never rides the shared + # cache: its bytes differ from the base's. + slot_share.pop(comp, None) + from .models.loading import load_component_override + + for comp, comp_path in sorted(overrides.items()): + injected[comp] = await _to_thread_complete( + load_component_override, path, comp, comp_path, + dtype=str(getattr(binding, "dtype", "") or "")) if slot_share: valid = self._model_index_components(path) for comp, key in list(slot_share.items()): diff --git a/src/gen_worker/models/loading.py b/src/gen_worker/models/loading.py index 68cc33b5..5609ece0 100644 --- a/src/gen_worker/models/loading.py +++ b/src/gen_worker/models/loading.py @@ -845,6 +845,63 @@ def model_index_components(path: str | Path) -> set: return set() +def model_index_entry(path: str | Path, component: str) -> Optional[tuple]: + """``(library, class_name)`` the tree's model_index.json declares for + ``component``, or None when absent/unreadable.""" + try: + with open(Path(path) / "model_index.json", "r", encoding="utf-8") as f: + index = json.load(f) + entry = index.get(component) + if (isinstance(entry, (list, tuple)) and len(entry) == 2 + and all(isinstance(e, str) and e for e in entry)): + return (entry[0], entry[1]) + except Exception: + pass + return None + + +def load_component_override( + base_path: str | Path, component: str, override_path: str | Path, + *, dtype: str = "", +) -> Any: + """Load one named pipeline component from an OVERRIDE snapshot tree + (pgw#617 hierarchical bindings). The module class comes from the BASE + tree's model_index.json; weights come from the override tree's + ``/`` subtree when present, else its root. ``dtype`` (the + base binding's) wins; otherwise the override's on-disk dtype is + honored. Blocking; callers on an event loop run it off-thread.""" + import importlib + + entry = model_index_entry(base_path, component) + if entry is None: + raise ValueError( + f"component {component!r} is not in the base composition " + f"(model_index components: " + f"{sorted(model_index_components(base_path))})" + ) + library, class_name = entry + module = importlib.import_module(library) + cls = getattr(module, class_name, None) + if cls is None or not callable(getattr(cls, "from_pretrained", None)): + raise ValueError( + f"{library}.{class_name} declares no from_pretrained loader" + ) + src = Path(override_path) / component + if not src.is_dir(): + src = Path(override_path) + kwargs: Dict[str, Any] = {} + wanted = dtype or detect_on_disk_dtype(src) + if wanted in ("bf16", "fp16", "bfloat16", "float16", "fp32", "float32"): + try: + kwargs["torch_dtype"] = get_torch_dtype(wanted) + except ImportError: + pass # torch-less environment: loader fails on its own terms + try: + return cls.from_pretrained(str(src), **kwargs) + except TypeError: + return cls.from_pretrained(str(src)) + + def _safetensors_data_bytes(p: Path) -> int: import struct diff --git a/src/gen_worker/pb/worker_scheduler_pb2.py b/src/gen_worker/pb/worker_scheduler_pb2.py index 3677287c..68700397 100644 --- a/src/gen_worker/pb/worker_scheduler_pb2.py +++ b/src/gen_worker/pb/worker_scheduler_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16worker_scheduler.proto\x12\x0e\x63ozy.scheduler\"\xe6\x03\n\rWorkerMessage\x12&\n\x05hello\x18\x01 \x01(\x0b\x32\x15.cozy.scheduler.HelloH\x00\x12\x31\n\x0bstate_delta\x18\x02 \x01(\x0b\x32\x1a.cozy.scheduler.StateDeltaH\x00\x12\x33\n\x0cjob_accepted\x18\x03 \x01(\x0b\x32\x1b.cozy.scheduler.JobAcceptedH\x00\x12/\n\njob_result\x18\x04 \x01(\x0b\x32\x19.cozy.scheduler.JobResultH\x00\x12\x33\n\x0cjob_progress\x18\x05 \x01(\x0b\x32\x1b.cozy.scheduler.JobProgressH\x00\x12\x31\n\x0bmodel_event\x18\x06 \x01(\x0b\x32\x1a.cozy.scheduler.ModelEventH\x00\x12\x37\n\x0e\x66n_unavailable\x18\x07 \x01(\x0b\x32\x1d.cozy.scheduler.FnUnavailableH\x00\x12\x31\n\x0b\x66n_degraded\x18\x08 \x01(\x0b\x32\x1a.cozy.scheduler.FnDegradedH\x00\x12\x39\n\x0f\x61\x63tivity_update\x18\t \x01(\x0b\x32\x1e.cozy.scheduler.ActivityUpdateH\x00\x42\x05\n\x03msg\"\xb0\x02\n\x10SchedulerMessage\x12-\n\thello_ack\x18\x01 \x01(\x0b\x32\x18.cozy.scheduler.HelloAckH\x00\x12)\n\x07run_job\x18\x02 \x01(\x0b\x32\x16.cozy.scheduler.RunJobH\x00\x12/\n\ncancel_job\x18\x03 \x01(\x0b\x32\x19.cozy.scheduler.CancelJobH\x00\x12+\n\x08model_op\x18\x04 \x01(\x0b\x32\x17.cozy.scheduler.ModelOpH\x00\x12&\n\x05\x64rain\x18\x05 \x01(\x0b\x32\x15.cozy.scheduler.DrainH\x00\x12\x35\n\rtoken_refresh\x18\x06 \x01(\x0b\x32\x1c.cozy.scheduler.TokenRefreshH\x00\x42\x05\n\x03msg\"\xc7\x02\n\x05Hello\x12\x39\n\x10protocol_version\x18\x01 \x01(\x0e\x32\x1f.cozy.scheduler.ProtocolVersion\x12\x11\n\tworker_id\x18\x02 \x01(\t\x12\x12\n\nrelease_id\x18\x03 \x01(\t\x12\x32\n\tresources\x18\x04 \x01(\x0b\x32\x1f.cozy.scheduler.WorkerResources\x12)\n\x05state\x18\x05 \x01(\x0b\x32\x1a.cozy.scheduler.StateDelta\x12.\n\x06models\x18\x06 \x03(\x0b\x32\x1e.cozy.scheduler.ModelResidency\x12.\n\tin_flight\x18\x07 \x03(\x0b\x32\x1b.cozy.scheduler.InFlightJob\x12\x1d\n\x15heartbeat_interval_ms\x18\x08 \x01(\x04\"\x9b\x02\n\x0fWorkerResources\x12\x11\n\tgpu_count\x18\x01 \x01(\x05\x12\x18\n\x10vram_total_bytes\x18\x02 \x01(\x03\x12\x10\n\x08gpu_name\x18\x03 \x01(\t\x12\x0e\n\x06gpu_sm\x18\x04 \x01(\t\x12\x16\n\x0einstalled_libs\x18\x05 \x03(\t\x12\x14\n\x0cimage_digest\x18\x06 \x01(\t\x12\x12\n\ngit_commit\x18\x07 \x01(\t\x12\x13\n\x0binstance_id\x18\x08 \x01(\t\x12/\n\x0bhost_canary\x18\t \x01(\x0b\x32\x1a.cozy.scheduler.HostCanary\x12\x15\n\rtorch_version\x18\n \x01(\t\x12\x1a\n\x12gen_worker_version\x18\x0b \x01(\t\"\xc9\x01\n\nHostCanary\x12\x13\n\x0bmemcpy_gbps\x18\x01 \x01(\x01\x12\x10\n\x08h2d_gbps\x18\x02 \x01(\x01\x12\x10\n\x08\x64\x32h_gbps\x18\x03 \x01(\x01\x12\x17\n\x0fpinned_alloc_ok\x18\x04 \x01(\x08\x12\x17\n\x0f\x63pu_single_mbps\x18\x05 \x01(\x01\x12\x16\n\x0e\x63pu_multi_mbps\x18\x06 \x01(\x01\x12\r\n\x05vcpus\x18\x07 \x01(\x05\x12\x14\n\x0cram_total_gb\x18\x08 \x01(\x01\x12\x13\n\x0b\x64uration_ms\x18\t \x01(\x03\"\x95\x01\n\x0eModelResidency\x12\x0b\n\x03ref\x18\x01 \x01(\t\x12+\n\x04tier\x18\x02 \x01(\x0e\x32\x1d.cozy.scheduler.ResidencyTier\x12\x12\n\nvram_bytes\x18\x03 \x01(\x03\x12\x17\n\x0fsnapshot_digest\x18\x04 \x01(\t\x12\x1c\n\x14residency_generation\x18\x05 \x01(\x04\"2\n\x0bInFlightJob\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\"\xdd\x01\n\x08HelloAck\x12\x39\n\x10protocol_version\x18\x01 \x01(\x0e\x32\x1f.cozy.scheduler.ProtocolVersion\x12\x15\n\rfile_base_url\x18\x02 \x01(\t\x12\x0c\n\x04keep\x18\x03 \x03(\t\x12\x34\n\x0bresolutions\x18\x04 \x03(\x0b\x32\x1f.cozy.scheduler.ModelResolution\x12;\n\x11\x64\x65sired_residency\x18\x05 \x01(\x0b\x32 .cozy.scheduler.DesiredResidency\"\xf7\x01\n\x10\x44\x65siredResidency\x12\x12\n\ngeneration\x18\x01 \x01(\x04\x12\x11\n\tdisk_refs\x18\x02 \x03(\t\x12,\n\x03hot\x18\x03 \x03(\x0b\x32\x1f.cozy.scheduler.DesiredInstance\x12\x42\n\tsnapshots\x18\x04 \x03(\x0b\x32/.cozy.scheduler.DesiredResidency.SnapshotsEntry\x1aJ\n\x0eSnapshotsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\'\n\x05value\x18\x02 \x01(\x0b\x32\x18.cozy.scheduler.Snapshot:\x02\x38\x01\"V\n\x0f\x44\x65siredInstance\x12\x15\n\rfunction_name\x18\x01 \x01(\t\x12,\n\x06models\x18\x02 \x03(\x0b\x32\x1c.cozy.scheduler.ModelBinding\"P\n\x0fModelResolution\x12\x0b\n\x03ref\x18\x01 \x01(\t\x12\x14\n\x0cresolved_ref\x18\x02 \x01(\t\x12\x0c\n\x04\x63\x61st\x18\x03 \x01(\t\x12\x0c\n\x04lane\x18\x04 \x01(\t\"\xe8\x02\n\nStateDelta\x12*\n\x05phase\x18\x01 \x01(\x0e\x32\x1b.cozy.scheduler.WorkerPhase\x12\x1b\n\x13\x61vailable_functions\x18\x02 \x03(\t\x12\x19\n\x11loading_functions\x18\x03 \x03(\t\x12\x17\n\x0f\x66ree_vram_bytes\x18\x04 \x01(\x03\x12\x17\n\x0f\x66inalizing_jobs\x18\x05 \x01(\x05\x12%\n\x1dobserved_residency_generation\x18\x06 \x01(\x04\x12\x36\n\x0f\x63ompile_targets\x18\x07 \x03(\x0b\x32\x1d.cozy.scheduler.CompileTarget\x12\x30\n\x0c\x63\x65ll_lookups\x18\x08 \x03(\x0b\x32\x1a.cozy.scheduler.CellLookup\x12\x33\n\ndisk_usage\x18\t \x01(\x0b\x32\x1f.cozy.scheduler.DiskUsageReport\"\xa9\x01\n\x10StorageTierUsage\x12)\n\x04tier\x18\x01 \x01(\x0e\x32\x1b.cozy.scheduler.StorageTier\x12\x12\n\nmount_path\x18\x02 \x01(\t\x12\x13\n\x0btotal_bytes\x18\x03 \x01(\x03\x12\x12\n\nfree_bytes\x18\x04 \x01(\x03\x12\x12\n\nused_bytes\x18\x05 \x01(\x03\x12\x19\n\x11reclaimable_bytes\x18\x06 \x01(\x03\"_\n\x0f\x44iskUsageReport\x12/\n\x05tiers\x18\x01 \x03(\x0b\x32 .cozy.scheduler.StorageTierUsage\x12\x1b\n\x13\x63\x61pacity_generation\x18\x02 \x01(\x04\".\n\nCellLookup\x12\x0e\n\x06\x66\x61mily\x18\x01 \x01(\t\x12\x10\n\x08\x63\x65ll_key\x18\x02 \x01(\t\"\xc6\x03\n\rCompileTarget\x12\x16\n\x0eincarnation_id\x18\x01 \x01(\t\x12\x0e\n\x06\x66\x61mily\x18\x02 \x01(\t\x12\x1c\n\x14pipeline_weight_lane\x18\x03 \x01(\t\x12\x13\n\x0blora_bucket\x18\x04 \x01(\x05\x12\x17\n\x0f\x63ontract_digest\x18\x05 \x01(\t\x12\x1a\n\x12\x61\x63tive_compile_ref\x18\x06 \x01(\t\x12&\n\x1e\x61\x63tive_compile_snapshot_digest\x18\x07 \x01(\t\x12\x16\n\x0e\x66unction_names\x18\x08 \x03(\t\x12<\n\x0emodel_bindings\x18\t \x03(\x0b\x32$.cozy.scheduler.CompileTargetBinding\x12\x1a\n\x12requested_cell_key\x18\n \x01(\t\x12Q\n\x13requested_cell_axes\x18\x0b \x03(\x0b\x32\x34.cozy.scheduler.CompileTarget.RequestedCellAxesEntry\x1a\x38\n\x16RequestedCellAxesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"J\n\x14\x43ompileTargetBinding\x12\x0c\n\x04slot\x18\x01 \x01(\t\x12\x0b\n\x03ref\x18\x02 \x01(\t\x12\x17\n\x0fsnapshot_digest\x18\x03 \x01(\t\"\xc8\x04\n\x06RunJob\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\x12\x15\n\rfunction_name\x18\x03 \x01(\t\x12\x15\n\rinput_payload\x18\x04 \x01(\x0c\x12\x12\n\ntimeout_ms\x18\x05 \x01(\x03\x12\x0e\n\x06tenant\x18\x06 \x01(\t\x12\x12\n\ninvoker_id\x18\x07 \x01(\t\x12\x18\n\x10\x63\x61pability_token\x18\x08 \x01(\t\x12/\n\x0boutput_mode\x18\t \x01(\x0e\x32\x1a.cozy.scheduler.OutputMode\x12\x30\n\x07\x63ompute\x18\n \x01(\x0b\x32\x1f.cozy.scheduler.ResolvedCompute\x12,\n\x06models\x18\x0b \x03(\x0b\x32\x1c.cozy.scheduler.ModelBinding\x12\x38\n\tsnapshots\x18\x0c \x03(\x0b\x32%.cozy.scheduler.RunJob.SnapshotsEntry\x12\x42\n\x10required_compile\x18\r \x01(\x0b\x32(.cozy.scheduler.RequiredCompileExecution\x12\x0c\n\x04lane\x18\x0e \x01(\t\x12\x30\n\x0cinput_assets\x18\x0f \x03(\x0b\x32\x1a.cozy.scheduler.InputAsset\x1aJ\n\x0eSnapshotsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\'\n\x05value\x18\x02 \x01(\x0b\x32\x18.cozy.scheduler.Snapshot:\x02\x38\x01\"w\n\nInputAsset\x12\x10\n\x08\x61sset_id\x18\x01 \x01(\t\x12\x12\n\nsource_ref\x18\x02 \x01(\t\x12\x0e\n\x06\x62lake3\x18\x03 \x01(\t\x12\x12\n\nsize_bytes\x18\x04 \x01(\x03\x12\x0c\n\x04kind\x18\x05 \x01(\t\x12\x11\n\tmime_type\x18\x06 \x01(\t\"\x82\x01\n\x18RequiredCompileExecution\x12\x1d\n\x15target_incarnation_id\x18\x01 \x01(\t\x12\x10\n\x08\x63\x65ll_ref\x18\x02 \x01(\t\x12\x1c\n\x14\x63\x65ll_snapshot_digest\x18\x03 \x01(\t\x12\x17\n\x0f\x63ontract_digest\x18\x04 \x01(\t\"Y\n\x0fResolvedCompute\x12\x13\n\x0b\x61\x63\x63\x65lerator\x18\x01 \x01(\t\x12\x11\n\tgpu_index\x18\x02 \x01(\x05J\x04\x08\x03\x10\x04J\x04\x08\x04\x10\x05R\tgpu_countR\x07vram_gb\"q\n\x0cModelBinding\x12\x0c\n\x04slot\x18\x01 \x01(\t\x12\x0b\n\x03ref\x18\x02 \x01(\t\x12*\n\x05loras\x18\x03 \x03(\x0b\x32\x1b.cozy.scheduler.LoraOverlay\x12\x1a\n\x12inference_defaults\x18\x04 \x01(\t\"F\n\x0bLoraOverlay\x12\x0b\n\x03ref\x18\x01 \x01(\t\x12\x0e\n\x06weight\x18\x02 \x01(\x01\x12\x1a\n\x12inference_defaults\x18\x03 \x01(\t\"G\n\x08Snapshot\x12\x0e\n\x06\x64igest\x18\x01 \x01(\t\x12+\n\x05\x66iles\x18\x02 \x03(\x0b\x32\x1c.cozy.scheduler.SnapshotFile\"M\n\x0cSnapshotFile\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x12\n\nsize_bytes\x18\x02 \x01(\x03\x12\x0e\n\x06\x62lake3\x18\x03 \x01(\t\x12\x0b\n\x03url\x18\x04 \x01(\t\"2\n\x0bJobAccepted\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\"\xce\x01\n\tJobResult\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\x12)\n\x06status\x18\x03 \x01(\x0e\x32\x19.cozy.scheduler.JobStatus\x12\x10\n\x06inline\x18\x04 \x01(\x0cH\x00\x12\x12\n\x08\x62lob_ref\x18\x05 \x01(\tH\x00\x12\x14\n\x0csafe_message\x18\x06 \x01(\t\x12+\n\x07metrics\x18\x07 \x01(\x0b\x32\x1a.cozy.scheduler.JobMetricsB\x08\n\x06output\"\xc2\x02\n\nJobMetrics\x12\x12\n\nruntime_ms\x18\x01 \x01(\x03\x12\x10\n\x08queue_ms\x18\x02 \x01(\x03\x12\x18\n\x10rss_at_end_bytes\x18\x03 \x01(\x03\x12\x17\n\x0fpeak_vram_bytes\x18\x04 \x01(\x03\x12\x1c\n\x14\x63oncurrency_at_start\x18\x05 \x01(\x05\x12\x1f\n\x17output_media_duration_s\x18\x06 \x01(\x01\x12\x14\n\x0cinput_tokens\x18\x07 \x01(\x03\x12\x1b\n\x13input_cached_tokens\x18\x08 \x01(\x03\x12\x15\n\routput_tokens\x18\t \x01(\x03\x12\x14\n\x0coutput_count\x18\n \x01(\x03\x12\x14\n\x0cslot_held_ms\x18\x0b \x01(\x03\x12\x18\n\x10\x66inalize_wall_ms\x18\x0c \x01(\x03\x12\x0c\n\x04lane\x18\r \x01(\t\"c\n\x0bJobProgress\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\x12\x0b\n\x03seq\x18\x03 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x05 \x01(\t\"0\n\tCancelJob\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\"\xa0\x01\n\x07ModelOp\x12\'\n\x02op\x18\x01 \x01(\x0e\x32\x1b.cozy.scheduler.ModelOpKind\x12\x0b\n\x03ref\x18\x02 \x01(\t\x12*\n\x08snapshot\x18\x03 \x01(\x0b\x32\x18.cozy.scheduler.Snapshot\x12\x14\n\x0coperation_id\x18\x04 \x01(\t\x12\x1d\n\x15target_incarnation_id\x18\x05 \x01(\t\"\x9b\x04\n\nModelEvent\x12\x0b\n\x03ref\x18\x01 \x01(\t\x12)\n\x05state\x18\x02 \x01(\x0e\x32\x1a.cozy.scheduler.ModelState\x12\x12\n\nvram_bytes\x18\x03 \x01(\x03\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x12\n\nbytes_done\x18\x05 \x01(\x03\x12\x13\n\x0b\x62ytes_total\x18\x06 \x01(\x03\x12\x13\n\x0b\x64uration_ms\x18\x07 \x01(\x03\x12\x12\n\ncache_hits\x18\x08 \x01(\x03\x12\x14\n\x0c\x63\x61\x63he_misses\x18\t \x01(\x03\x12\x10\n\x08warmup_s\x18\n \x01(\x01\x12\x1f\n\x17host_ram_required_bytes\x18\x0b \x01(\x03\x12\'\n\x1fhost_ram_available_before_bytes\x18\x0c \x01(\x03\x12&\n\x1ehost_ram_available_after_bytes\x18\r \x01(\x03\x12\x1d\n\x15host_ram_evicted_refs\x18\x0e \x03(\t\x12$\n\x1chost_ram_capacity_generation\x18\x0f \x01(\x04\x12\x17\n\x0fsnapshot_digest\x18\x10 \x01(\t\x12\x1c\n\x14residency_generation\x18\x11 \x01(\x04\x12\x14\n\x0coperation_id\x18\x12 \x01(\t\x12\x1d\n\x15target_incarnation_id\x18\x13 \x01(\t\x12\x15\n\rnetwork_bytes\x18\x14 \x01(\x03\"\xc6\x01\n\x0e\x41\x63tivityUpdate\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\r\n\x05phase\x18\x02 \x01(\t\x12\x0c\n\x04step\x18\x03 \x01(\x03\x12\x13\n\x0btotal_steps\x18\x04 \x01(\x03\x12\x0b\n\x03seq\x18\x05 \x01(\x04\x12,\n\x05state\x18\x06 \x01(\x0e\x32\x1d.cozy.scheduler.ActivityState\x12\r\n\x05\x65rror\x18\x07 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x08 \x01(\t\x12\x1a\n\x12updated_at_unix_ms\x18\t \x01(\x03\"\xaa\x01\n\rFnUnavailable\x12\x15\n\rfunction_name\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x03 \x01(\t\x12\x35\n\x04\x61xes\x18\x04 \x03(\x0b\x32\'.cozy.scheduler.FnUnavailable.AxesEntry\x1a+\n\tAxesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8d\x01\n\nFnDegraded\x12\x15\n\rfunction_name\x18\x01 \x01(\t\x12\x0e\n\x06wanted\x18\x02 \x01(\t\x12\x0b\n\x03ran\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\t\x12\x1e\n\x16\x65st_latency_multiplier\x18\x05 \x01(\x01\x12\x1b\n\x13recommended_vram_gb\x18\x06 \x01(\x01\"\x1c\n\x05\x44rain\x12\x13\n\x0b\x64\x65\x61\x64line_ms\x18\x01 \x01(\x03\"6\n\x0cTokenRefresh\x12\r\n\x05token\x18\x01 \x01(\t\x12\x17\n\x0f\x65xpires_at_unix\x18\x02 \x01(\x03*Q\n\x0fProtocolVersion\x12 \n\x1cPROTOCOL_VERSION_UNSPECIFIED\x10\x00\x12\x1c\n\x18PROTOCOL_VERSION_CURRENT\x10\x04*y\n\rResidencyTier\x12\x1e\n\x1aRESIDENCY_TIER_UNSPECIFIED\x10\x00\x12\x17\n\x13RESIDENCY_TIER_DISK\x10\x01\x12\x16\n\x12RESIDENCY_TIER_RAM\x10\x02\x12\x17\n\x13RESIDENCY_TIER_VRAM\x10\x03*v\n\x0bStorageTier\x12\x1c\n\x18STORAGE_TIER_UNSPECIFIED\x10\x00\x12\x1a\n\x16STORAGE_TIER_CONTAINER\x10\x01\x12\x17\n\x13STORAGE_TIER_VOLUME\x10\x02\x12\x14\n\x10STORAGE_TIER_NFS\x10\x03*\xd8\x01\n\x0bWorkerPhase\x12\x1c\n\x18WORKER_PHASE_UNSPECIFIED\x10\x00\x12\x18\n\x14WORKER_PHASE_BOOTING\x10\x01\x12#\n\x1fWORKER_PHASE_DOWNLOADING_MODELS\x10\x02\x12\"\n\x1eWORKER_PHASE_LOADING_PIPELINES\x10\x03\x12\x18\n\x14WORKER_PHASE_WARMING\x10\x04\x12\x16\n\x12WORKER_PHASE_READY\x10\x05\x12\x16\n\x12WORKER_PHASE_ERROR\x10\x06*V\n\nOutputMode\x12\x1b\n\x17OUTPUT_MODE_UNSPECIFIED\x10\x00\x12\x13\n\x0fOUTPUT_MODE_URL\x10\x01\x12\x16\n\x12OUTPUT_MODE_INLINE\x10\x02*\x9b\x01\n\tJobStatus\x12\x1a\n\x16JOB_STATUS_UNSPECIFIED\x10\x00\x12\x11\n\rJOB_STATUS_OK\x10\x01\x12\x16\n\x12JOB_STATUS_INVALID\x10\x02\x12\x18\n\x14JOB_STATUS_RETRYABLE\x10\x03\x12\x14\n\x10JOB_STATUS_FATAL\x10\x04\x12\x17\n\x13JOB_STATUS_CANCELED\x10\x05*\xa7\x01\n\x0bModelOpKind\x12\x1d\n\x19MODEL_OP_KIND_UNSPECIFIED\x10\x00\x12%\n!MODEL_OP_KIND_ADOPT_COMPILE_CACHE\x10\x04\"\x04\x08\x01\x10\x01\"\x04\x08\x02\x10\x02\"\x04\x08\x03\x10\x03*\x16MODEL_OP_KIND_DOWNLOAD*\x12MODEL_OP_KIND_LOAD*\x14MODEL_OP_KIND_UNLOAD*\x82\x02\n\nModelState\x12\x1b\n\x17MODEL_STATE_UNSPECIFIED\x10\x00\x12\x1b\n\x17MODEL_STATE_DOWNLOADING\x10\x01\x12\x17\n\x13MODEL_STATE_ON_DISK\x10\x02\x12\x16\n\x12MODEL_STATE_IN_RAM\x10\x03\x12\x17\n\x13MODEL_STATE_IN_VRAM\x10\x04\x12\x17\n\x13MODEL_STATE_EVICTED\x10\x05\x12\x16\n\x12MODEL_STATE_FAILED\x10\x06\x12\x17\n\x13MODEL_STATE_ADOPTED\x10\x07\x12&\n\"MODEL_STATE_HOST_CAPACITY_PROGRESS\x10\x08*\x84\x01\n\rActivityState\x12\x1e\n\x1a\x41\x43TIVITY_STATE_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x41\x43TIVITY_STATE_RUNNING\x10\x01\x12\x1c\n\x18\x41\x43TIVITY_STATE_COMPLETED\x10\x02\x12\x19\n\x15\x41\x43TIVITY_STATE_FAILED\x10\x03\x32\x61\n\x0fWorkerScheduler\x12N\n\x07\x43onnect\x12\x1d.cozy.scheduler.WorkerMessage\x1a .cozy.scheduler.SchedulerMessage(\x01\x30\x01\x42\x61Z_github.com/cozy-creator/tensorhub/internal/orchestrator/grpc/pb/workerscheduler;workerschedulerb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16worker_scheduler.proto\x12\x0e\x63ozy.scheduler\"\xe6\x03\n\rWorkerMessage\x12&\n\x05hello\x18\x01 \x01(\x0b\x32\x15.cozy.scheduler.HelloH\x00\x12\x31\n\x0bstate_delta\x18\x02 \x01(\x0b\x32\x1a.cozy.scheduler.StateDeltaH\x00\x12\x33\n\x0cjob_accepted\x18\x03 \x01(\x0b\x32\x1b.cozy.scheduler.JobAcceptedH\x00\x12/\n\njob_result\x18\x04 \x01(\x0b\x32\x19.cozy.scheduler.JobResultH\x00\x12\x33\n\x0cjob_progress\x18\x05 \x01(\x0b\x32\x1b.cozy.scheduler.JobProgressH\x00\x12\x31\n\x0bmodel_event\x18\x06 \x01(\x0b\x32\x1a.cozy.scheduler.ModelEventH\x00\x12\x37\n\x0e\x66n_unavailable\x18\x07 \x01(\x0b\x32\x1d.cozy.scheduler.FnUnavailableH\x00\x12\x31\n\x0b\x66n_degraded\x18\x08 \x01(\x0b\x32\x1a.cozy.scheduler.FnDegradedH\x00\x12\x39\n\x0f\x61\x63tivity_update\x18\t \x01(\x0b\x32\x1e.cozy.scheduler.ActivityUpdateH\x00\x42\x05\n\x03msg\"\xb0\x02\n\x10SchedulerMessage\x12-\n\thello_ack\x18\x01 \x01(\x0b\x32\x18.cozy.scheduler.HelloAckH\x00\x12)\n\x07run_job\x18\x02 \x01(\x0b\x32\x16.cozy.scheduler.RunJobH\x00\x12/\n\ncancel_job\x18\x03 \x01(\x0b\x32\x19.cozy.scheduler.CancelJobH\x00\x12+\n\x08model_op\x18\x04 \x01(\x0b\x32\x17.cozy.scheduler.ModelOpH\x00\x12&\n\x05\x64rain\x18\x05 \x01(\x0b\x32\x15.cozy.scheduler.DrainH\x00\x12\x35\n\rtoken_refresh\x18\x06 \x01(\x0b\x32\x1c.cozy.scheduler.TokenRefreshH\x00\x42\x05\n\x03msg\"\xc7\x02\n\x05Hello\x12\x39\n\x10protocol_version\x18\x01 \x01(\x0e\x32\x1f.cozy.scheduler.ProtocolVersion\x12\x11\n\tworker_id\x18\x02 \x01(\t\x12\x12\n\nrelease_id\x18\x03 \x01(\t\x12\x32\n\tresources\x18\x04 \x01(\x0b\x32\x1f.cozy.scheduler.WorkerResources\x12)\n\x05state\x18\x05 \x01(\x0b\x32\x1a.cozy.scheduler.StateDelta\x12.\n\x06models\x18\x06 \x03(\x0b\x32\x1e.cozy.scheduler.ModelResidency\x12.\n\tin_flight\x18\x07 \x03(\x0b\x32\x1b.cozy.scheduler.InFlightJob\x12\x1d\n\x15heartbeat_interval_ms\x18\x08 \x01(\x04\"\x9b\x02\n\x0fWorkerResources\x12\x11\n\tgpu_count\x18\x01 \x01(\x05\x12\x18\n\x10vram_total_bytes\x18\x02 \x01(\x03\x12\x10\n\x08gpu_name\x18\x03 \x01(\t\x12\x0e\n\x06gpu_sm\x18\x04 \x01(\t\x12\x16\n\x0einstalled_libs\x18\x05 \x03(\t\x12\x14\n\x0cimage_digest\x18\x06 \x01(\t\x12\x12\n\ngit_commit\x18\x07 \x01(\t\x12\x13\n\x0binstance_id\x18\x08 \x01(\t\x12/\n\x0bhost_canary\x18\t \x01(\x0b\x32\x1a.cozy.scheduler.HostCanary\x12\x15\n\rtorch_version\x18\n \x01(\t\x12\x1a\n\x12gen_worker_version\x18\x0b \x01(\t\"\xc9\x01\n\nHostCanary\x12\x13\n\x0bmemcpy_gbps\x18\x01 \x01(\x01\x12\x10\n\x08h2d_gbps\x18\x02 \x01(\x01\x12\x10\n\x08\x64\x32h_gbps\x18\x03 \x01(\x01\x12\x17\n\x0fpinned_alloc_ok\x18\x04 \x01(\x08\x12\x17\n\x0f\x63pu_single_mbps\x18\x05 \x01(\x01\x12\x16\n\x0e\x63pu_multi_mbps\x18\x06 \x01(\x01\x12\r\n\x05vcpus\x18\x07 \x01(\x05\x12\x14\n\x0cram_total_gb\x18\x08 \x01(\x01\x12\x13\n\x0b\x64uration_ms\x18\t \x01(\x03\"\x95\x01\n\x0eModelResidency\x12\x0b\n\x03ref\x18\x01 \x01(\t\x12+\n\x04tier\x18\x02 \x01(\x0e\x32\x1d.cozy.scheduler.ResidencyTier\x12\x12\n\nvram_bytes\x18\x03 \x01(\x03\x12\x17\n\x0fsnapshot_digest\x18\x04 \x01(\t\x12\x1c\n\x14residency_generation\x18\x05 \x01(\x04\"2\n\x0bInFlightJob\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\"\xdd\x01\n\x08HelloAck\x12\x39\n\x10protocol_version\x18\x01 \x01(\x0e\x32\x1f.cozy.scheduler.ProtocolVersion\x12\x15\n\rfile_base_url\x18\x02 \x01(\t\x12\x0c\n\x04keep\x18\x03 \x03(\t\x12\x34\n\x0bresolutions\x18\x04 \x03(\x0b\x32\x1f.cozy.scheduler.ModelResolution\x12;\n\x11\x64\x65sired_residency\x18\x05 \x01(\x0b\x32 .cozy.scheduler.DesiredResidency\"\xf7\x01\n\x10\x44\x65siredResidency\x12\x12\n\ngeneration\x18\x01 \x01(\x04\x12\x11\n\tdisk_refs\x18\x02 \x03(\t\x12,\n\x03hot\x18\x03 \x03(\x0b\x32\x1f.cozy.scheduler.DesiredInstance\x12\x42\n\tsnapshots\x18\x04 \x03(\x0b\x32/.cozy.scheduler.DesiredResidency.SnapshotsEntry\x1aJ\n\x0eSnapshotsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\'\n\x05value\x18\x02 \x01(\x0b\x32\x18.cozy.scheduler.Snapshot:\x02\x38\x01\"V\n\x0f\x44\x65siredInstance\x12\x15\n\rfunction_name\x18\x01 \x01(\t\x12,\n\x06models\x18\x02 \x03(\x0b\x32\x1c.cozy.scheduler.ModelBinding\"P\n\x0fModelResolution\x12\x0b\n\x03ref\x18\x01 \x01(\t\x12\x14\n\x0cresolved_ref\x18\x02 \x01(\t\x12\x0c\n\x04\x63\x61st\x18\x03 \x01(\t\x12\x0c\n\x04lane\x18\x04 \x01(\t\"\xe8\x02\n\nStateDelta\x12*\n\x05phase\x18\x01 \x01(\x0e\x32\x1b.cozy.scheduler.WorkerPhase\x12\x1b\n\x13\x61vailable_functions\x18\x02 \x03(\t\x12\x19\n\x11loading_functions\x18\x03 \x03(\t\x12\x17\n\x0f\x66ree_vram_bytes\x18\x04 \x01(\x03\x12\x17\n\x0f\x66inalizing_jobs\x18\x05 \x01(\x05\x12%\n\x1dobserved_residency_generation\x18\x06 \x01(\x04\x12\x36\n\x0f\x63ompile_targets\x18\x07 \x03(\x0b\x32\x1d.cozy.scheduler.CompileTarget\x12\x30\n\x0c\x63\x65ll_lookups\x18\x08 \x03(\x0b\x32\x1a.cozy.scheduler.CellLookup\x12\x33\n\ndisk_usage\x18\t \x01(\x0b\x32\x1f.cozy.scheduler.DiskUsageReport\"\xa9\x01\n\x10StorageTierUsage\x12)\n\x04tier\x18\x01 \x01(\x0e\x32\x1b.cozy.scheduler.StorageTier\x12\x12\n\nmount_path\x18\x02 \x01(\t\x12\x13\n\x0btotal_bytes\x18\x03 \x01(\x03\x12\x12\n\nfree_bytes\x18\x04 \x01(\x03\x12\x12\n\nused_bytes\x18\x05 \x01(\x03\x12\x19\n\x11reclaimable_bytes\x18\x06 \x01(\x03\"_\n\x0f\x44iskUsageReport\x12/\n\x05tiers\x18\x01 \x03(\x0b\x32 .cozy.scheduler.StorageTierUsage\x12\x1b\n\x13\x63\x61pacity_generation\x18\x02 \x01(\x04\".\n\nCellLookup\x12\x0e\n\x06\x66\x61mily\x18\x01 \x01(\t\x12\x10\n\x08\x63\x65ll_key\x18\x02 \x01(\t\"\xc6\x03\n\rCompileTarget\x12\x16\n\x0eincarnation_id\x18\x01 \x01(\t\x12\x0e\n\x06\x66\x61mily\x18\x02 \x01(\t\x12\x1c\n\x14pipeline_weight_lane\x18\x03 \x01(\t\x12\x13\n\x0blora_bucket\x18\x04 \x01(\x05\x12\x17\n\x0f\x63ontract_digest\x18\x05 \x01(\t\x12\x1a\n\x12\x61\x63tive_compile_ref\x18\x06 \x01(\t\x12&\n\x1e\x61\x63tive_compile_snapshot_digest\x18\x07 \x01(\t\x12\x16\n\x0e\x66unction_names\x18\x08 \x03(\t\x12<\n\x0emodel_bindings\x18\t \x03(\x0b\x32$.cozy.scheduler.CompileTargetBinding\x12\x1a\n\x12requested_cell_key\x18\n \x01(\t\x12Q\n\x13requested_cell_axes\x18\x0b \x03(\x0b\x32\x34.cozy.scheduler.CompileTarget.RequestedCellAxesEntry\x1a\x38\n\x16RequestedCellAxesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"J\n\x14\x43ompileTargetBinding\x12\x0c\n\x04slot\x18\x01 \x01(\t\x12\x0b\n\x03ref\x18\x02 \x01(\t\x12\x17\n\x0fsnapshot_digest\x18\x03 \x01(\t\"\xc8\x04\n\x06RunJob\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\x12\x15\n\rfunction_name\x18\x03 \x01(\t\x12\x15\n\rinput_payload\x18\x04 \x01(\x0c\x12\x12\n\ntimeout_ms\x18\x05 \x01(\x03\x12\x0e\n\x06tenant\x18\x06 \x01(\t\x12\x12\n\ninvoker_id\x18\x07 \x01(\t\x12\x18\n\x10\x63\x61pability_token\x18\x08 \x01(\t\x12/\n\x0boutput_mode\x18\t \x01(\x0e\x32\x1a.cozy.scheduler.OutputMode\x12\x30\n\x07\x63ompute\x18\n \x01(\x0b\x32\x1f.cozy.scheduler.ResolvedCompute\x12,\n\x06models\x18\x0b \x03(\x0b\x32\x1c.cozy.scheduler.ModelBinding\x12\x38\n\tsnapshots\x18\x0c \x03(\x0b\x32%.cozy.scheduler.RunJob.SnapshotsEntry\x12\x42\n\x10required_compile\x18\r \x01(\x0b\x32(.cozy.scheduler.RequiredCompileExecution\x12\x0c\n\x04lane\x18\x0e \x01(\t\x12\x30\n\x0cinput_assets\x18\x0f \x03(\x0b\x32\x1a.cozy.scheduler.InputAsset\x1aJ\n\x0eSnapshotsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\'\n\x05value\x18\x02 \x01(\x0b\x32\x18.cozy.scheduler.Snapshot:\x02\x38\x01\"w\n\nInputAsset\x12\x10\n\x08\x61sset_id\x18\x01 \x01(\t\x12\x12\n\nsource_ref\x18\x02 \x01(\t\x12\x0e\n\x06\x62lake3\x18\x03 \x01(\t\x12\x12\n\nsize_bytes\x18\x04 \x01(\x03\x12\x0c\n\x04kind\x18\x05 \x01(\t\x12\x11\n\tmime_type\x18\x06 \x01(\t\"\x82\x01\n\x18RequiredCompileExecution\x12\x1d\n\x15target_incarnation_id\x18\x01 \x01(\t\x12\x10\n\x08\x63\x65ll_ref\x18\x02 \x01(\t\x12\x1c\n\x14\x63\x65ll_snapshot_digest\x18\x03 \x01(\t\x12\x17\n\x0f\x63ontract_digest\x18\x04 \x01(\t\"Y\n\x0fResolvedCompute\x12\x13\n\x0b\x61\x63\x63\x65lerator\x18\x01 \x01(\t\x12\x11\n\tgpu_index\x18\x02 \x01(\x05J\x04\x08\x03\x10\x04J\x04\x08\x04\x10\x05R\tgpu_countR\x07vram_gb\"\xe6\x01\n\x0cModelBinding\x12\x0c\n\x04slot\x18\x01 \x01(\t\x12\x0b\n\x03ref\x18\x02 \x01(\t\x12*\n\x05loras\x18\x03 \x03(\x0b\x32\x1b.cozy.scheduler.LoraOverlay\x12\x1a\n\x12inference_defaults\x18\x04 \x01(\t\x12@\n\ncomponents\x18\x05 \x03(\x0b\x32,.cozy.scheduler.ModelBinding.ComponentsEntry\x1a\x31\n\x0f\x43omponentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"F\n\x0bLoraOverlay\x12\x0b\n\x03ref\x18\x01 \x01(\t\x12\x0e\n\x06weight\x18\x02 \x01(\x01\x12\x1a\n\x12inference_defaults\x18\x03 \x01(\t\"G\n\x08Snapshot\x12\x0e\n\x06\x64igest\x18\x01 \x01(\t\x12+\n\x05\x66iles\x18\x02 \x03(\x0b\x32\x1c.cozy.scheduler.SnapshotFile\"M\n\x0cSnapshotFile\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x12\n\nsize_bytes\x18\x02 \x01(\x03\x12\x0e\n\x06\x62lake3\x18\x03 \x01(\t\x12\x0b\n\x03url\x18\x04 \x01(\t\"2\n\x0bJobAccepted\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\"\xce\x01\n\tJobResult\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\x12)\n\x06status\x18\x03 \x01(\x0e\x32\x19.cozy.scheduler.JobStatus\x12\x10\n\x06inline\x18\x04 \x01(\x0cH\x00\x12\x12\n\x08\x62lob_ref\x18\x05 \x01(\tH\x00\x12\x14\n\x0csafe_message\x18\x06 \x01(\t\x12+\n\x07metrics\x18\x07 \x01(\x0b\x32\x1a.cozy.scheduler.JobMetricsB\x08\n\x06output\"\xc2\x02\n\nJobMetrics\x12\x12\n\nruntime_ms\x18\x01 \x01(\x03\x12\x10\n\x08queue_ms\x18\x02 \x01(\x03\x12\x18\n\x10rss_at_end_bytes\x18\x03 \x01(\x03\x12\x17\n\x0fpeak_vram_bytes\x18\x04 \x01(\x03\x12\x1c\n\x14\x63oncurrency_at_start\x18\x05 \x01(\x05\x12\x1f\n\x17output_media_duration_s\x18\x06 \x01(\x01\x12\x14\n\x0cinput_tokens\x18\x07 \x01(\x03\x12\x1b\n\x13input_cached_tokens\x18\x08 \x01(\x03\x12\x15\n\routput_tokens\x18\t \x01(\x03\x12\x14\n\x0coutput_count\x18\n \x01(\x03\x12\x14\n\x0cslot_held_ms\x18\x0b \x01(\x03\x12\x18\n\x10\x66inalize_wall_ms\x18\x0c \x01(\x03\x12\x0c\n\x04lane\x18\r \x01(\t\"c\n\x0bJobProgress\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\x12\x0b\n\x03seq\x18\x03 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x05 \x01(\t\"0\n\tCancelJob\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0f\n\x07\x61ttempt\x18\x02 \x01(\x03\"\xa0\x01\n\x07ModelOp\x12\'\n\x02op\x18\x01 \x01(\x0e\x32\x1b.cozy.scheduler.ModelOpKind\x12\x0b\n\x03ref\x18\x02 \x01(\t\x12*\n\x08snapshot\x18\x03 \x01(\x0b\x32\x18.cozy.scheduler.Snapshot\x12\x14\n\x0coperation_id\x18\x04 \x01(\t\x12\x1d\n\x15target_incarnation_id\x18\x05 \x01(\t\"\x9b\x04\n\nModelEvent\x12\x0b\n\x03ref\x18\x01 \x01(\t\x12)\n\x05state\x18\x02 \x01(\x0e\x32\x1a.cozy.scheduler.ModelState\x12\x12\n\nvram_bytes\x18\x03 \x01(\x03\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12\x12\n\nbytes_done\x18\x05 \x01(\x03\x12\x13\n\x0b\x62ytes_total\x18\x06 \x01(\x03\x12\x13\n\x0b\x64uration_ms\x18\x07 \x01(\x03\x12\x12\n\ncache_hits\x18\x08 \x01(\x03\x12\x14\n\x0c\x63\x61\x63he_misses\x18\t \x01(\x03\x12\x10\n\x08warmup_s\x18\n \x01(\x01\x12\x1f\n\x17host_ram_required_bytes\x18\x0b \x01(\x03\x12\'\n\x1fhost_ram_available_before_bytes\x18\x0c \x01(\x03\x12&\n\x1ehost_ram_available_after_bytes\x18\r \x01(\x03\x12\x1d\n\x15host_ram_evicted_refs\x18\x0e \x03(\t\x12$\n\x1chost_ram_capacity_generation\x18\x0f \x01(\x04\x12\x17\n\x0fsnapshot_digest\x18\x10 \x01(\t\x12\x1c\n\x14residency_generation\x18\x11 \x01(\x04\x12\x14\n\x0coperation_id\x18\x12 \x01(\t\x12\x1d\n\x15target_incarnation_id\x18\x13 \x01(\t\x12\x15\n\rnetwork_bytes\x18\x14 \x01(\x03\"\xc6\x01\n\x0e\x41\x63tivityUpdate\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\r\n\x05phase\x18\x02 \x01(\t\x12\x0c\n\x04step\x18\x03 \x01(\x03\x12\x13\n\x0btotal_steps\x18\x04 \x01(\x03\x12\x0b\n\x03seq\x18\x05 \x01(\x04\x12,\n\x05state\x18\x06 \x01(\x0e\x32\x1d.cozy.scheduler.ActivityState\x12\r\n\x05\x65rror\x18\x07 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x08 \x01(\t\x12\x1a\n\x12updated_at_unix_ms\x18\t \x01(\x03\"\xaa\x01\n\rFnUnavailable\x12\x15\n\rfunction_name\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x03 \x01(\t\x12\x35\n\x04\x61xes\x18\x04 \x03(\x0b\x32\'.cozy.scheduler.FnUnavailable.AxesEntry\x1a+\n\tAxesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8d\x01\n\nFnDegraded\x12\x15\n\rfunction_name\x18\x01 \x01(\t\x12\x0e\n\x06wanted\x18\x02 \x01(\t\x12\x0b\n\x03ran\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\t\x12\x1e\n\x16\x65st_latency_multiplier\x18\x05 \x01(\x01\x12\x1b\n\x13recommended_vram_gb\x18\x06 \x01(\x01\"\x1c\n\x05\x44rain\x12\x13\n\x0b\x64\x65\x61\x64line_ms\x18\x01 \x01(\x03\"6\n\x0cTokenRefresh\x12\r\n\x05token\x18\x01 \x01(\t\x12\x17\n\x0f\x65xpires_at_unix\x18\x02 \x01(\x03*Q\n\x0fProtocolVersion\x12 \n\x1cPROTOCOL_VERSION_UNSPECIFIED\x10\x00\x12\x1c\n\x18PROTOCOL_VERSION_CURRENT\x10\x04*y\n\rResidencyTier\x12\x1e\n\x1aRESIDENCY_TIER_UNSPECIFIED\x10\x00\x12\x17\n\x13RESIDENCY_TIER_DISK\x10\x01\x12\x16\n\x12RESIDENCY_TIER_RAM\x10\x02\x12\x17\n\x13RESIDENCY_TIER_VRAM\x10\x03*v\n\x0bStorageTier\x12\x1c\n\x18STORAGE_TIER_UNSPECIFIED\x10\x00\x12\x1a\n\x16STORAGE_TIER_CONTAINER\x10\x01\x12\x17\n\x13STORAGE_TIER_VOLUME\x10\x02\x12\x14\n\x10STORAGE_TIER_NFS\x10\x03*\xd8\x01\n\x0bWorkerPhase\x12\x1c\n\x18WORKER_PHASE_UNSPECIFIED\x10\x00\x12\x18\n\x14WORKER_PHASE_BOOTING\x10\x01\x12#\n\x1fWORKER_PHASE_DOWNLOADING_MODELS\x10\x02\x12\"\n\x1eWORKER_PHASE_LOADING_PIPELINES\x10\x03\x12\x18\n\x14WORKER_PHASE_WARMING\x10\x04\x12\x16\n\x12WORKER_PHASE_READY\x10\x05\x12\x16\n\x12WORKER_PHASE_ERROR\x10\x06*V\n\nOutputMode\x12\x1b\n\x17OUTPUT_MODE_UNSPECIFIED\x10\x00\x12\x13\n\x0fOUTPUT_MODE_URL\x10\x01\x12\x16\n\x12OUTPUT_MODE_INLINE\x10\x02*\x9b\x01\n\tJobStatus\x12\x1a\n\x16JOB_STATUS_UNSPECIFIED\x10\x00\x12\x11\n\rJOB_STATUS_OK\x10\x01\x12\x16\n\x12JOB_STATUS_INVALID\x10\x02\x12\x18\n\x14JOB_STATUS_RETRYABLE\x10\x03\x12\x14\n\x10JOB_STATUS_FATAL\x10\x04\x12\x17\n\x13JOB_STATUS_CANCELED\x10\x05*\xa7\x01\n\x0bModelOpKind\x12\x1d\n\x19MODEL_OP_KIND_UNSPECIFIED\x10\x00\x12%\n!MODEL_OP_KIND_ADOPT_COMPILE_CACHE\x10\x04\"\x04\x08\x01\x10\x01\"\x04\x08\x02\x10\x02\"\x04\x08\x03\x10\x03*\x16MODEL_OP_KIND_DOWNLOAD*\x12MODEL_OP_KIND_LOAD*\x14MODEL_OP_KIND_UNLOAD*\x82\x02\n\nModelState\x12\x1b\n\x17MODEL_STATE_UNSPECIFIED\x10\x00\x12\x1b\n\x17MODEL_STATE_DOWNLOADING\x10\x01\x12\x17\n\x13MODEL_STATE_ON_DISK\x10\x02\x12\x16\n\x12MODEL_STATE_IN_RAM\x10\x03\x12\x17\n\x13MODEL_STATE_IN_VRAM\x10\x04\x12\x17\n\x13MODEL_STATE_EVICTED\x10\x05\x12\x16\n\x12MODEL_STATE_FAILED\x10\x06\x12\x17\n\x13MODEL_STATE_ADOPTED\x10\x07\x12&\n\"MODEL_STATE_HOST_CAPACITY_PROGRESS\x10\x08*\x84\x01\n\rActivityState\x12\x1e\n\x1a\x41\x43TIVITY_STATE_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x41\x43TIVITY_STATE_RUNNING\x10\x01\x12\x1c\n\x18\x41\x43TIVITY_STATE_COMPLETED\x10\x02\x12\x19\n\x15\x41\x43TIVITY_STATE_FAILED\x10\x03\x32\x61\n\x0fWorkerScheduler\x12N\n\x07\x43onnect\x12\x1d.cozy.scheduler.WorkerMessage\x1a .cozy.scheduler.SchedulerMessage(\x01\x30\x01\x42\x61Z_github.com/cozy-creator/tensorhub/internal/orchestrator/grpc/pb/workerscheduler;workerschedulerb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -38,26 +38,28 @@ _globals['_COMPILETARGET_REQUESTEDCELLAXESENTRY']._serialized_options = b'8\001' _globals['_RUNJOB_SNAPSHOTSENTRY']._loaded_options = None _globals['_RUNJOB_SNAPSHOTSENTRY']._serialized_options = b'8\001' + _globals['_MODELBINDING_COMPONENTSENTRY']._loaded_options = None + _globals['_MODELBINDING_COMPONENTSENTRY']._serialized_options = b'8\001' _globals['_FNUNAVAILABLE_AXESENTRY']._loaded_options = None _globals['_FNUNAVAILABLE_AXESENTRY']._serialized_options = b'8\001' - _globals['_PROTOCOLVERSION']._serialized_start=7036 - _globals['_PROTOCOLVERSION']._serialized_end=7117 - _globals['_RESIDENCYTIER']._serialized_start=7119 - _globals['_RESIDENCYTIER']._serialized_end=7240 - _globals['_STORAGETIER']._serialized_start=7242 - _globals['_STORAGETIER']._serialized_end=7360 - _globals['_WORKERPHASE']._serialized_start=7363 - _globals['_WORKERPHASE']._serialized_end=7579 - _globals['_OUTPUTMODE']._serialized_start=7581 - _globals['_OUTPUTMODE']._serialized_end=7667 - _globals['_JOBSTATUS']._serialized_start=7670 - _globals['_JOBSTATUS']._serialized_end=7825 - _globals['_MODELOPKIND']._serialized_start=7828 - _globals['_MODELOPKIND']._serialized_end=7995 - _globals['_MODELSTATE']._serialized_start=7998 - _globals['_MODELSTATE']._serialized_end=8256 - _globals['_ACTIVITYSTATE']._serialized_start=8259 - _globals['_ACTIVITYSTATE']._serialized_end=8391 + _globals['_PROTOCOLVERSION']._serialized_start=7154 + _globals['_PROTOCOLVERSION']._serialized_end=7235 + _globals['_RESIDENCYTIER']._serialized_start=7237 + _globals['_RESIDENCYTIER']._serialized_end=7358 + _globals['_STORAGETIER']._serialized_start=7360 + _globals['_STORAGETIER']._serialized_end=7478 + _globals['_WORKERPHASE']._serialized_start=7481 + _globals['_WORKERPHASE']._serialized_end=7697 + _globals['_OUTPUTMODE']._serialized_start=7699 + _globals['_OUTPUTMODE']._serialized_end=7785 + _globals['_JOBSTATUS']._serialized_start=7788 + _globals['_JOBSTATUS']._serialized_end=7943 + _globals['_MODELOPKIND']._serialized_start=7946 + _globals['_MODELOPKIND']._serialized_end=8113 + _globals['_MODELSTATE']._serialized_start=8116 + _globals['_MODELSTATE']._serialized_end=8374 + _globals['_ACTIVITYSTATE']._serialized_start=8377 + _globals['_ACTIVITYSTATE']._serialized_end=8509 _globals['_WORKERMESSAGE']._serialized_start=43 _globals['_WORKERMESSAGE']._serialized_end=529 _globals['_SCHEDULERMESSAGE']._serialized_start=532 @@ -106,40 +108,42 @@ _globals['_REQUIREDCOMPILEEXECUTION']._serialized_end=4558 _globals['_RESOLVEDCOMPUTE']._serialized_start=4560 _globals['_RESOLVEDCOMPUTE']._serialized_end=4649 - _globals['_MODELBINDING']._serialized_start=4651 - _globals['_MODELBINDING']._serialized_end=4764 - _globals['_LORAOVERLAY']._serialized_start=4766 - _globals['_LORAOVERLAY']._serialized_end=4836 - _globals['_SNAPSHOT']._serialized_start=4838 - _globals['_SNAPSHOT']._serialized_end=4909 - _globals['_SNAPSHOTFILE']._serialized_start=4911 - _globals['_SNAPSHOTFILE']._serialized_end=4988 - _globals['_JOBACCEPTED']._serialized_start=4990 - _globals['_JOBACCEPTED']._serialized_end=5040 - _globals['_JOBRESULT']._serialized_start=5043 - _globals['_JOBRESULT']._serialized_end=5249 - _globals['_JOBMETRICS']._serialized_start=5252 - _globals['_JOBMETRICS']._serialized_end=5574 - _globals['_JOBPROGRESS']._serialized_start=5576 - _globals['_JOBPROGRESS']._serialized_end=5675 - _globals['_CANCELJOB']._serialized_start=5677 - _globals['_CANCELJOB']._serialized_end=5725 - _globals['_MODELOP']._serialized_start=5728 - _globals['_MODELOP']._serialized_end=5888 - _globals['_MODELEVENT']._serialized_start=5891 - _globals['_MODELEVENT']._serialized_end=6430 - _globals['_ACTIVITYUPDATE']._serialized_start=6433 - _globals['_ACTIVITYUPDATE']._serialized_end=6631 - _globals['_FNUNAVAILABLE']._serialized_start=6634 - _globals['_FNUNAVAILABLE']._serialized_end=6804 - _globals['_FNUNAVAILABLE_AXESENTRY']._serialized_start=6761 - _globals['_FNUNAVAILABLE_AXESENTRY']._serialized_end=6804 - _globals['_FNDEGRADED']._serialized_start=6807 - _globals['_FNDEGRADED']._serialized_end=6948 - _globals['_DRAIN']._serialized_start=6950 - _globals['_DRAIN']._serialized_end=6978 - _globals['_TOKENREFRESH']._serialized_start=6980 - _globals['_TOKENREFRESH']._serialized_end=7034 - _globals['_WORKERSCHEDULER']._serialized_start=8393 - _globals['_WORKERSCHEDULER']._serialized_end=8490 + _globals['_MODELBINDING']._serialized_start=4652 + _globals['_MODELBINDING']._serialized_end=4882 + _globals['_MODELBINDING_COMPONENTSENTRY']._serialized_start=4833 + _globals['_MODELBINDING_COMPONENTSENTRY']._serialized_end=4882 + _globals['_LORAOVERLAY']._serialized_start=4884 + _globals['_LORAOVERLAY']._serialized_end=4954 + _globals['_SNAPSHOT']._serialized_start=4956 + _globals['_SNAPSHOT']._serialized_end=5027 + _globals['_SNAPSHOTFILE']._serialized_start=5029 + _globals['_SNAPSHOTFILE']._serialized_end=5106 + _globals['_JOBACCEPTED']._serialized_start=5108 + _globals['_JOBACCEPTED']._serialized_end=5158 + _globals['_JOBRESULT']._serialized_start=5161 + _globals['_JOBRESULT']._serialized_end=5367 + _globals['_JOBMETRICS']._serialized_start=5370 + _globals['_JOBMETRICS']._serialized_end=5692 + _globals['_JOBPROGRESS']._serialized_start=5694 + _globals['_JOBPROGRESS']._serialized_end=5793 + _globals['_CANCELJOB']._serialized_start=5795 + _globals['_CANCELJOB']._serialized_end=5843 + _globals['_MODELOP']._serialized_start=5846 + _globals['_MODELOP']._serialized_end=6006 + _globals['_MODELEVENT']._serialized_start=6009 + _globals['_MODELEVENT']._serialized_end=6548 + _globals['_ACTIVITYUPDATE']._serialized_start=6551 + _globals['_ACTIVITYUPDATE']._serialized_end=6749 + _globals['_FNUNAVAILABLE']._serialized_start=6752 + _globals['_FNUNAVAILABLE']._serialized_end=6922 + _globals['_FNUNAVAILABLE_AXESENTRY']._serialized_start=6879 + _globals['_FNUNAVAILABLE_AXESENTRY']._serialized_end=6922 + _globals['_FNDEGRADED']._serialized_start=6925 + _globals['_FNDEGRADED']._serialized_end=7066 + _globals['_DRAIN']._serialized_start=7068 + _globals['_DRAIN']._serialized_end=7096 + _globals['_TOKENREFRESH']._serialized_start=7098 + _globals['_TOKENREFRESH']._serialized_end=7152 + _globals['_WORKERSCHEDULER']._serialized_start=8511 + _globals['_WORKERSCHEDULER']._serialized_end=8608 # @@protoc_insertion_point(module_scope) diff --git a/src/gen_worker/pb/worker_scheduler_pb2.pyi b/src/gen_worker/pb/worker_scheduler_pb2.pyi index 876cfb66..a690f689 100644 --- a/src/gen_worker/pb/worker_scheduler_pb2.pyi +++ b/src/gen_worker/pb/worker_scheduler_pb2.pyi @@ -472,16 +472,25 @@ class ResolvedCompute(_message.Message): def __init__(self, accelerator: _Optional[str] = ..., gpu_index: _Optional[int] = ...) -> None: ... class ModelBinding(_message.Message): - __slots__ = ("slot", "ref", "loras", "inference_defaults") + __slots__ = ("slot", "ref", "loras", "inference_defaults", "components") + class ComponentsEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... SLOT_FIELD_NUMBER: _ClassVar[int] REF_FIELD_NUMBER: _ClassVar[int] LORAS_FIELD_NUMBER: _ClassVar[int] INFERENCE_DEFAULTS_FIELD_NUMBER: _ClassVar[int] + COMPONENTS_FIELD_NUMBER: _ClassVar[int] slot: str ref: str loras: _containers.RepeatedCompositeFieldContainer[LoraOverlay] inference_defaults: str - def __init__(self, slot: _Optional[str] = ..., ref: _Optional[str] = ..., loras: _Optional[_Iterable[_Union[LoraOverlay, _Mapping]]] = ..., inference_defaults: _Optional[str] = ...) -> None: ... + components: _containers.ScalarMap[str, str] + def __init__(self, slot: _Optional[str] = ..., ref: _Optional[str] = ..., loras: _Optional[_Iterable[_Union[LoraOverlay, _Mapping]]] = ..., inference_defaults: _Optional[str] = ..., components: _Optional[_Mapping[str, str]] = ...) -> None: ... class LoraOverlay(_message.Message): __slots__ = ("ref", "weight", "inference_defaults") diff --git a/src/gen_worker/registry.py b/src/gen_worker/registry.py index 5e7291bc..4749a374 100644 --- a/src/gen_worker/registry.py +++ b/src/gen_worker/registry.py @@ -129,12 +129,10 @@ def _validate_slot_selected_by( """pgw#520: a Slot's ``selected_by`` must name a plain-``str`` (or ``str | ModelRef``, pgw#524 item 5) field on THIS handler's payload — validated per-handler (not per-class) because one ``models=`` decl can - be shared by methods with different payload types. Also enforces pgw#524 - item 4: a request-branching slot (``selected_by`` set) with no - ``default_checkpoint`` has nothing to seed the hub mapping or bootstrap - placement with — tensorhub rejects it at manifest registration - (``builder.normalizeManifestSlot``), so fail here at AUTHOR time instead - of at publish time. Fails at spec-construction time (discovery walk / + be shared by methods with different payload types. ``selected_by`` + slots may omit ``default_checkpoint`` (pgw#617: deploy-time bindings + seed the hub mapping — tensorhub relaxed the mirrored registration + rule in th#980). Fails at spec-construction time (discovery walk / CLI collection / executor boot), never at first invoke.""" if not slots: return @@ -145,14 +143,6 @@ def _validate_slot_selected_by( for slot_name, slot in slots.items(): if not slot.selected_by: continue - if slot.default_checkpoint is None: - raise ValueError( - f"{owner}: Slot({slot_name!r}).selected_by={slot.selected_by!r} " - "requires default_checkpoint=... — a request-branching slot " - "with no code-side default has nothing to seed the hub " - "mapping/bootstrap placement with (the hub rejects this at " - "registration; fail here instead)" - ) if slot.selected_by not in hints: raise ValueError( f"{owner}: Slot({slot_name!r}).selected_by={slot.selected_by!r} " diff --git a/tests/harness/toy_endpoints.py b/tests/harness/toy_endpoints.py index 58b1814b..665a7e8f 100644 --- a/tests/harness/toy_endpoints.py +++ b/tests/harness/toy_endpoints.py @@ -215,6 +215,68 @@ def slot_identity_catalog(self, ctx: RequestContext, data: EchoIn) -> EchoOut: return EchoOut(response=f"{ref.source}:{ref.path}:{ref.tag}#{ref.flavor}") +# --------------------------------------------------------------------------- +# pgw#617: hierarchical component bindings (load-then-substitute). +# --------------------------------------------------------------------------- + + +class ToyVae: + """Component class named by the composed base's model_index.json.""" + + def __init__(self, content: str, source: str) -> None: + self.content = content + self.source = source + + @classmethod + def from_pretrained(cls, path: str, **_kw: object) -> "ToyVae": + return cls((Path(path) / "weights.txt").read_text(), source=str(path)) + + +class ToyComposedPipeline: + """Diffusers-shaped fake: from_pretrained loads components from the tree + unless a preloaded module is injected via kwargs (the gw#479/pgw#617 + ``components=`` mechanism).""" + + def __init__(self, base_weights: str, vae: ToyVae, injected: bool) -> None: + self.base_weights = base_weights + self.vae = vae + self.vae_injected = injected + + @classmethod + def from_pretrained(cls, path: str, **kwargs: object) -> "ToyComposedPipeline": + base = (Path(path) / "transformer" / "weights.txt").read_text() + vae = kwargs.get("vae") + if isinstance(vae, ToyVae): + return cls(base, vae, injected=True) + return cls(base, ToyVae.from_pretrained(str(Path(path) / "vae")), injected=False) + + def to(self, device: str) -> "ToyComposedPipeline": + return self + + +COMPOSED_DECLARED = Hub("harness/composed-base", tag="prod") +COMPOSED_SETUPS: list = [] # one entry per setup() run (identity-change proof) + + +@endpoint(models={ + "pipeline": Slot( + ToyComposedPipeline, default_checkpoint=COMPOSED_DECLARED, + default_config=_ToyDefaults(), + ), +}) +class ComposedEndpoint: + def setup(self, pipeline: ToyComposedPipeline) -> None: + COMPOSED_SETUPS.append(id(self)) + self.pipe = pipeline + + def composed_echo(self, ctx: RequestContext, data: EchoIn) -> EchoOut: + p = self.pipe + return EchoOut(response=( + f"base={p.base_weights}|vae={p.vae.content}" + f"|injected={p.vae_injected}|setups={len(COMPOSED_SETUPS)}" + )) + + # --------------------------------------------------------------------------- # P9: typed billing usage on JobMetrics, inline vs blob_ref by size alone. # --------------------------------------------------------------------------- diff --git a/tests/test_component_bindings_pgw617.py b/tests/test_component_bindings_pgw617.py new file mode 100644 index 00000000..5a928e21 --- /dev/null +++ b/tests/test_component_bindings_pgw617.py @@ -0,0 +1,182 @@ +"""pgw#617: hierarchical slot bindings — load base composition, substitute +components from ``RunJob.ModelBinding.components`` (th#980 companion). + +Real hub-double boundary (worker/lifecycle/executor real, blake3 blob host): + + * flat bindings (empty components map) behave byte-identically to today; + * a dispatched component override loads from its OWN materialized snapshot + and is injected into the base ``from_pretrained`` (load-then-substitute); + * the composition (base + sorted component refs) is the instance identity — + a component-only rebind re-runs setup, and the flat instance survives + untouched alongside it; + * an unknown component name refuses typed (ComponentSubstitutionError), + never mid-denoise; + * registry relaxation: a ``selected_by=`` slot may omit + ``default_checkpoint`` (deploy-time bindings seed the hub mapping). +""" + +from __future__ import annotations + +import msgspec +import pytest + +from gen_worker import RequestContext, Slot, endpoint +from gen_worker.api.binding import wire_ref +from gen_worker.pb import worker_scheduler_pb2 as pb +from gen_worker.registry import extract_specs + +from harness.blob_host import BlobHost +from harness.hub_double import hub_double, is_ready, is_result_for +from harness.toy_endpoints import ( + COMPOSED_DECLARED, + COMPOSED_SETUPS, + EchoIn, + EchoOut, +) + + +def _payload(**kw: object) -> bytes: + return msgspec.msgpack.encode(EchoIn(**kw)) # type: ignore[arg-type] + + +def _decode(data: bytes) -> EchoOut: + return msgspec.msgpack.decode(data, type=EchoOut) + + +BASE_REF = wire_ref(COMPOSED_DECLARED) +VAE_REF = "harness/override-vae:prod" + + +def _base_snapshot(blobs: BlobHost) -> pb.Snapshot: + model_index = ( + b'{"_class_name": "ToyComposedPipeline",' + b' "vae": ["harness.toy_endpoints", "ToyVae"],' + b' "transformer": ["harness.toy_endpoints", "ToyVae"]}' + ) + return blobs.snapshot("snap-composed-base", [ + blobs.file("mi", model_index, path_in_snapshot="model_index.json"), + blobs.file("tw", b"base-transformer", path_in_snapshot="transformer/weights.txt"), + blobs.file("vw", b"base-vae", path_in_snapshot="vae/weights.txt"), + ]) + + +def _vae_snapshot(blobs: BlobHost) -> pb.Snapshot: + # Override tree carries the component at its ROOT (no subfolder): the + # loader falls back from `/vae` to ``. + return blobs.snapshot("snap-override-vae", [ + blobs.file("ow", b"override-vae", path_in_snapshot="weights.txt"), + ]) + + +def _run(conn, rid: str, models, snapshots) -> pb.JobResult: + conn.send(run_job=pb.RunJob( + request_id=rid, attempt=1, function_name="composed-echo", + input_payload=_payload(), models=models, snapshots=snapshots, + )) + return conn.wait_for(is_result_for(rid)).job_result + + +def test_flat_then_substituted_then_flat(tmp_path) -> None: + """One connection, three dispatches: flat -> vae override -> flat. + + The override run substitutes the injected module (load-then-substitute) + and derives a NEW instance (setup re-runs); the final flat run reuses + the FIRST instance (no third setup) — composition identity separates + them both ways.""" + COMPOSED_SETUPS.clear() # module-global across same-process workers + blobs = BlobHost(tmp_path) + try: + base_snap = _base_snapshot(blobs) + vae_snap = _vae_snapshot(blobs) + flat = [pb.ModelBinding(slot="pipeline", ref=BASE_REF)] + subst = [pb.ModelBinding( + slot="pipeline", ref=BASE_REF, components={"vae": VAE_REF}, + )] + snaps = {BASE_REF: base_snap, VAE_REF: vae_snap} + with hub_double() as (scheduler, _harness): + conn = scheduler.wait_connection(0) + conn.wait_for(is_ready) + + res = _run(conn, "r-flat", flat, {BASE_REF: base_snap}) + assert res.status == pb.JOB_STATUS_OK, res.safe_message + flat_out = _decode(res.inline).response + assert "base=base-transformer" in flat_out + assert "vae=base-vae" in flat_out + assert "injected=False" in flat_out + assert "setups=1" in flat_out + + res = _run(conn, "r-subst", subst, snaps) + assert res.status == pb.JOB_STATUS_OK, res.safe_message + subst_out = _decode(res.inline).response + assert "base=base-transformer" in subst_out + assert "vae=override-vae" in subst_out # substituted bytes + assert "injected=True" in subst_out # via components= kwarg + assert "setups=2" in subst_out # NEW composition identity + + res = _run(conn, "r-flat-2", flat, {BASE_REF: base_snap}) + assert res.status == pb.JOB_STATUS_OK, res.safe_message + back_out = _decode(res.inline).response + assert "vae=base-vae" in back_out + assert "setups=2" in back_out # flat instance reused, no reload + finally: + blobs.shutdown() + + +def test_unknown_component_refuses_typed(tmp_path) -> None: + blobs = BlobHost(tmp_path) + try: + base_snap = _base_snapshot(blobs) + vae_snap = _vae_snapshot(blobs) + models = [pb.ModelBinding( + slot="pipeline", ref=BASE_REF, + components={"text_encoder": VAE_REF}, + )] + with hub_double() as (scheduler, _harness): + conn = scheduler.wait_connection(0) + conn.wait_for(is_ready) + res = _run(conn, "r-unknown", models, + {BASE_REF: base_snap, VAE_REF: vae_snap}) + assert res.status != pb.JOB_STATUS_OK + assert "ComponentSubstitutionError" in res.safe_message + assert "text_encoder" in res.safe_message + finally: + blobs.shutdown() + + +# --------------------------------------------------------------------------- +# Registry relaxation: selected_by slots may be default-less (pgw#617). +# --------------------------------------------------------------------------- + + +class _PickIn(msgspec.Struct): + model: str = "" + + +class _PickOut(msgspec.Struct): + response: str + + +def test_selected_by_slot_may_omit_default_checkpoint() -> None: + @endpoint(models={"pipeline": Slot(str, selected_by="model")}) + class DefaultlessCatalog: + def setup(self, pipeline: str) -> None: + self.pipeline_path = pipeline + + def pick(self, ctx: RequestContext, payload: _PickIn) -> _PickOut: + return _PickOut(response="ok") + + specs = extract_specs(DefaultlessCatalog) + assert [s.name for s in specs] == ["pick"] + assert specs[0].slots["pipeline"].default_checkpoint is None + + +def test_selected_by_still_validates_payload_field() -> None: + @endpoint(models={"pipeline": Slot(str, selected_by="nonexistent")}) + class BadCatalog: + def setup(self, pipeline: str) -> None: ... + + def bad_pick(self, ctx: RequestContext, payload: _PickIn) -> _PickOut: + return _PickOut(response="no") + + with pytest.raises(ValueError, match="names no field"): + extract_specs(BadCatalog) diff --git a/uv.lock b/uv.lock index 5ab97d71..d1439f36 100644 --- a/uv.lock +++ b/uv.lock @@ -583,7 +583,7 @@ wheels = [ [[package]] name = "gen-worker" -version = "0.43.1" +version = "0.44.0" source = { editable = "." } dependencies = [ { name = "blake3" },