Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@ jobs:
- name: pgw#1049 guard - torch-settings writes outside the authority
run: uv run python scripts/lint_settings_writers.py

# GATING (pgw#1143, §1.33): `Slot(layouts=...)` is the per-slot layout
# DEMAND the hub's gate reads. It must be readable by the AST sweep as
# well as by the import, so a computed declaration is refused where it
# is written — otherwise the published manifest becomes the only place
# the demand can be read, which is the dual-declaration hazard.
- name: pgw#1143 guard - Slot(layouts=) declarations are literals
run: uv run python scripts/lint_layout_declarations.py

# GATING (pgw#1122): the compute child holds no worker credential BY
# CONSTRUCTION, so a gate that reads one to answer "who am I?" or "is
# there a hub?" refuses on every real serving pod (pgw#1108, then
Expand Down
1 change: 1 addition & 0 deletions changelog.d/pgw1143.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **pgw#1143 steps 1-3 (DESIGN-RULINGS §1.33, with th#1809 T1-T2): a slot DECLARES the tensor layouts its code can execute.** `Slot(layouts={component_path: (handle, ...)})` is the code-side DEMAND half of the layout contract — an ordered tuple per component path (`"*"` is the whole tree), ordered by preference, published into the release manifest as `functions[].slots[].layouts` so the hub can gate a rebind against what the AUTHOR says the slot needs instead of against what the installed wheel happens to decode (2 distinct decoder signatures across 64 releases). **Absent is UNDECLARED** — a tri-state, never "accepts everything": an empty mapping and an empty tuple are both decoration-time errors, because collapsing that tri-state is th#1580's fail-open defect wearing a new name. It lives on `Slot` and deliberately not on `Compile`, whose fields feed `contract_axes()` and would re-key every cell in the fleet for a fact §1.33 point 5 says must never enter the key. Handles are validated against `KNOWN_CONTRACTS` at the constructor and component keys against the DERIVED component tree at decoration (a key matching nothing is a build error naming the tree); the SDK emits HANDLES only and the hub resolves each to its descriptor digest at manifest ingest, so the transcription is allowed to be stale and is CHECKED, never authoritative. The decoder census stays a LOWER bound: a declared handle no `@implements_contract` decoder backs lands on the manifest as `layouts_census_unbacked` and does not refuse, since the blockwise conditioner is decoded natively by `transformers` with zero cozy markers. New `fast gates` guard `scripts/lint_layout_declarations.py` refuses a declaration the AST sweep cannot read (a comprehension, an f-string, a helper-built dict), because a computed declaration would make the published manifest the only place the demand can be read — the dual-declaration hazard pgw#1107 spent a program deleting.
28 changes: 28 additions & 0 deletions docs/endpoint-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,34 @@ class Generate:
components (the qwen text encoder, a shared VAE) load once and are
refcounted across checkpoint picks; there is nothing to declare
(`share_components=` is deleted).
- **`layouts=` declares WHAT BYTES THIS SLOT'S CODE CAN EXECUTE** (§1.33,
pgw#1143) — the DEMAND half of the tensor-layout contract, per component
path, ordered by preference:

```python
from gen_worker.models.tensor_layout_contract import (
CONTRACT_HF_FP8_BLOCKWISE, CONTRACT_PLAIN_BF16)

Slot(StableDiffusionXLPipeline, selected_by="model", layouts={
"*": (CONTRACT_PLAIN_BF16,),
"text_encoder": (CONTRACT_HF_FP8_BLOCKWISE, CONTRACT_PLAIN_BF16),
})
```

`"*"` is the whole-tree default; a component key overrides it for that
component. The hub compares an artifact's PROVEN layout against this set at
rebind and refuses a mismatch with both sides named, before any pod is
bought. **Omitting `layouts=` leaves the slot UNDECLARED** — the gate then
falls back to the image-wide decoder census, and absence is never read as
"accepts everything"; an empty mapping or an empty tuple is a
decoration-time error. Handles must be registered
(`KNOWN_CONTRACTS`, transcribed from tensorhub's `internal/tensorlayout`)
and written as LITERALS or as constants imported from that module —
`scripts/lint_layout_declarations.py` refuses anything the AST sweep cannot
read. Declaring a handle no decoder in the image backs is NOT an error: it
lands in the build log as `layouts_census_unbacked`, because plenty of
layouts are decoded natively by `transformers`/`diffusers` with no cozy
marker.

**Per-family defaults vocabulary**: a typed, versioned,
JSON-Schema-exportable struct per architecture — the shape tensorhub validates
Expand Down
186 changes: 186 additions & 0 deletions scripts/lint_layout_declarations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""pgw#1143 step 3 (§1.33): `Slot(layouts=...)` is READABLE WITHOUT IMPORTING.

The per-slot DEMAND is the one fact the hub's layout gate reads out of the
manifest, so it has to be legible to BOTH paths that ever look at it:

* the IMPORT path — `gen_worker.discovery` imports endpoint modules with heavy
deps stubbed (`discovery/heavy_deps.py`: "that metadata is torch-free by
design"), and
* the AST path — the sweep pgw#1107 used to find the eight class-less
`Compile(...)` declarations, and this script.

A computed declaration (a comprehension, an f-string, a dict built by a
helper, a value read from config) is invisible to the second. It would make
the published manifest the only place the demand can be read, which is exactly
the dual-declaration hazard pgw#1107 spent a program deleting — and it is
unreviewable in a diff, which is where a layout demand is actually judged.

So: `layouts=` must be a DICT LITERAL whose keys are string literals and whose
values are tuple/list literals of string literals or of names imported from
`gen_worker.models.tensor_layout_contract`. Nothing else.

This is a fence, not a taste rule. It carries no allowlist by design: there is
no declaration this refuses that could not be written literally instead.

Usage:

python scripts/lint_layout_declarations.py [PATH ...]

Defaults to this repository's `src/`. An ENDPOINT repo runs the same script
against its own tree — the constraint is on the declaration, not on where it
lives.
"""

from __future__ import annotations

import ast
import sys
from pathlib import Path
from typing import Iterator, List, Set, Tuple

REPO = Path(__file__).resolve().parents[1]

#: `src/` only, deliberately. The subject is a declaration that gets
#: PUBLISHED — an endpoint's `Slot(layouts=...)` reaching a release manifest.
#: `tests/` holds the opposite by construction: a test proving the constructor
#: refuses a computed declaration has to WRITE one, and sweeping it would make
#: this fence and its own negative test mutually exclusive. An endpoint repo
#: runs this against its own tree, where every declaration is a real one.
DEFAULT_ROOTS = (REPO / "src",)

#: The module whose module-level constants may stand in for a handle literal.
#: It is the SDK's transcription of tensorhub's registry, so a name imported
#: from it resolves to a handle string the lint can still classify by NAME.
VOCABULARY_MODULE = "gen_worker.models.tensor_layout_contract"


def _iter_python_files(roots: Tuple[Path, ...]) -> Iterator[Path]:
for root in roots:
if root.is_file() and root.suffix == ".py":
yield root
continue
if not root.is_dir():
continue
for path in sorted(root.rglob("*.py")):
yield path


def _vocabulary_names(tree: ast.Module) -> Set[str]:
"""Local names bound to constants of the vocabulary module."""
names: Set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module == VOCABULARY_MODULE:
for alias in node.names:
names.add(alias.asname or alias.name)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name == VOCABULARY_MODULE:
names.add(alias.asname or alias.name.split(".")[0])
return names


def _handle_is_readable(node: ast.expr, vocabulary: Set[str]) -> bool:
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return True
if isinstance(node, ast.Name):
return node.id in vocabulary
if isinstance(node, ast.Attribute):
# `tensor_layout_contract.CONTRACT_PLAIN_BF16` — the module itself was
# imported under a name the sweep can resolve.
root: ast.expr = node
while isinstance(root, ast.Attribute):
root = root.value
return isinstance(root, ast.Name) and root.id in vocabulary
return False


def _check_layouts(
path: Path, call: ast.Call, value: ast.expr, vocabulary: Set[str],
) -> List[str]:
where = f"{path}:{value.lineno}"
if not isinstance(value, ast.Dict):
return [
f"{where}: layouts= is a {type(value).__name__}, not a dict "
"literal — the AST sweep cannot read it, so the hub's copy would "
"be the only place this demand can be read"
]
problems: List[str] = []
for key, item in zip(value.keys, value.values):
if key is None:
problems.append(
f"{where}: layouts= splats another mapping (**); a splat hides "
"the component paths from the sweep")
continue
if not (isinstance(key, ast.Constant) and isinstance(key.value, str)):
problems.append(
f"{path}:{key.lineno}: layouts= key is not a string literal")
continue
if not isinstance(item, (ast.Tuple, ast.List)):
problems.append(
f"{path}:{item.lineno}: layouts[{key.value!r}] is a "
f"{type(item).__name__}, not a tuple literal — order is "
"preference and a computed sequence has no reviewable order")
continue
for element in item.elts:
if not _handle_is_readable(element, vocabulary):
problems.append(
f"{path}:{element.lineno}: layouts[{key.value!r}] holds a "
f"{type(element).__name__}; every handle must be a string "
f"literal or a constant imported from {VOCABULARY_MODULE}")
return problems


def _call_name(call: ast.Call) -> str:
func = call.func
if isinstance(func, ast.Name):
return func.id
if isinstance(func, ast.Attribute):
return func.attr
return ""


def scan(path: Path) -> List[str]:
try:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
except (SyntaxError, UnicodeDecodeError) as exc:
return [f"{path}: could not parse ({exc})"]
vocabulary = _vocabulary_names(tree)
problems: List[str] = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call) or _call_name(node) != "Slot":
continue
for kw in node.keywords:
if kw.arg == "layouts":
problems.extend(_check_layouts(path, node, kw.value, vocabulary))
elif kw.arg is None:
# `Slot(cls, **kwargs)` — a layouts= could be hiding in there.
problems.append(
f"{path}:{node.lineno}: Slot(**kwargs) may carry a "
"layouts= the sweep cannot see; pass the declaration "
"explicitly")
return problems


def main(argv: List[str]) -> int:
roots = tuple(Path(a).resolve() for a in argv[1:]) or DEFAULT_ROOTS
problems: List[str] = []
for path in _iter_python_files(roots):
problems.extend(scan(path))
if problems:
print("pgw#1143: Slot(layouts=...) declarations that the AST sweep "
"cannot read:\n", file=sys.stderr)
for problem in problems:
print(f" {problem}", file=sys.stderr)
print(
"\nWrite the declaration literally — a dict literal of string "
"literals mapping to tuple literals of handles. The demand is "
"reviewed in the diff and read by the hub; both need it visible "
"without running the module.",
file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv))
43 changes: 40 additions & 3 deletions src/gen_worker/api/slot.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ def generate(self, ctx: RequestContext[SdxlDefaults], p: In) -> Out:

from __future__ import annotations

from typing import Any, Dict, Generic, Optional, Sequence, Type, TypeVar
from typing import Any, Dict, Generic, Mapping, Optional, Sequence, Type, TypeVar

import msgspec

from .binding import ModelRef
from ..families.base import KIND_LORA, GenerationDefaults, family_for
from ..models.tensor_layout_contract import normalize_layout_demand

D = TypeVar("D", bound=GenerationDefaults)

Expand Down Expand Up @@ -125,6 +126,32 @@ class (diffusers-style: exposes ``_get_signature_keys`` or a components
multi-slot shape must mark exactly one root — ambiguity is a
decoration-time error, never a silent fallback.

``layouts`` is the slot's per-component DEMAND (§1.33, pgw#1143): a
mapping from component path to the ORDERED tuple of tensor-layout
contract handles this slot's code can execute. ``"*"`` is the whole-tree
default; a component key overrides it for that component only::

Slot(StableDiffusionXLPipeline, selected_by="model", layouts={
"*": (CONTRACT_PLAIN_BF16,),
"text_encoder": (CONTRACT_HF_FP8_BLOCKWISE, CONTRACT_PLAIN_BF16),
})

Order IS preference. The handles are validated here against the SDK's
transcribed ``KNOWN_CONTRACTS``; the KEYS are validated at decoration
against the slot's DERIVED component tree, so a key that would silently
match nothing is a build error naming the tree. **Absent is UNDECLARED**
— a tri-state, neither "accepts everything" nor "accepts nothing": the
hub's gate has no teeth on a slot that declares nothing and falls back to
the image-wide decoder census. An empty mapping or an empty tuple is a
decoration-time error, because collapsing the tri-state is th#1580's
fail-open defect wearing a new name.

This lives on ``Slot`` and NOT on ``Compile``: ``Compile``'s fields feed
``contract_axes()``, a cell-key input, and §1.33 point 5 is that
conversion is upstream of compute and invisible to cell identity. A
layout declaration there would either re-key every cell in the fleet or
sit inside the key struct while deliberately not participating.

``optional`` is DERIVED, never passed: a slot is optional exactly when
its ``setup()`` parameter carries a default (``edit: Pipe | None =
None``). The signature is the single source of truth, so the two can
Expand All @@ -135,7 +162,7 @@ class (diffusers-style: exposes ``_get_signature_keys`` or a components

__slots__ = (
"pipeline_cls", "selected_by", "family", "default_checkpoint", "root",
"optional",
"optional", "layouts",
)

def __init__(
Expand All @@ -146,6 +173,7 @@ def __init__(
family: Optional[str] = None,
default_checkpoint: Optional[ModelRef] = None,
root: bool = False,
layouts: Optional[Mapping[str, Sequence[str]]] = None,
) -> None:
if not isinstance(pipeline_cls, type):
raise TypeError(
Expand All @@ -163,12 +191,21 @@ def __init__(
self.default_checkpoint = default_checkpoint
self.root = bool(root)
self.optional = False # derived at decoration from setup()'s default
# None is UNDECLARED and stays None; anything else normalizes now, at
# the declaration site, so the traceback names the Slot the author
# wrote rather than a manifest key.
self.layouts: Optional[Dict[str, tuple[str, ...]]] = (
None if layouts is None
else normalize_layout_demand(
layouts, where=f"Slot({pipeline_cls.__name__})")
)

def __repr__(self) -> str: # pragma: no cover - debug aid
return (
f"Slot({self.pipeline_cls.__name__}, selected_by={self.selected_by!r}, "
f"family={self.family!r}, default_checkpoint={self.default_checkpoint!r}, "
f"root={self.root!r}, optional={self.optional!r})"
f"root={self.root!r}, optional={self.optional!r}, "
f"layouts={self.layouts!r})"
)


Expand Down
36 changes: 36 additions & 0 deletions src/gen_worker/discovery/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
VideoAsset,
)
from gen_worker.discovery.execution_lanes import (
DerivedExecutionLanes,
derive_execution_lanes,
execution_lanes_for_function,
manifest_block,
Expand Down Expand Up @@ -451,6 +452,16 @@ def _slot_to_manifest(
{"name": part, "kind": kind}
for part, kind in sorted(components.items())
]
# §1.33 / pgw#1143: the per-component DEMAND. Absent means UNDECLARED —
# the hub's gate falls back to the image-wide decoder census for this
# slot, and never reads absence as "accepts everything". Handles only:
# the hub resolves each to its descriptor DIGEST at ingest, against its
# own registry, which is the only moment one wheel and one hub are both
# pinned.
if slot.layouts:
out["layouts"] = {
path: list(handles) for path, handles in slot.layouts.items()
}
return out


Expand Down Expand Up @@ -939,6 +950,9 @@ def discover_manifest(root: Optional[Path] = None) -> Dict[str, Any]:
{"execution_lane": e.execution_lane, "reason": e.reason}
for e in exclusions
]
unbacked = _census_unbacked_layouts(fn, derived)
if unbacked:
fn["layouts_census_unbacked"] = list(unbacked)

manifest: Dict[str, Any] = {
"functions": functions,
Expand All @@ -950,6 +964,28 @@ def discover_manifest(root: Optional[Path] = None) -> Dict[str, Any]:
return manifest


def _census_unbacked_layouts(
fn: Dict[str, Any], derived: DerivedExecutionLanes,
) -> List[str]:
"""Declared handles no `@implements_contract` decoder in this image backs.

**NOT a refusal**, deliberately (§1.33: the census is a LOWER-bound sanity
check, and a lower bound that refuses is an upper bound). The ruling's own
worked example is decoded natively by `transformers` via
`quantization_config` with zero cozy markers, so refusing here would make
the design's motivating case illegal. It lands on the manifest and in the
build log so an author can see the gap and judge it.
"""
backed = {c.contract for c in derived.contracts}
unbacked: List[str] = []
for slot in fn.get("slots") or []:
for handles in (slot.get("layouts") or {}).values():
for handle in handles:
if handle not in backed and handle not in unbacked:
unbacked.append(handle)
return sorted(unbacked)


def _strip_none(obj: Any) -> Any:
"""Recursively remove None values from dicts/lists (TOML has no null type)."""
if isinstance(obj, dict):
Expand Down
Loading
Loading