fix: reconcile merged runtime behavior

This commit is contained in:
Dobromir Popov
2026-07-17 14:29:09 +03:00
parent 6aced6a005
commit f0197bfa83
3 changed files with 45 additions and 58 deletions

View File

@@ -36,7 +36,6 @@ from .capability import (
CapabilityReport, CapabilityReport,
build_capability_report, build_capability_report,
) )
from .native_backend import NativeWorkerBackendAdapter
from .recipe_manifest import ( from .recipe_manifest import (
DEFAULT_RECIPE_ID, DEFAULT_RECIPE_ID,
Recipe, Recipe,
@@ -450,9 +449,11 @@ def _validate_recipe(
category: str | None = None category: str | None = None
error: BaseException | None = None error: BaseException | None = None
diagnostics: list[str] = [] diagnostics: list[str] = []
detail: dict = {}
try: try:
backend = load_backend(selection, recipe) backend = load_backend(selection, recipe)
probe_forward(backend) detail = probe_forward(backend)
except DoctorError as exc: except DoctorError as exc:
category, error = exc.category, exc category, error = exc.category, exc
diagnostics = [str(exc), exc.hint] diagnostics = [str(exc), exc.hint]
@@ -463,48 +464,23 @@ def _validate_recipe(
duration_ms = int((time.monotonic() - started) * 1000) duration_ms = int((time.monotonic() - started) * 1000)
device = _backend_device(backend, selection) device = _backend_device(backend, selection)
# Only the native adapter has an authoritative immutable GGUF report and
# deployment pin. The Transformers path deliberately remains dark: a
# model/config fingerprint is not an exact ArtifactIdentity.
identity = backend.identity if isinstance(backend, NativeWorkerBackendAdapter) else None
model_id = selection.model_id if identity is None else identity.artifact.artifact_id
shard_start = selection.shard_start if identity is None else identity.shard_start
shard_end = selection.shard_end if identity is None else identity.shard_end - 1
recipe_id = recipe.id if identity is None else identity.recipe.recipe_id
recipe_version = recipe.version if identity is None else identity.recipe.recipe_version
catalogue_version = (
manifest.catalogue_version if identity is None else identity.recipe.catalogue_version
)
backend_id = recipe.backend_id if identity is None else identity.recipe.backend_id
quantization = (
selection.quantization if identity is None else identity.recipe.weight_quantization
)
runtime = _runtime_versions()
model_config = _model_config(backend)
revision = None
if identity is not None:
revision = identity.artifact.revision
model_config = "sha256:" + identity.artifact.architecture_digest
runtime = {**runtime, "native_runtime": identity.recipe.runtime_version}
report = build_capability_report( report = build_capability_report(
model_id=model_id, model_id=selection.model_id,
shard_start=shard_start, shard_start=selection.shard_start,
shard_end=shard_end, shard_end=selection.shard_end,
recipe_id=recipe_id, recipe_id=recipe.id,
recipe_version=recipe_version, recipe_version=recipe.version,
catalogue_version=catalogue_version, catalogue_version=manifest.catalogue_version,
backend_id=backend_id, backend_id=recipe.backend_id,
device=device, device=device,
device_name=_backend_device_name(device), device_name=_backend_device_name(device),
quantization=quantization, quantization=selection.quantization,
runtime=runtime, runtime=_runtime_versions(),
revision=revision, model_config=_model_config(backend),
model_config=model_config,
status=STATUS_FAILED if category else STATUS_PASSED, status=STATUS_FAILED if category else STATUS_PASSED,
duration_ms=duration_ms, duration_ms=duration_ms,
diagnostics=[d for d in diagnostics if d] or None, diagnostics=[d for d in diagnostics if d] or None,
validated_at=clock(), validated_at=clock(),
identity=identity,
) )
if category: if category:
return RecipeResult( return RecipeResult(

View File

@@ -1576,8 +1576,7 @@ class TorchNodeServer:
if drop_directive is not None: if drop_directive is not None:
model_id = str(drop_directive.get("model") or "") model_id = str(drop_directive.get("model") or "")
removed = self._backends.pop(model_id, None) removed = self._backends.pop(model_id, None)
if removed is None: if removed is not None:
return None
if self._backends: if self._backends:
self._backend = next(iter(self._backends.values())) self._backend = next(iter(self._backends.values()))
self._tracker_mode = self._backend.shard_start == 0 self._tracker_mode = self._backend.shard_start == 0
@@ -1588,7 +1587,13 @@ class TorchNodeServer:
self._server.backends = dict(self._backends) self._server.backends = dict(self._backends)
self._server.backend = self._backend self._server.backend = self._backend
self._server.tracker_mode = self._tracker_mode self._server.tracker_mode = self._tracker_mode
return {"action": "DROP_SHARD", "model": model_id} # A DROP_SHARD may be paired with a subsequent LOAD_SHARD in the
# same heartbeat; continue so the replacement is applied atomically.
if not any(
directive.get("action") in {"ADD_SHARD", "LOAD_SHARD"}
for directive in directives
):
return {"action": "DROP_SHARD", "model": model_id} if removed is not None else None
add_directive = next( add_directive = next(
(directive for directive in reversed(directives) if directive.get("action") == "ADD_SHARD"), (directive for directive in reversed(directives) if directive.get("action") == "ADD_SHARD"),
None, None,
@@ -1602,8 +1607,11 @@ class TorchNodeServer:
return None return None
shard_start = int(directive["shard_start"]) shard_start = int(directive["shard_start"])
shard_end = int(directive["shard_end"]) shard_end = int(directive["shard_end"])
quantization = str(directive.get("quantization") or self._backend.quantization) quantization = str(
model_id = str(directive.get("model") or self._backend.model_id) directive.get("quantization")
or (self._backend.quantization if self._backend is not None else "auto")
)
model_id = str(directive.get("model") or (self._backend.model_id if self._backend is not None else ""))
replacing = directive.get("action") == "LOAD_SHARD" replacing = directive.get("action") == "LOAD_SHARD"
if not replacing and len(self._backends) >= self._max_loaded_shards: if not replacing and len(self._backends) >= self._max_loaded_shards:
print( print(

View File

@@ -3295,12 +3295,15 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
node.hf_repo or node.model node.hf_repo or node.model
for node in alive for node in alive
if node.model is not None if node.model is not None
# The same model can be registered under its HF repository while # Explicit HF repositories are emitted as stable identifiers even
# the catalogue exposes its short preset id. Do not emit a second # when they also resolve to a short-name preset; clients may use
# repo-keyed entry when either node identifier resolves to a preset. # either identifier when selecting a model.
and _resolve_model_preset( and (
server.model_presets, node.hf_repo or node.model, node.hf_repo is not None
or _resolve_model_preset(
server.model_presets, node.model,
)[1] is None )[1] is None
)
and node.shard_start is not None and node.shard_start is not None
and node.shard_end is not None and node.shard_end is not None
and node.num_layers is not None and node.num_layers is not None