fix(artifacts): skip symlinks when sealing cache artifacts - #679
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review. WalkthroughCache artifact packaging now enumerates regular files and directories explicitly. Symlinks and unsupported entries are skipped with bounded diagnostics. Tests cover symlink handling, unusual filenames, and empty directories. Architecture and deployment documentation describe the packaging and extraction rules. ChangesCache artifact packaging
Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: ⚪ Minimal · up to The change skips derived symlink entries during artifact packaging while preserving required files and directories; no actionable merge-blocking risk remains beyond normal checks and review. Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
Packaging raised on the first symlink found under a cache root, so a worker whose FlashInfer cache held one never published that artifact at all: the publisher retried every 5s until MX_ARTIFACT_READY_TIMEOUT_SECS and gave up, and the JIT-compiled modules the artifact exists to share went with it. FlashInfer hits this on Blackwell, where each JIT module lookup relinks its trtllmGen_*_export include path into the cubin directory. Enumerate the archive members explicitly and hand tar that list via --null --no-recursion -T instead of letting it walk the tree. Symlinks are left out: engines rebuild them on demand (FlashInfer's ensure_symlink() replaces whatever sits at the path, so a link carried across pods would be deleted and recreated anyway), and neither the walk nor tar descends through a symlinked directory, so skipping one costs the link entry alone. A link resolving inside the cache root logs at debug, one that leaves the root or dangles logs a warning naming the paths, and neither fails the publish. An explicit member list rather than --exclude patterns: tar reads exclude patterns as globs, so a cache entry named like config[sm100] would slip past its own exclusion and land in the archive as a symlink member, which the target then rejects in _validate_tar_members after a full transfer. Listing members keeps the file-or-directory invariant on the source side, where it can be enforced. Signed-off-by: Zhongdongming Dai <zhongdongmin@nvidia.com>
9ef9d10 to
dbe8e87
Compare
|
/ok to test dbe8e87 |
Problem
Artifact packaging raised on the first symlink found under a cache root:
A user running Nemotron-3-Ultra on B200 (vLLM 0.27.1, disagg) hit this and read it as a harmless warning. It is not:
prepare_source()throws,PublisherThreadretries every 5s untilMX_ARTIFACT_READY_TIMEOUT_SECS(default 1800s) and gives up, so that worker never publishes its FlashInfer artifact at all — the JIT-compiled modules the artifact exists to share go with it, and every decode worker recompiles them itself. Weight transfer is unaffected (140 GB in 8.84s in their logs); this is only the artifact path.This is not an edge case on Blackwell. FlashInfer's trtllm-gen kernels ship as
prebuilt cubins plus headers stored content-addressed under a hash directory,
while generated
.cufiles include them from a fixed layout, sojit/fused_moe.pybridges the two with a symlink on every JIT module lookup.Any SM100 worker using trtllm-gen fused MoE hits it.
Fix
Enumerate archive members explicitly and hand tar that list, instead of letting
tar walk the tree:
Symlinks are left out of the list. They are derived state that engines rebuild:
FlashInfer's
ensure_symlink()removes whatever sits at the path (including areal directory, via
shutil.rmtree) and recreates the link, and it runs beforebuild_and_load()'s cache-hit check, so it executes in every process thattouches the op. A link carried across pods would be deleted and recreated
anyway.
Skipping a symlink costs the link entry alone. Neither
os.walk(followlinks=False)nor tar descends through a symlinked directory, so no subtree that tar would
otherwise have archived is lost.
Logging is graded and never fatal: a link resolving inside the cache root logs
at debug (its target is archived under its real path); a link that leaves the
root or dangles logs one warning naming the paths.
Why an explicit member list rather than
--excludetar reads exclude patterns as globs, so an entry whose name contains glob
metacharacters slips past its own exclusion:
That symlink would then reach the target and fail
_validate_tar_membersafter a full transfer, turning a source-side problem into a target-side one.
--exclude-fromfixes the metacharacter case but still cannot express a namecontaining a newline (
--nullonly applies to-T). An explicit member listis an allowlist: nothing that is not listed can enter the archive, whatever it
is named. That keeps the file-or-directory invariant on the source side, where
it can be enforced, and leaves the extraction path untouched — no new
validation code, no new attack surface.
Other approaches considered
tar -h(dereference). The bytes would be deleted by the target'sensure_symlink(), so it is pure waste. It also breaks on dangling links(tar exits 1) and on symlink cycles — GNU tar 1.35 segfaults on
a/loop -> ../aafter expanding 5625 levels.extraction side for zero benefit, since the target rebuilds the link itself.
Non-regular files (fifo, socket, device nodes) are skipped the same way. They
serve the same invariant: previously the source accepted them and the target
rejected them after a full transfer.
Testing
Replaces
test_tarred_p2p_artifact_transfer_rejects_symlinkwith five cases:internal symlink, external symlink, broken symlink, awkward member names
(
config[sm100].incand a name containing a newline, alongside symlinks withthe same shapes), and empty-directory preservation.
test_artifact_transfer.py,test_vllm_artifacts.py,test_sglang_artifacts.py,test_artifact_health_url.py: 119 passed.Also verified end to end against a reproduction of the reported layout (cubin
include tree under site-packages, cache under
0.6.16.post3/100a/generated/trtllm_export/fused_moe_trtllm_sm100/...): publishsucceeds with one warning naming the skipped link, and the target receives
cached_ops/fused_moe_trtllm_sm100/module.so, the emptycached_ops/tmp, andthe link's parent directory.
Pre-existing unrelated failures in
test_vmm_*(themodelexpress.vmm._alloc_extC extension is not built in this environment) reproduce identically with these
changes stashed.
Behavior change
Symlinks under a cache root are no longer an error for any artifact type. The
worst case is a target cache missing one derived entry, which degrades to a
recompile — these are all regenerable JIT caches. The current worst case is the
entire cache failing to publish, every time.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation