Преглед на файлове

staging: EXL3 live-cutover fixes — src from config, syntactic link resolution, ro-safe export placeholders

- config.toml: hf_cache_dir moved root-level (was silently absorbed into the
  last [[clusters]] table -> staging used ~/.cache/huggingface; dgx2's symlink
  to its own mirror made src==dst)
- dgx2: ~/.cache/huggingface repointed to the NAS (was self-symlink)
- hub_mirror.py: link_target() joins syntactically (composed-model shared-root
  rule); realpath() duplicated 147-175 GiB/node of weights into snapshot trees
- hub_mirror.py: export resolves symlinked repo-blob-layout mirror entries to
  real local blobs (weights hardlink locally, not NAS binds); skip-listed
  subtrees materialize mountpoint placeholders + per-file binds (runtime guard
  requires weight shards on a read-only filesystem); /model + /draft-exl3 back
  to read_only=true
- live verify: verify-only staging (131 links, 30 binds), clean_pages guard
  passed, decode 46.8 tok/s c1; 175+147 GiB duplicates removed from mirrors
Alexander Korolev преди 3 седмици
родител
ревизия
e586bd2416
променени са 3 файла, в които са добавени 91 реда и са изтрити 11 реда
  1. 37 0
      history/night-shifts/2026-09-27.md
  2. 4 1
      recipes/dsv41-flash-exl3-2x/recipe.toml
  3. 50 10
      src/staging/scripts/hub_mirror.py

+ 37 - 0
history/night-shifts/2026-09-27.md

@@ -242,3 +242,40 @@ without re-copying, and rebuilds ranges when they don't verify. E2E on
 dgx1 (synthetic fixture, rank1/tp4): pack, verify-only, adoption,
 corrupt-one-row rebuild — all green; sparse-ness confirmed (528 K logical,
 147 K allocated).
+
+## EXL3 lane cutover — declarative staging live, 46.8 tok/s decode c1
+
+Mia stopped (bench served), EXL3 brought up on `2nodes-dac` via the migrated
+`[[staging]]` recipe. Three real bugs surfaced on the live cutover and were
+fixed in `hub_mirror.py` / config / recipe:
+
+1. **`hf_cache_dir` misplacement in config.toml** — it sat *after* the
+   `[[clusters]]` headers and was parsed into the cluster table, silently
+   ignored: staging fell back to `~/.cache/huggingface`. On dgx1 that is a
+   symlink to the NAS (looked fine), on dgx2 to the node mirror itself
+   (src==dst: page15 vanished, export went full-local, manifest empty).
+   Key moved root-level; dgx2's symlink repointed to the NAS.
+2. **`realpath()` on source links** broke the composed-model shared-root
+   rule: with a symlinked src the blob targets landed "outside" and were
+   duplicated into the snapshot tree as real files (147–175 GiB/node of
+   dead weight, re-copied every serve). Fixed with syntactic link
+   resolution (`normpath(dirname+readlink)`), same technique as
+   composed_model mounts. The 175+147 GiB of duplicates removed.
+3. **Export materialization** got two fixes: (a) the mirror's older
+   repo-blob layout behind symlinks is now resolved to the real local blob
+   (weights hardlink locally instead of falling to NAS binds); (b)
+   skip-listed subtrees (page15) get mountpoint placeholders + per-file
+   binds, which is what the runtime's `clean_pages_advised` guard requires:
+   weight shards on a **read-only** filesystem (`statvfs ST_RDONLY`), so
+   the /model and /draft-exl3 binds went back to `read_only = true`.
+
+Live verification (dgx1+dgx2): staging verify-only (131 links, 30 page15
+binds), guard passed (`files=56` advised), page15 visible, /model ro,
+`/v1/completions` clean, **decode 46.8 tok/s c1** (2x the recorded
+17–33 tok/s band — the localization goal achieved). Disk: dgx1 865→690 GB
+used after cleanup (duplicates + `/var/tmp/engram-local` 49 G stale ×2 +
+Mia packed shards on dgx1/2, regenerable via `[[staging]]`).
+
+Side effect worth remembering: **the packed-Mia lanes now repack
+declaratively on their next serve** (~8 min/node) instead of carrying the
+state on disk.

+ 4 - 1
recipes/dsv41-flash-exl3-2x/recipe.toml

@@ -108,7 +108,10 @@ export = [
 ]
 
 # /model and /draft-exl3: regular-file views of the staged export trees
-# (single dir mounts; page15 overlays arrive via the export manifest).
+# (hardlinks onto mirror blobs). read_only = true is REQUIRED: the runtime
+# guards weight shards against a writable filesystem. The skipped page15
+# table arrives as per-file binds from the export manifest onto mountpoint
+# placeholders that staging creates inside the export trees.
 [[extra_mounts]]
 host = "/home/kilork/hf-local/model"
 container = "/model"

+ 50 - 10
src/staging/scripts/hub_mirror.py

@@ -183,6 +183,16 @@ class Stager:
         "outside" the tree and turn them into duplicated real files."""
         return os.path.normpath(os.path.join(os.path.dirname(p), os.readlink(p)))
 
+    def resolve_local(self, path):
+        """Normalize a candidate mirror path: follow one symlink hop (old
+        repo-blob layout) as long as the resolved path stays inside the
+        mirror; None if it escapes or the link is dangling."""
+        if os.path.islink(path):
+            path = os.path.realpath(path)
+            if not path.startswith(self.dst.rstrip(os.sep) + os.sep):
+                return None
+        return os.path.normpath(path)
+
     def stage_links(self, links):
         for p, rel in links:
             target = self.link_target(p)
@@ -294,32 +304,62 @@ class Stager:
                     target = self.link_target(p)
                     if not os.path.exists(target):
                         fail(f"dangling link in source snapshot: {p}")
-                    mirror = os.path.normpath(
-                        os.path.join(self.dst, os.path.relpath(target, self.src)))
+                    mirror = os.path.join(self.dst, os.path.relpath(target, self.src))
                 else:
                     target = p
-                    mirror = os.path.normpath(
-                        os.path.join(self.dst, os.path.relpath(p, self.src)))
+                    mirror = os.path.join(self.dst, os.path.relpath(p, self.src))
                 dst_entry = os.path.join(export_root, rel)
                 entries += 1
-                if os.path.isdir(mirror):
+                # the mirror tree may use either hub layout (hub/blobs/xx/sha)
+                # or the older repo-blob layout (models--X/blobs/sha) behind a
+                # symlink — normalize to a real local file if it resolves
+                # inside the mirror.
+                local = self.resolve_local(mirror)
+                # skip-listed subtrees (page15) must never be materialized as
+                # hardlinks: they were not staged, so which nodes happen to
+                # hold them is history. Emit them uniformly as per-file
+                # binds (local copy when verified, else source) — the export
+                # tree is then identical across nodes.
+                if any(part in self.skip for part in rel.split(os.sep)):
+                    if os.path.exists(dst_entry) or os.path.islink(dst_entry):
+                        os.remove(dst_entry)  # stale hardlink from an older pass
+                    if local is not None and os.path.isfile(local) \
+                            and (self.verify != "sample-hash"
+                                 or sample_hash(local) == sample_hash(target)):
+                        bind_src = local
+                    else:
+                        bind_src = target
+                    os.makedirs(os.path.dirname(dst_entry), exist_ok=True)
+                    if not os.path.exists(dst_entry):
+                        # mountpoint placeholder: the per-file bind lands on
+                        # it (runc cannot create it when the export dir is
+                        # mounted read-only, and the runtime guards its
+                        # weight shards behind a read-only filesystem).
+                        open(dst_entry, "wb").close()
+                    manifest_lines.append(f"{bind_src}\t{os.path.join(container, rel)}")
+                    manifest += 1
+                    continue
+                if local is not None and os.path.isdir(local):
                     os.makedirs(dst_entry, exist_ok=True)
                     continue
-                if os.path.exists(mirror) and not os.path.islink(mirror):
+                if local is not None and os.path.isfile(local):
+                    if (self.verify == "sample-hash"
+                            and sample_hash(local) != sample_hash(target)):
+                        fail(f"mirror blob {local} no longer matches source for {p}")
                     if (os.path.exists(dst_entry) and not os.path.islink(dst_entry)
-                            and os.path.samefile(mirror, dst_entry)):
+                            and os.path.samefile(local, dst_entry)):
                         links += 1
                         continue
                     os.makedirs(os.path.dirname(dst_entry), exist_ok=True)
                     if os.path.exists(dst_entry) or os.path.islink(dst_entry):
                         os.remove(dst_entry)
                     try:
-                        os.link(mirror, dst_entry)
+                        os.link(local, dst_entry)
                     except OSError as e:
-                        fail(f"hardlink {dst_entry} -> {mirror}: {e}")
+                        fail(f"hardlink {dst_entry} -> {local}: {e}")
                     links += 1
                 else:
-                    # source-only entry (skip-listed or never staged): bind it
+                    # source-only entry (never staged outside skip — bind source)
                     manifest_lines.append(f"{target}\t{os.path.join(container, rel)}")
                     manifest += 1
         print(f"STAGED export {name} entries={entries} links={links} "