Skip to content

[wip]fix(kvaware+kvd): two bugs blocking KV-aware routing + kvd together on SGLang, with image, docs and repro - #59

Closed
dorado269 wants to merge 9 commits into
mainfrom
yihou.dev.glm5.2.mxfp4.kvaware.kvd.pr
Closed

[wip]fix(kvaware+kvd): two bugs blocking KV-aware routing + kvd together on SGLang, with image, docs and repro#59
dorado269 wants to merge 9 commits into
mainfrom
yihou.dev.glm5.2.mxfp4.kvaware.kvd.pr

Conversation

@dorado269

Copy link
Copy Markdown
Collaborator

Two bug fixes found while bringing up KV-aware routing + kvd (tiered KV offload) together on the SGLang engine, plus the deployment image and the operator documentation for that configuration, plus a reproduction kit.

Verified end-to-end on GLM-5.2-MXFP4, two-node PD over Mooncake RDMA, DP-attention 8/8 on both legs, both features on.


The two fixes

fix(net)free_tcp_port_block returned the same base to every caller

It scanned downward from a fixed start (ip_local_port_range.low - count) and released the probe sockets before returning, so two engines launched on one host picked the same block deterministically. The second leg died with:

zmq.error.ZMQError: Address already in use (addr='tcp://*:32765')

Reached only from the kv-aware path (sglang/worker.py:77, when enable_kv_events is set and SGLang binds one KV-event publisher per DP rank at base + rank).

The reservation cannot be made exclusive — the probe binds 127.0.0.1:P while the real listener binds 0.0.0.0:P (zmq tcp://*). Two alternatives were tried and rejected against live MVPs:

  • Hold the reservation until the child binds → the child then fails with errno 98; we would be locking out our own subprocess.
  • 0.0.0.0 + SO_REUSEADDR → a second probe can still take the same port, so it buys nothing.

So the fix removes the determinism instead: 64 random bases in the same sub-ephemeral window, then the original exhaustive scan as fallback.

Blast radius: exactly one caller in the repo. vLLM and ATOM use the other function, free_tcp_port(), which is untouched.

fix(kvd) — bind-mounted L3 silently lost O_DIRECT

For a bind mount findmnt reports the source with the bind subpath in brackets:

/dev/md0[/mnt/nvme-raid/kvd-long]

That whole string went to lsblk, which answers "not a block device". The failure is silent: classification falls back to buffered I/O even on hardware that qualifies for O_DIRECT — i.e. exactly the normal deployment, an L3 directory bind-mounted into the engine container.

Blast radius: storage_classify is private to the kvd daemon, but the daemon is shared by all three engines, so vLLM's kvd L3 path benefits identically. Second-order effect checked deliberately: mount_source is also compared against /proc/mounts (src == source) for the NFS nconnect/rsize/wsize probes. /proc/mounts never carries the bracket form, so stripping moves that comparison from always fails to can succeed — a fix there too, not a regression.

Both fixes have tests that fail on the pre-fix code

git checkout <base> -- infera/common/net.py infera/kvd/storage_classify.py
pytest tests/unit/common/test_net_port_block.py tests/unit/kvd/test_storage_classify.py -q
# 3 failed, 48 passed        <- fixes reverted
# 51 passed                  <- fixes present

A trap worth flagging for reviewers: the first version of the storage_classify test passed on the broken code, because the fake _run returned success for lsblk regardless of its argument. It had to be taught the real contract (real lsblk exits non-zero on a bracketed target). If you extend these tests, revert the fix and confirm they go red.

Full unit suite, differential, same image: main = 83 failed / 965 passed / 58 errors; this branch = 83 failed / 971 passed / 58 errors. Identical failures, +6 passes (the new tests). The 83/58 are pre-existing container-environment limitations present on main, not introduced here.


build(sglang) — the deployment image

deploy/docker/Dockerfile.sglang.kvaware-kvd layers on Dockerfile.sglang rather than duplicating a 78.6 GB build — the existing image already contains engine + wrapper + kvd daemon + router + statctl, and one image runs all four roles.

What it adds is the contract:

  • a build-time self-check that imports InferaKvdBackend, wire_infera_kvd_backend, attach_to_radix_cache, statctl, and asserts free_tcp_port_block no longer returns a fixed base. It discriminates — 10 distinct bases post-fix, 1 pre-fix. A build that loses the kvd adapter (bad merge, or a base bump that moves HiCacheStorage) now fails at build time instead of starting fine and serving with no L3 at all.
  • PYTHONHASHSEED=0 baked in — for kvd this is a correctness requirement, not tuning: unset it is random per process, the two PD legs hash the same prompt differently, and every restart orphans the whole L3 cache.
  • a digest-pinnable base arg, because the unified-Mooncake rebuild is exactly the layer you don't want silently swapped.

Docs

docs(serving) — new manual/serving/kvaware_kvd_operations.md: which settings are experiment-only and what production should use instead, the recommended all-switches-on config, how to verify each switch actually took effect (with a negative case for each — all three features fail silently by degrading to a slower-but-correct path), and which numbers are tunable with recommended values and which direction to move them.

One trap documented there: --kv-overlap-weight defaults to 1.0 and the per-role weights fall back to it, so a PD deployment that doesn't set them explicitly behaves nearly round-robin and looks like kv-aware "does nothing".

docs(kvd)⚠️ this reverses #26 / 29a69ca ("kvd offload is vLLM-only"), so please sanity-check me here @jiejingzhangamd. The reasoning:

  • InferaKvdBackend (a SGLang HiCacheStorage backend) has been in the tree since Infera v0.1.0 — it predates the doc saying it doesn't exist.
  • The likely reason for that doc is that CI's kvd e2e covers only vLLM (tests/e2e/pd_mixed/vllm/test_mixed_kvd.py; there is no SGLang equivalent), so the path was untested rather than absent.
  • This PR tested it on hardware (below).

AIC GPU-Direct remains vLLM-only; SGLang reads via the daemon's POSIX path. Happy to drop this commit if you'd rather gate the doc change on a SGLang kvd e2e case landing first.


Hardware verification

Image built from this branch, sha256:c9292019…, same digest on both nodes. 8× MI355X per node, ionic RoCE, all 8 rails active.

Test Result
Correctness (temp=0 factual probe) 4/4, coherent
conc=32, 128 req, ISL/OSL 1024 128/128 needle correct, 0 HTTP errors, 128/128 finish=stop, 3.1 req/s
conc=128, 512 req 502/512 needle correct, 0 HTTP errors, 6.8 req/s

conc=128's 10 non-clean responses are all finish=length at exactly the 1024-token cap — the run-on mode of a harness that sends no chat template and suppresses EOS for throughput. The split is clean: 502 good at finish=stop median 149 tokens, 10 bad at exactly 1024. No KV-corruption signature at either concurrency.

kvd is shown to actually serve, not merely to be wired

This is the part that is easy to fake, so it is worth spelling out. During the stress tests kvd wrote 32 GB and read nothing (gets_total=0) — SGLang's in-GPU radix cache serves repeated prefixes without ever touching L3. A passing test proves nothing here, and neither does a latency win (in earlier work a reuse phase ran 2.7× faster with kvd's counters flat at zero).

The discriminating check is restart-and-replay — restarting the engine empties the GPU cache while the daemon and its L3 survive:

gets hits sets misses
before restart 0 0 18272 0
after restart 102 102 18272 (unchanged) 0

Reads with no new writes on an empty GPU cache. That reuse could only have come from L3.


Reproduction kit

kvaware_kvd_pr.packup_20260731/ — spec with criteria-vs-result, ordered reproduce steps, environment (hardware / drivers / fabric / image digests), per-patch what-why-how-context, the Dockerfiles, every script verbatim, raw JSON results, and logs.

Verified self-contained: a fresh clone at 8692fb4 plus these patches produces a tree hashing identical to this branch's HEAD.

notes.md records the wrong turns, including two general traps: a docker save | gzip that was silently truncated yet passed gzip -t (verify the tar, not the gzip), and a stress classifier that reported CORRUPT_REASONING for a correct answer because GLM-5.2 legitimately reasoned in Chinese.


Known gaps (deliberate, stated so nothing reads as more than it is)

  • No kvd latency benefit observed. kvd demonstrably served 102 blocks; it did not demonstrably make anything faster at this scale.
  • The overlap weights (20.0 / 2.0) were used but not validated — one prefill and one decode worker means the scorer never had a choice to make. They are documented as a starting point, not benchmarked optima.
  • No SGLang kvd e2e case added. That is what would permanently settle the doc question above.
  • 854ebf7 (long-context Mooncake wait_event fix) is deliberately not included — it lives on another branch and is tracked separately.

🤖 Generated with Claude Code

@jiejingzhangamd

Copy link
Copy Markdown
Collaborator

better remove
image
minimize code change

dorado269 and others added 8 commits August 1, 2026 05:22
free_tcp_port_block() scanned downward from a fixed base (ip_local_port_range
low - count) and released the probe sockets before returning. Two engines
started on one host therefore picked the *same* base deterministically.

This is reached only from the kv-aware path (worker.py, when enable_kv_events
is set), where SGLang binds one KV-event publisher per DP rank at base+rank.
The PD prefill and decode legs of a pair on one host both scanned from the
same place, both saw the same block free, and the second leg died with:

    zmq.error.ZMQError: Address already in use (addr='tcp://*:32765')

Two alternatives were tried and rejected against live MVPs:

  * Hold the reservation until the child binds. The probe binds 127.0.0.1:P
    while the real listener binds 0.0.0.0:P (zmq `tcp://*`); holding it makes
    our own subprocess fail with errno 98.
  * 0.0.0.0 + SO_REUSEADDR reservation. A second probe can still take the same
    port, so the reservation is not exclusive and buys nothing.

The reservation fundamentally cannot be exclusive, so remove the determinism
instead: try 64 random bases in the same sub-ephemeral window, then fall back
to the original exhaustive downward scan so we never fail while a block is
still free.

tests/unit/common/test_net_port_block.py covers: the block is free and
contiguous, it sits below the ephemeral range, repeated calls do not all
collide, and count<=1 delegates to free_tcp_port(). The collision test fails
on the pre-fix code (10/10 identical bases).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: yihou <yihou@amd.com>
For a bind mount findmnt reports the source with the bind subpath appended in
brackets:

    /dev/md0[/mnt/nvme-raid/kvd-long]

_findmnt() returned that whole string and the caller handed it to lsblk, which
answers "not a block device" and makes the rotational/queue probe fail. The
failure is silent: classification falls back to buffered I/O even on hardware
that qualifies for O_DIRECT.

This hits exactly the normal kvd deployment — an L3 long-tier directory
bind-mounted into the engine container — so a container-hosted kvd would never
use O_DIRECT on its NVMe tier, with no error to say why.

Keep only the part before '[', which is the device.

Two cases added to tests/unit/kvd/test_storage_classify.py. The fake `_run`
had to be taught the real lsblk contract (non-zero exit on a bracketed target);
without that the pre-fix code passes the new tests vacuously.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: yihou <yihou@amd.com>
The manual said KV-Cache Offload is "vLLM only (for now)" and the feature
matrix marked SGLang 🚧. Both are stale: infera has shipped
`InferaKvdBackend` (a SGLang `HiCacheStorage` backend) in
infera/engine/sglang/kvd_adapter.py, wired by --infera-kvd-socket.

Verified on hardware (GLM-5.2-MXFP4, two-node PD over Mooncake RDMA,
DP-attention on both legs): the backend connects, serves, and survives an
engine restart — a replay after restart added 170 hits with zero new sets,
i.e. the reuse came from L3 and not from the in-GPU radix cache.

Adds a SGLang section covering the parts that are not obvious from the vLLM
instructions, each verified against source:

  * --hicache-size (absolute GB) instead of --hicache-ratio. The default
    ratio 2.0 sizes the host pool off max_total_num_tokens and asked for
    355 GB *per DP rank* on a 1.7B model.
  * ratio < 1.5 silently disables L3 prefetch
    (hicache_validate.HICACHE_RATIO_DANGER_THRESHOLD).
  * On a PD decode leg kvd requires KV events ON: the decode leg forces
    disable_radix_cache, SGLang rejects that with --enable-hierarchical-cache,
    and infera only appends --disaggregation-decode-enable-radix-cache when
    kv-events are enabled and the backend is Mooncake (args.py). So
    --no-enable-kv-events on a decode leg silently disables kvd too.
  * --infera-kvd-socket fails fast (5 s probe, kvd_wiring._probe_kvd) rather
    than serving with a dead cache backend.
  * A latency win alone does not prove kvd served anything — SGLang's in-GPU
    radix cache serves a repeated prefix without touching L3. Read the daemon
    counters; restart to attribute a hit to L3.

AIC GPU-Direct remains vLLM-only; SGLang reads via the daemon POSIX path.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: yihou <yihou@amd.com>
Dockerfile.sglang already builds everything this deployment needs — engine,
infera wrapper, `infera.kvd` daemon, router, statctl — so this layers on it
rather than duplicating a ~78 GB build.

What it adds is the contract:

  * A build-time self-check that imports InferaKvdBackend,
    wire_infera_kvd_backend, attach_to_radix_cache and statctl, and asserts
    free_tcp_port_block no longer returns a fixed base. A build that loses the
    kvd adapter (bad merge, or a base bump that moves HiCacheStorage) fails
    here instead of starting fine and serving with no L3 at all. Verified the
    assertion discriminates: 10 distinct bases post-fix, 1 pre-fix.
  * PYTHONHASHSEED=0 baked in. For kvd this is a correctness requirement, not
    a tuning knob: unset, it is random per process, the two PD legs hash the
    same prompt differently, and every restart orphans the whole L3 cache.
  * The ionic Mooncake defaults from the verified GLM-5.2 two-node PD run
    (MC_GID_INDEX=1, MC_DISABLE_HIP_TRANSPORT=1,
    MOONCAKE_DISABLE_HIP_DMABUF=1), overridable at docker run.
  * A digest-pinnable base arg, because the unified Mooncake rebuild is
    exactly the layer you do not want silently swapped.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: yihou <yihou@amd.com>
The feature pages give the flags. This page covers what an operator actually
needs and could not get from them:

  * Experiment-only settings and what to use instead. The reproduction scripts
    carry MC_FORCE_TCP=1, an 8 GB hicache, an L3 tier on /tmp, and a manual
    libionic docker cp — every one of which is deliberately wrong for
    production, for a reason worth writing down (e.g. /tmp is usually tmpfs,
    so the "disk" tier is RAM competing with the RAM tier).
  * A recommended all-switches-on configuration for the three roles.
  * How to verify each switch took effect, with the specific log strings, and
    a negative case for each. All three features fail silently by degrading to
    a slower-but-correct path, so "it works" is not evidence. In our own run
    the reuse phase was 2.7x faster with kvd's counters at ZERO — the win was
    the in-GPU radix cache. The check that attributes reuse to kvd is a
    restart-and-replay: hits climb, sets do not.
  * Which numbers are tunable, their defaults, recommended values, and which
    direction to move them. Notably --kv-overlap-weight defaults to 1.0 and
    the per-role weights fall back to it, so a PD deployment that does not set
    them explicitly behaves nearly round-robin and looks like kv-aware "does
    nothing".
  * A "what we did not measure" section, so the recommended weights are not
    mistaken for benchmarked optima: no concurrency, one prefix, no observed
    latency win from kvd.

Every log string and flag default cited here was checked against source.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: yihou <yihou@amd.com>
Self-contained reproduction kit for the final clean run: the five commits as
patches, every script verbatim, the captured results, and the logs.

The run: GLM-5.2-MXFP4, two-node PD over Mooncake RDMA (real RDMA, not TCP),
DP-attention 8/8 on both legs, KV-aware routing and kvd both on, on an image
built from this branch's Dockerfile.sglang.kvaware-kvd.

  T1 correctness  4/4, coherent
  T2 conc=32      128/128 needle correct, 0 errors, 128/128 finish=stop, 3.1 req/s
  T3 conc=128     502/512 needle correct, 0 errors, 6.8 req/s

T3's 10 non-clean responses are all finish=length at exactly the 1024-token cap
— the run-on mode of a harness that sends no chat template and suppresses EOS
for throughput. Not a KV defect; the conc=128 gate is throughput without errors.

kvd is shown to actually serve, not merely to be wired: during the stress tests
it wrote 32 GB and read nothing (gets=0), because SGLang's in-GPU radix cache
serves repeats without touching L3. Restarting the engine empties that cache
while the daemon survives; replaying the same prefixes then gives 102 gets /
102 hits / 0 misses with sets_total unchanged. Reads with no new writes on an
empty GPU cache is the only clean attribution.

The kit is verified end to end: a fresh clone at 8692fb4 plus the five patches
produces a tree hashing identical to the branch HEAD, 51 tests pass, and
reverting just the two source files fails 3 of them.

notes.md records what went wrong, including two traps worth keeping: a
docker save | gzip that was silently truncated but passed `gzip -t` (verify the
tar, not the gzip), and a classifier that reports CORRUPT_REASONING for a
correct answer because GLM-5.2 reasoned in Chinese.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: yihou <yihou@amd.com>
Walked the packup skill's completeness checklist against the folder and found
three items genuinely missing:

  * §2 (purpose + spec) — the success criteria lived scattered in README with
    no criterion-by-criterion verdict. Adds spec.md: the ask as given, each of
    the 7 deliverables with its bar and the measured result, and the operator's
    2026-07-31 refinement of the conc=128 bar (throughput without errors; the
    finish=length tail is the no-chat-template / EOS-suppressed run-on mode,
    not a KV defect). Also records what was ruled out of scope (854ebf7,
    GPU-direct).
  * §6 (debug/fix outcomes) — the patches had no what/why/how/context notes.
    Adds patches/README.md, one section per patch, including the two rejected
    alternatives behind 0001, the test that initially passed on the broken code
    in 0002, the blast radius of each fix (0001 has exactly one caller; vLLM and
    ATOM use free_tcp_port, untouched), and an explicit flag that 0003 reverses
    29a69ca and should be confirmed with its author in review.
  * §4 (software env) — the Dockerfiles were referenced by repo path only, so
    the packup was not self-contained. Copies Dockerfile.sglang.kvaware-kvd,
    Dockerfile.sglang and the two build/entrypoint scripts into dockerfiles/.

Also checked and recorded as clean: no file > 4 MB, no secret values (the three
grep hits in the engine logs are SGLang's server_args dump — ssl_keyfile_password=None,
..._token=None), and every original left in place (work.kvaware_kvd,
work.kvaware_kvd.packup_20260730, kvaware.kvd.packup.better, and the remote
workspace under /mnt/vast all intact — this packup only ever copied).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: yihou <yihou@amd.com>
The `lint` job was failing on four hooks: trailing-whitespace, end-of-file-fixer,
ruff-format and ruff-check.

Almost all of it is whitespace. Two changes are worth a second look before this
is dismissed as noise, since both touch files that are evidence rather than
source:

  * the .patch files lose the single space on blank context lines. Checked that
    this does not break them — `git apply` reconstructs blank context lines, and
    a stripped patch still applies with the correct result.
  * results/raw/*.json gain a trailing newline. Content is untouched; the diff is
    "\ No newline at end of file" only.

The one non-mechanical fix is E402 in scripts/stress_capture.py: `import os` sat
mid-file next to the IDX-replay block. Hoisted into the top import block, which
is where ruff's isort ordering wants it anyway.

Rebased onto main first — the branch was 39 commits behind.

Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
@jiejingzhangamd
jiejingzhangamd force-pushed the yihou.dev.glm5.2.mxfp4.kvaware.kvd.pr branch from 52d7119 to 5a4dfba Compare August 1, 2026 05:25
@jiejingzhangamd

Copy link
Copy Markdown
Collaborator

@yihou heads up — I force-pushed this branch (52d711955a4dfba2). If you have unpushed local work on it, rebase it onto the new head before doing anything else; --force-with-lease protects what was on the remote, not what is only on your machine.

What changed, and nothing else:

Rebased onto main. The branch was 39 commits behind. All 7 commits replayed with no conflicts, authorship untouched.

Fixed the lint job (one new commit). Four hooks were failing — trailing-whitespace, end-of-file-fixer, ruff-format, ruff-check. Almost all whitespace, but two of them touch files that are evidence rather than source, so they were checked rather than waved through:

  • the .patch files lose the single space on blank context lines. Verified this does not break them: git apply reconstructs blank context lines and a stripped patch still applies with the correct result.
  • results/raw/*.json gain a trailing newline. Content untouched — the diff is \ No newline at end of file only.

The one real fix is E402 in scripts/stress_capture.py: import os sat mid-file next to the IDX-replay block, hoisted into the top import block.

Added the missing DCO sign-off — please read this part. The dco job was failing on all 7 commits. .github/workflows/dco.yml requires the author's email to appear in the sign-off, so signing them under my name would have kept the check red with a different message. Each commit is therefore signed off as its own author — yours carry Signed-off-by: yihou <yihou@amd.com>.

I did this at @kzjeef's instruction, on the statement that you had authorised it. Flagging it explicitly because a DCO sign-off is a certification made in your name, and it should not appear in the history without you knowing it was added by someone else's rebase. If you did not authorise it, say so and I will strip the trailers so you can sign them yourself — for the record that is just:

git fetch && git checkout yihou.dev.glm5.2.mxfp4.kvaware.kvd.pr
git rebase --signoff origin/main && git push --force-with-lease

The trees are byte-identical to before the sign-off; only trailers were added.

Still outstanding: REVIEW_REQUIRED.

@jiejingzhangamd jiejingzhangamd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the ~40 lines of behaviour change (infera/common/net.py, infera/kvd/storage_classify.py); the remaining ~10k lines are the packup archive. Both diagnoses are convincing and the tests are good — 4 new for the port block, 50 for storage classification including two bind-mount cases. One blocking issue.

Blocking — free_tcp_port_block crashes on a tuned ip_local_port_range

highest = low - count
randomised = [random.randint(1024, highest) for _ in range(_PORT_BLOCK_TRIES)]

random.randint raises when highest < 1024, which happens whenever the host's ephemeral range starts at or near 1024. net.ipv4.ip_local_port_range = 1024 65535 is a real high-concurrency tuning, not a hypothetical.

Verified against both versions with the range faked to 1024 65535:

main (original) -> RuntimeError: could not find 8 contiguous free TCP ports
PR  (randomised)-> ValueError: empty range in randrange(1024, 1017)

So it is a regression in two ways. The old code's range(low - count, 1024, -1) was simply empty and fell through to the deliberate RuntimeError, which names the problem. The new code raises ValueError out of random before probing a single port — on a host where the old code would also have failed, but legibly.

Suggest guarding the window and keeping the existing failure path:

highest = low - count
if highest < 1024:
    raise RuntimeError(
        f"no room for a {count}-port block below the ephemeral range "
        f"(ip_local_port_range starts at {low})"
    )

Worth a test — the existing four all run against the real /proc value, so this path cannot be hit in CI.

Non-blocking — find("[") misses one bind-mount shape

bracket = source.find("[")
if bracket > 0:

> 0 rather than >= 0 is right, and I assume deliberate: it stops an IPv6 source like [2001:db8::1]:/export from being truncated to nothing. Worth a comment saying so, since it reads like an off-by-one.

It does leave one case: an IPv6 NFS source that is bind-mounted, [2001:db8::1]:/export[/sub], keeps its subpath and hits the same "not a block device" path this PR fixes. rfind handles that and is identical everywhere else:

/dev/md0[/mnt/nvme-raid/kvd-long]   find -> /dev/md0                      rfind -> /dev/md0
[2001:db8::1]:/export               find -> unchanged (correct)           rfind -> unchanged
[2001:db8::1]:/export[/sub]         find -> unchanged (subpath kept)      rfind -> [2001:db8::1]:/export

Marginal, since NFS classifies as buffered anyway — but it is a one-character change.

Non-blocking — the 64 random probes can repeat

[random.randint(...) for _ in range(64)] samples with replacement, so some of the 64 tries are wasted on bases already probed. random.sample(range(1024, highest + 1), k=min(64, highest - 1023)) would not, and would also make the empty window above impossible to express. Only matters under contention, which is exactly when the tries are worth the most.

Checked and fine

  • Fork safety. The failure this fixes is two engines starting concurrently on one host, so it matters whether forked children draw the same sequence — CPython reseeds random on fork, and two children produced different picks in a direct test. Not an issue.
  • The exhaustive fallback still starts deterministically at highest. If all 64 random tries fail, two callers re-converge — but at that point the host is nearly out of blocks and the RuntimeError is imminent regardless.
  • Reservation is loopback-only and released before returning. The docstring explains why this cannot be exclusive; the reasoning holds.

Note on the branch

I rebased this onto main (was 39 commits behind), fixed the lint job, and added the DCO sign-off — see the comment above for exactly what changed and how to undo the sign-off if it was applied in error.

The randomised scan sampled `random.randint(1024, low - count)` without checking
that the window is non-empty. On a host whose ephemeral range starts at or near
1024 — `net.ipv4.ip_local_port_range = 1024 65535` is a real high-concurrency
tuning, not a hypothetical — `low - count` falls below 1024 and the call raises
out of `random` before probing a single port.

That regressed the failure twice over. Such a host has no room for a contiguous
block either way, but the original code's `range(low - count, 1024, -1)` was
simply empty and fell through to the deliberate RuntimeError, which names the
problem. Compared with the range faked to `1024 65535`:

    before this branch   RuntimeError: could not find 8 contiguous free TCP ports
    on this branch       ValueError: empty range in randrange(1024, 1017)
    after this commit    RuntimeError: no room for a 8-port block below the
                         ephemeral range (ip_local_port_range starts at 1024)

Also switches the 64 probes to `random.sample`, i.e. sampling without
replacement. Independent randints repeat bases, wasting tries under contention —
which is exactly when each try is worth the most. It also makes the empty window
unrepresentable rather than merely guarded.

Adds the regression test. The existing four all read the real /proc value, so
this path cannot be reached in CI; verified the new one fails with the original
ValueError when the fix is reverted.

Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
@jiejingzhangamd

Copy link
Copy Markdown
Collaborator

Pushed 2cd56f43 fixing the blocking issue I raised, rather than leaving it for a round-trip.

free_tcp_port_block no longer crashes on a tuned ip_local_port_range. Guarded the empty window and kept the deliberate RuntimeError, so the message names the cause:

before this branch   RuntimeError: could not find 8 contiguous free TCP ports
on this branch       ValueError: empty range in randrange(1024, 1017)
after 2cd56f43       RuntimeError: no room for a 8-port block below the
                     ephemeral range (ip_local_port_range starts at 1024)

Switched the 64 probes to random.sample. Independent randint draws repeat bases, wasting tries under contention — which is when each try is worth the most. It also makes the empty window unrepresentable rather than merely guarded, so the guard and the sampling cannot drift apart.

Added the regression test. The existing four read the real /proc value, so this path cannot be reached in CI. Verified it fails with the original ValueError when the fix is reverted — a test that cannot fail is not worth having.

Behaviour on a normal host is unchanged: still a random base below the ephemeral range.

The two non-blocking points from the review are untouched and yours to judge — find("[") vs rfind("[") for an IPv6 NFS bind mount, which is marginal since NFS classifies as buffered anyway.

@dorado269
dorado269 marked this pull request as draft August 1, 2026 05:50
@dorado269 dorado269 changed the title fix(kvaware+kvd): two bugs blocking KV-aware routing + kvd together on SGLang, with image, docs and repro [wip]fix(kvaware+kvd): two bugs blocking KV-aware routing + kvd together on SGLang, with image, docs and repro Aug 1, 2026
@dorado269 dorado269 closed this Aug 3, 2026
jiejingzhangamd added a commit that referenced this pull request Aug 3, 2026
The GLM-5.2 header recorded 'No Address already in use' from the DP
attention run, worded so it reads as 'main has this bug and we did not
trip it'. Two things are wrong with that now.

The run predates #79, which landed the randomised scan start in
free_tcp_port_block on main -- the same fix #59 carried. So the risk is
retired, not merely untriggered.

And the observation proved less than the wording implied even at the
time: that collision needs two engines on ONE host, and a cross-node PD
pair cannot produce it. Recording a clean result from a topology that
cannot fail the test reads as evidence when it is not.

Both the manifest header and the README table now say this.

Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants