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
66 changes: 60 additions & 6 deletions hack/bump
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
pin is moved by its group's hook and checked after it).
3. Hooks, in the group's order: lock (images.lock), readme-history, render-latest,
ppa-write, convert-pins, ledger-rules, constraints, docs-lock, flake-update,
repin (the digests group); ledger-rules also runs whenever console/ changed.
repin (the digests group), hf-meta (each moved Hugging Face repo's record in
the saved metadata bundles, tests/fixtures/hf, read again at its new revision);
ledger-rules also runs whenever console/ changed.
4. hack/substitute-goldens: each exact old pin string becomes its new one in
tests/** only.
tests/** only (hack/pinlib/goldens.py says which strings a test keeps).
5. The group's tests (skipped by --dry-run and --no-tests).
6. The policy verdict (hack/pins policy) of the resulting diff, and a PR body.

Expand Down Expand Up @@ -47,10 +49,10 @@ import tomllib

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from pinlib import goldens, ledgerinfo, lockfile, oci, policy, registry, sources # noqa: E402
from pinlib import goldens, http, ledgerinfo, lockfile, oci, policy, registry, sources # noqa: E402

HOOKS = ("lock", "readme-history", "render-latest", "ppa-write", "convert-pins", "ledger-rules", "constraints",
"docs-lock", "flake-update", "repin")
"docs-lock", "flake-update", "repin", "hf-meta")


class BumpError(Exception):
Expand Down Expand Up @@ -296,10 +298,61 @@ def hook_repin(root, ctx):
ctx["touched"].update(path for path, *_ in changes)


HF_FIXTURES = "tests/fixtures/hf"


def _hf_get(url, rng=None):
"""console/model_opt.py's get (bytes, None on 401 or 404) through pinlib.http, so
PINLIB_FIXTURES serves it in tests."""
try:
return http.request(url, {"Range": "bytes=%d-%d" % rng} if rng else None).body
except http.HTTPError as e:
if e.status in (401, 404):
return None
raise BumpError("hf-meta: %s" % e) from None


def hook_hf_meta(root, ctx):
"""Each saved metadata bundle (tests/fixtures/hf) that records a moved Hugging Face
repo at its old revision gets that repo's record as Hugging Face serves it at the
new one, read by console/model_opt.py's own repo_record; the other records stay."""
moved = {p["source"]["model"]: (old, new) for p, old, new in ctx["moves"]
if p.get("source", {}).get("kind") == "hf" and old and new and old != new}
d = os.path.join(root, HF_FIXTURES)
if not moved or not os.path.isdir(d):
return
keep, sys.dont_write_bytecode = sys.dont_write_bytecode, True # no __pycache__ in the bumped tree
try:
mo = oci.load_path("model_opt_bump", os.path.join(root, "console", "model_opt.py"))
finally:
sys.dont_write_bytecode = keep
for name in sorted(os.listdir(d)):
if not name.endswith(".json"):
continue
path = os.path.join(d, name)
with open(path, encoding="utf-8") as f:
bundle = json.load(f)
repos = bundle.get("repos") or {}
hit = False
for rid, (old, new) in sorted(moved.items()):
if (repos.get(rid) or {}).get("sha") != old:
continue
rec = mo.repo_record(_hf_get, rid, new, target=rid in (bundle.get("requested"), bundle.get("target")))
if rec is None or rec.get("sha") != new:
raise BumpError("hf-meta: Hugging Face has no %s at %s" % (rid, new))
repos[rid], hit = rec, True
ctx["notes"].append("hf-meta: %s/%s %s %s -> %s" % (HF_FIXTURES, name, rid, old[:8], new[:8]))
if hit:
with open(path + ".tmp", "w", encoding="utf-8") as f:
f.write(json.dumps(bundle, indent=1, sort_keys=True) + "\n")
os.replace(path + ".tmp", path)
ctx["touched"].add("%s/%s" % (HF_FIXTURES, name))


HOOK_FNS = {"lock": hook_lock, "readme-history": hook_readme_history, "render-latest": hook_render_latest,
"ppa-write": hook_ppa_write, "convert-pins": hook_convert_pins, "ledger-rules": hook_ledger_rules,
"constraints": hook_constraints, "docs-lock": hook_docs_lock, "flake-update": hook_flake_update,
"repin": hook_repin}
"repin": hook_repin, "hf-meta": hook_hf_meta}


# ---- known issues -------------------------------------------------------------------
Expand Down Expand Up @@ -509,7 +562,8 @@ def bump(root, group, reason=None, dry_run=False, resolved=None, run_tests=True,
continue
edits += edits_for(p, spans, cur, r)
pairs = apply_edits(root, tree, edits)
ctx = {"touched": {s.file for s, _ in edits}, "edits": edits, "pins": [m[0] for m in moves], "notes": notes}
ctx = {"touched": {s.file for s, _ in edits}, "edits": edits, "pins": [m[0] for m in moves], "notes": notes,
"moves": [(p, cur.get("v"), r.v) for p, _, cur, r in moves]}
for h in hooks:
if h not in HOOK_FNS:
raise BumpError("group %s: unknown hook %r" % (group, h))
Expand Down
145 changes: 112 additions & 33 deletions hack/pinlib/goldens.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,32 @@

A test that names a pinned value (a golden transcript, a recorded image list) moves
with the pin. Only tests/** is written; never a ledger, historical, arm or
eat-stock path, nor .github/. Idempotent: a second run with the same pairs
changes nothing, even when an old string is part of its new one (1.37 -> 1.37.1).
eat-stock path, nor .github/. A digest or revision also moves cut to its first 12
hex digits, as plans and audits print it. A pair leaves a test alone when
- another pin's site outside tests/ still holds the old string (two images at
one digest: the test may check the other one);
- the test names a kept record that still holds it (a ledger arm's or a
historical run's file: one no bumper writes, prose (.md) aside) and so
reads that record;
- the old string is a bare version (a site's value alone, no image name, no
key) and the test is code (.py, .sh or a #! script): there it is as likely
the test's own input, as in a version-compare case. In a recorded file (a
golden transcript, a fixture) it moves.
Prose and records that no test names hold nothing back. Idempotent: a second run
with the same pairs changes nothing, even when an old string is part of its new
one (1.37 -> 1.37.1).
"""

import os
import re
import subprocess

from . import classes

MASK = "\x00{%d}\x00"
BARE = re.compile(r"[A-Za-z0-9._+~-]+")
HEX = re.compile(r"(sha256:)?[0-9a-f]{40,64}")
SHORT = 12 # hex digits of a cut digest or revision


def substitute_text(text, pairs):
Expand All @@ -28,51 +44,114 @@ def substitute_text(text, pairs):
return masked


def test_files(root):
def _tracked(root):
try:
out = subprocess.run(["git", "-C", root, "ls-files", "-z", "tests"], capture_output=True, check=True).stdout
paths = [p for p in out.decode().split("\0") if p]
out = subprocess.run(["git", "-C", root, "ls-files", "-z"], capture_output=True, check=True).stdout
return sorted(p for p in out.decode().split("\0") if p)
except (OSError, subprocess.CalledProcessError):
paths = []
for d, _, fs in os.walk(os.path.join(root, "tests")):
paths += [os.path.relpath(os.path.join(d, f), root) for f in fs]
return sorted(p for p in paths if p.startswith("tests/") and classes.writable(root, p))
for d, ds, fs in os.walk(root):
ds[:] = [x for x in ds if x != ".git"]
paths += [os.path.relpath(os.path.join(d, f), root).replace(os.sep, "/") for f in fs]
return sorted(paths)


def _read(root, path):
try:
with open(os.path.join(root, path), encoding="utf-8") as f:
return f.read()
except (UnicodeDecodeError, FileNotFoundError, IsADirectoryError, NotADirectoryError):
return None


def test_files(root):
return sorted(p for p in _tracked(root) if p.startswith("tests/") and classes.writable(root, p))


def is_code(path, text):
"""A test script, as opposed to a recorded file (a golden, a fixture)."""
return path.endswith((".py", ".sh")) or text.startswith("#!")


def is_bare(s):
"""A version or tag on its own (v1.37.1-k3s1, release-1.1.0), not a digest,
a revision or a string with an image name or key around it."""
return bool(BARE.fullmatch(s)) and not HEX.fullmatch(s)


def site_texts(root):
"""Every string a registry pin's site outside tests/ holds now: each site's
whole match and each of its values."""
from . import registry
path = os.path.join(root, "pins", "registry.toml")
if not os.path.exists(path):
return set()
tree = registry.Tree(root)
_, _, spans_by = registry.all_pins(tree, registry.load(path))
out = set()
for spans in spans_by.values():
for s in spans:
if s.file.startswith("tests/"):
continue
out.add(s.text)
if s.match:
out.add(tree.read(s.file)[s.match[0]:s.match[1]])
return out


def live_pairs(root, pairs):
"""The pairs whose old string no file outside tests/ holds any more (prose and
recorded data aside). An old string still pinned elsewhere (a ledger arm, a
historical run) may be what a test checks, so it stays."""
from . import oci
texts = []
for p in oci.images_lock().scanned_files(root):
if p.startswith("tests/"):
continue
try:
with open(os.path.join(root, p), encoding="utf-8") as f:
texts.append(f.read())
except (UnicodeDecodeError, FileNotFoundError, IsADirectoryError):
"""The pairs whose old string no pin site outside tests/ holds any more, each
digest or revision also cut to its first 12 hex digits."""
held = site_texts(root)
out = []
for o, n in pairs:
if not o or o == n or o in held:
continue
blob = "\x00".join(texts)
return [(o, n) for o, n in pairs if o and o != n and o not in blob]
out.append((o, n))
if HEX.fullmatch(o) and HEX.fullmatch(n):
k = len("sha256:") + SHORT if o.startswith("sha256:") else SHORT
if o[:k] != n[:k]:
out.append((o[:k], n[:k]))
return out


def kept_record(root, path):
"""A file outside tests/ that no bumper writes (a ledger arm, a historical run),
prose (.md) aside."""
return not path.startswith("tests/") and not path.endswith(".md") and not classes.writable(root, path)


def substitute(root, pairs, paths=None):
"""Apply pairs to tests/** (or the given paths, still only under tests/); the
paths changed. Only pairs whose old string is gone from the rest of the repo."""
paths changed. See the module docstring for the pairs a test keeps."""
pairs = live_pairs(root, pairs)
changed = []
tests = {}
for p in paths if paths is not None else test_files(root):
if not p.startswith("tests/") or not classes.writable(root, p):
continue
full = os.path.join(root, p)
try:
with open(full, encoding="utf-8") as f:
text = f.read()
except (UnicodeDecodeError, FileNotFoundError, IsADirectoryError):
continue
new = substitute_text(text, pairs)
if p.startswith("tests/") and classes.writable(root, p):
text = _read(root, p)
if text is not None:
tests[p] = text
# the kept records that still hold each old string some test holds
wanted = {o for o, _ in pairs if any(o in t for t in tests.values())}
holders = {o: [] for o in wanted}
if wanted:
for p in _tracked(root):
if not kept_record(root, p):
continue
text = _read(root, p)
if text is None:
continue
for o in wanted:
if o in text:
holders[o].append(p)
changed = []
for p, text in sorted(tests.items()):
code = is_code(p, text)
mine = [(o, n) for o, n in pairs if o in text and not (code and is_bare(o))
and not any(h in text for h in holders.get(o, ()))]
new = substitute_text(text, mine)
if new != text:
full = os.path.join(root, p)
with open(full + ".tmp", "w", encoding="utf-8") as f:
f.write(new)
os.chmod(full + ".tmp", os.stat(full).st_mode)
Expand Down
2 changes: 2 additions & 0 deletions hack/pinlib/pindiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
r"(?<![0-9a-f])[0-9a-f]{40}(?![0-9a-f])", # git / Hugging Face revisions
r"(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])", # bare sha256
r"(?<![0-9a-f])[0-9a-f]{7,12}(?![0-9a-f])(?=…)", # a digest cut short in prose
r"sha256:[0-9a-f]{12}(?![0-9a-f])", # a digest cut to 12, as audits print it
r"(?<=@)[0-9a-f]{12}(?![0-9a-f])", # a revision or digest cut to 12, as plans print it
r"\d{4}-\d{2}-\d{2}", # ISO date
r"(?<!\d)\d{8}(?!\d)", # YYYYMMDD
r"(?<!\d)\d{9,11}(?!\d)", # epoch seconds (flake.lock lastModified)
Expand Down
10 changes: 6 additions & 4 deletions hack/substitute-goldens
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

A test that names a pinned value (a golden transcript, a recorded image list)
moves with the pin. Only tests/** is written: never a ledger, historical, arm or
eat-stock path, nor .github/. A pair applies only once no file outside tests/
holds its old string any more (one still pinned elsewhere may be what a test
checks). Idempotent: a second run changes nothing, even when an old string is
part of its new one (1.37 -> 1.37.1). hack/bump runs it after every bump.
eat-stock path, nor .github/. A digest or revision also moves cut to 12 hex
digits. A test keeps an old string that another pin's site still holds, that a
kept record it names still holds (a ledger run it reads), or that is a bare
version in test code (hack/pinlib/goldens.py, pins/README.md).
Idempotent: a second run changes nothing, even when an old string is part of its
new one (1.37 -> 1.37.1). hack/bump runs it after every bump.
--pairs FILE: a JSON list of [old, new]. Prints the paths changed.
"""

Expand Down
6 changes: 4 additions & 2 deletions packaging/check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ nfpm_verify() {
rpm -qpi /dist/${rpm}
rpm -qpR /dist/${rpm}
rpm -qplv /dist/${rpm}
rpm -K /dist/${rpm}
rpm -i /dist/${rpm}
# The package is unsigned: check its digests only. RPM 6 enforces signatures by
# default (Fedora 45's 6.1 fails a plain -K or -i on an unsigned package).
rpm -K --nosignature /dist/${rpm}
rpm -i --nosignature /dist/${rpm}
rpm -V k3d-gpu
${installed_checks}
rpm -e k3d-gpu
Expand Down
29 changes: 25 additions & 4 deletions pins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Until `pins/classes` exists, `hack/pinlib/classes.py` gives every path its class
| `hack/pins propose-amendment <row>` | a PENDING row's runner image pins under a dated amendment stub |
| `hack/bump <group>` | resolve, rewrite the sites, run the hooks, substitute goldens, run the group's tests, write the PR body and verdict |
| `hack/pin-only-diff <base> <head>` | is the diff only pin moves (else: changes-defaults) |
| `hack/substitute-goldens OLD=NEW...` | exact-string substitution in `tests/**` only |
| `hack/substitute-goldens OLD=NEW...` | exact-string substitution in `tests/**` only (see Goldens below) |

`hack/bump` writes the working tree it runs in; try a group in a scratch clone:

Expand Down Expand Up @@ -84,9 +84,30 @@ git clone -q . /tmp/k && /tmp/k/hack/bump telemetry --root /tmp/k --dry-run
A `[groups.<name>]` table: `tests` (suites `hack/bump` runs after the rewrite;
CI runs everything again on the PR), `hooks` (built-in steps after the rewrite,
in order: `lock`, `readme-history`, `render-latest`, `ppa-write`, `convert-pins`,
`ledger-rules`, `constraints`, `docs-lock`, `flake-update`, `repin`) and `outputs`
(files those hooks write; `hack/pin-only-diff` accepts any change there).
Coupled components go in one group, so they land in one PR.
`ledger-rules`, `constraints`, `docs-lock`, `flake-update`, `repin`, `hf-meta`) and
`outputs` (files those hooks write; `hack/pin-only-diff` accepts any change there).
Coupled components go in one group, so they land in one PR. `hf-meta` reads each
moved Hugging Face repo's record again at its new revision, in every saved metadata
bundle under `tests/fixtures/hf` that recorded it at the old one.

## Goldens

After the rewrite and the hooks, `hack/bump` replaces each exact old pin string
(a whole site match, a digest, a revision, a version or tag) with its new one in
`tests/**`; a digest or revision also moves cut to its first 12 hex digits, as
plans and audits print it (`sha256:<12>`, `@<12>`: `hack/pin-only-diff` counts
both as pin tokens). A test keeps the old string when

- another pin's site outside `tests/` still holds it (two images at one digest);
- the test names a kept record that still holds it (a file no bumper writes,
prose aside: a ledger arm's or a historical run's pin), so it reads that record;
- the old string is a bare version or tag (`v1.37.1-k3s1`, `release-1.1.0`) and
the test is code (`.py`, `.sh` or a `#!` script), where it is as likely the
test's own input. In a recorded file (a golden transcript, a fixture) it moves.

Prose and records that no test names hold nothing back. A test script that checks
a pinned value reads it from the file that pins it (the launcher, the Dockerfile,
`bench/competitors.lock`), so a bump never has to edit the script.

## Hold a pin

Expand Down
8 changes: 8 additions & 0 deletions pins/registry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ tests = ["tests/detect_test.sh"]

[groups.hf-console-fallback]
tests = ["tests/model_opt_test.sh"]
hooks = ["hf-meta"]
outputs = ["tests/fixtures/hf/*.json"]

[groups.convert-pins]
tests = ["tests/console_test.sh", "tests/model_opt_test.sh"]
Expand Down Expand Up @@ -504,6 +506,8 @@ derived = [
{ file = "console/model_opt.py", regex = 'o\.get\("vllm_version"\) or "(?P<v>[0-9.]+)"', from = "{v_bare}" },
{ file = "console/model_opt.py", regex = '^VLLM_[A-Z_]+ = "https://github\.com/vllm-project/vllm/blob/(?P<v>v[0-9.]+)/', from = "{v}", count = 3 },
{ file = "console/model_opt.py", regex = '"--language-model-only [^\n]*"https://github\.com/vllm-project/vllm/blob/(?P<v>v[0-9.]+)/vllm/engine/arg_utils\.py', from = "{v}" },
# the vLLM version the launcher hands the optimizer (agent_optimize)
{ file = "scripts/k3d-gpu", regex = 'device=\$\{AGENT_SEL%% \*\} vver=(?P<v>[0-9.]+) ', from = "{v_bare}" },
]

[[pin]]
Expand All @@ -522,6 +526,9 @@ sites = [
{ file = "console/moe_predict.py", ref = "intel/llm-scaler-vllm" },
{ file = "console/plan.py", regex = '"(?P<vllm>[0-9.]+)" if backend == "llm-scaler"' },
]
derived = [
{ file = "scripts/k3d-gpu", regex = 'llm-scaler\) vver=(?P<v>[0-9.]+) ;;', from = "{vllm}" },
]

[[pin]]
id = "gaudi-vllm"
Expand All @@ -539,6 +546,7 @@ sites = [
]
derived = [
{ file = "console/plan.py", regex = '^AGENT_VLLM_VERSION = \{"HPU": "(?P<v>[0-9.]+)"', from = "{vllm}" },
{ file = "scripts/k3d-gpu", regex = '\[ -z "\$\{AGENT_HPU:-\}" \] \|\| vver=(?P<v>[0-9.]+)', from = "{vllm}" },
]

[[pin]]
Expand Down
Loading
Loading