dsv41-offload / offload /dspark /INTEGRATION.md
ProprietaryLegal's picture
PLI Labs public snapshot 2026-10-28 (history squashed)
0693474
|
Raw History Blame Contribute Delete
30.9 kB

Status 2026-10-23: Original integration checklist retained; capacity/async production confirmed W5. Use promotion README and roadmap for current TP2/native gates. See live roadmap and measured record; campaign review decisions are in LANES.md / lane reports outside the repo.

Remote DSpark drafter — Spark-side integration checklist (lane O3 → coordinator)

Everything below is for the coordinator (session coordinator). Lanes never restart the serve. Every step runs inside an open window (offload/launch/window.sh open …), through offload/launch/start_exp.sh <config> on :8001. To roll back, run ./start.sh, or start any config without the overlays.

Wire/state contract: offload/dspark/CONTRACT.md (v1.8). Code: offload/dspark/spark/ (lane O3, branch offload/o3-proxy). Worker: lane O2, offload/dspark/worker/ (server.py, CONTRACT §6–§9).

0. What the overlay changes, and what it does not

  • One image file is replaced: vllm/v1/worker/gpu/spec_decode/__init__.py. The replacement is the image file plus one guarded branch: when VLLM_DSPARK_REMOTE is non-empty, method == "dspark" builds the remote speculator.
    • Unset, the image path runs unchanged. tests/test_speculator.py::test_overlay_is_image_plus_guarded_branch pins the diff.
    • The base file's sha256 61b3acba…aa1b7e8 equals the live container's file (checked 2026-10-22 on sparkhead).
  • One package is added: vllm/v1/worker/gpu/spec_decode/remote_dspark/, the contents of offload/dspark/spark/remote_dspark/.
  • Unchanged:
    • The target model, verifier, rejection sampler, depth policy, 0040 schedule, 0047 adaptive window, the scheduler, and the API/aliases.
    • SPEC JSON, --kv-cache-memory-bytes and all production env vars stay as they are, unless a step below says otherwise.
  • Both ranks need the same overlays and the same env: TP ranks must make identical decisions. Only rank 0 connects to x86host; rank 1 receives drafts through one NCCL broadcast on the TP group.
  • Compatible with the coordinator's capture overlay (offload/dspark/capture/speculator.py, mounted over dspark/speculator.py). The two can run together; for timing, use neither.

1. Preparation

1.1 Files (from the merged offload-x86host checkout on sparkhead)

start_exp.sh copies OVERLAYS to ~/dsv41-offload-run/overlay/ on both nodes and bind-mounts them read-only. The ready configs in offload/dspark/spark/launch-configs/ already list both overlays:

offload/dspark/spark/remote_dspark=/usr/local/lib/python3.12/dist-packages/vllm/v1/worker/gpu/spec_decode/remote_dspark
offload/dspark/spark/overlay/vllm/v1/worker/gpu/spec_decode/__init__.py=/usr/local/lib/python3.12/dist-packages/vllm/v1/worker/gpu/spec_decode/__init__.py

Copy the config you want into offload/launch/configs/, or pass its path directly.

1.2 Identities

python3 offload/dspark/spark/tools/identity.py <S2's DSPARK_COPY_DONE> prints the two values the configs carry (live legal-v2a, 2026-10-22):

VLLM_DSPARK_REMOTE_IDENTITY=be2c1436fa9f9e66b398e1b1da2e140e6fddc82a11939e2ac4240a0ca79543bd
VLLM_DSPARK_REMOTE_MARKOV_IDENTITY=0b6e71e638abd5abce2db433965bb5ff523cbbe92ed7ae54324a50ccaeffa19a

The worker must report the same values at HELLO; otherwise rank 0 disables the remote and logs both digests.

1.3 Transport

The endpoint scheme selects the protocol. The worker has to speak the same one.

  • o1rdma://10.202.28.1:18620?bind=10.202.28.2: lane O1's ofl, RDMA RC. This is what all four configs use, and what O2's server is expected to serve (an ofl server, service dspark-v1).
    • Measured Spark A ↔ x86host p50: echo 5.6 µs, 75 KB 33 µs (O1 API.md).
    • o1tcp://… selects ofl's TCP backend on the same server and port, for debugging.
    • Both need libofl.so + ofl.py in VLLM_DSPARK_REMOTE_OFL_DIR; the configs mount ~/dsv41-offload-run/ofl at /opt/dsv41-ofl. Build on sparkhead's host, aarch64 (O1 API.md "Build"), and put the same directory on sparkworker so the bind mount source exists there too; rank 1 never loads it:
      cd ~/dsv41-deploy/offload/transport && make OUT=$HOME/dsv41-offload-run/ofl $HOME/dsv41-offload-run/ofl/libofl.so \
        && cp ofl.py $HOME/dsv41-offload-run/ofl/
      ssh sparkworker mkdir -p dsv41-offload-run/ofl && scp -q $HOME/dsv41-offload-run/ofl/{libofl.so,ofl.py} sparkworker:dsv41-offload-run/ofl/
      
    • O1 commit f256568 or later gives a stable payload_buf. The proxy does not use it yet: each STEP is copied into the ofl send slot once, ≈ 20–40 µs per decode round and ≈ 0.5–1 ms per 4-request prefill step.
  • tcp://10.202.28.1:18620?bind=10.202.28.2: the contract's own TCP stub (length-prefixed frames). It is a different wire protocol from ofl's TCP backend. Only the reference mock_worker (TCP-stub mode) or a worker that implements the stub speaks it. It needs no library.
  • remote_dspark/mock_worker.py can stand in for the worker on x86host, for plumbing checks without the GPU drafter. It serves either protocol: --ofl-dir <dir with libofl.so + ofl.py>, or the stub. Its drafts are fake, so use shadow policy only.

1.4 Offline checks (Controller, CPU)

cd offload/dspark/spark && python3 -m pytest -q tests        # 160 pass (2026-10-22, O3-FIX)
  • An end-to-end engine simulator checks exact drafts through: chunked prefill with trimmed taps, decode with random k and rejections, slot reuse, re-add/replay, timeouts, worker restart, frame and per-request errors, and dropped connections.
  • Also covered: the speculators with vLLM stubs, the overlay diff and dispatch, and the proxy over ofl's TCP backend (.build/ofl).

1.5 Pre-flight against the real worker (before the experiment start)

Run inside an open window or with the serve idle. The probe refuses unless /metrics shows 0 running and 0 waiting. During a window nothing listens on :8000, so pass --no-metrics-check only when the serve is down.

Run the probe on sparkhead's host with the system python3. It needs no numpy and no torch (checked with numpy hidden): it uses only remote_dspark/{wire,transport,o1_adapter}.py from the merged checkout.

VLLM_DSPARK_REMOTE_OFL_DIR=$HOME/dsv41-offload-run/ofl python3 ~/dsv41-deploy/offload/dspark/spark/tools/probe.py \
  --endpoint 'o1rdma://10.202.28.1:18620?bind=10.202.28.2' \
  --identity be2c1436fa9f9e66b398e1b1da2e140e6fddc82a11939e2ac4240a0ca79543bd \
  --markov-identity 0b6e71e638abd5abce2db433965bb5ff523cbbe92ed7ae54324a50ccaeffa19a --hello --step-rows 128

Expected:

  • hello_status: OK, with the worker's identity and markov digests equal to ours;
  • ping_us (TCP ~30–60 µs, RDMA ~6 µs);
  • step_status: OK and step_req_status: OK for a 128-row window. step_worker_us is O2's draft time for B=1.

--hello opens a new session, which drops an engine session's windows on the worker. Use it only while no engine is connected; the engine would recover by itself (NO_SESSION → HELLO → resync).

2. Order of in-engine checks

step config purpose pass criterion
1 C-shadow.env wiring + parity. Production drafts unchanged; the remote runs alongside the worker connects (log remote DSpark worker '…' ready); rounds_ok ≈ rounds; shadow.full_block_agree_rate high; mismatches are near-ties (examples[].remote_margin small)
2 C-shadow-remote.env acceptance with remote proposals (local still compared) vllm:spec_decode_num_accepted_tokens_total / num_drafts_total within noise of A on the same prompts
3 timing inputs read latency.local_propose_us and latency.rank0_round_us from the shadow stats replace the PLACEHOLDER REMOTE_ROUND_MS / REMOTE_DRAFT_MS in C-capacity / C-resilience (§4)
4 C-capacity.env the capacity mode: no local drafter starts; reclaimed memory measured (§5); outputs sane; speed vs A (bench2)
4b C-capacity-async-cal1.env (lane A2's hand-off; CUDA_MODULE_LOADING=EAGER stays: the proxy needs the GIL, and pybind11 extension launches hold it — S9 P1) same, with async rounds (the CPU never blocks; the engine stream waits on a host word the proxy thread writes with no CUDA call) log remote DSpark async mode on (host-flag hand-off self-test passed; …); stats async: true, async_handoff; speed vs step 4. Deploy libdsv41_hostflag.so next to libofl.so (§1.3) or let rank 0 compile hostflag.c at startup
4c A-local.env vs C-capacity.env (then C-capacity-nodrop.env if needed) prefix-cache hits without the drafter's KV layers (§5, "KV group annotation") the second request of a repeated ≥ 32K prompt has the same cached_tokens as under A
5 C-capacity(-async).env with KV_BYTES_OVERRIDE re-sized KV after step 4's measurement larger GPU KV cache size / Maximum concurrency in the log; the 256K/300K runs of plan §4.5
6 C-resilience.env per-round fallback stop the worker mid-run → local_fallback_rounds grows, outputs continue; restart it → remote resumes (resync_rounds)
7 failure drill in capacity stop/restart the worker mid-run the target-only rounds are counted (fallback_not_ready, fallback_timeout); after restart hellos +1, resync_rounds +1, rounds_ok grows again

Step 1 shows rank 0's gather in gather_us, and the network plus worker time in rtt_us / worker_us. In shadow mode the step is slower than A because both drafters run; do not benchmark shadow.

3. What to read

  • Rank 0 stats file: docker exec dsv41-serve cat /tmp/dsv41-remote-dspark.json on sparkhead. It is written every VLLM_DSPARK_REMOTE_LOG_EVERY rounds; a log line is emitted at the same time.
    • counters: rounds_ok, rounds_failed, rounds_some_failed, fallback_<reason> (not_ready, disabled, reconnected (the connection changed after the round's entries were built; nothing sent), cooldown, timeout, transport_error, bad_reply, frame_<status>, internal_error), req_status_<status>, resync_rounds, resync_rows, rows_sent, bytes_sent, bytes_received, hellos, disconnects, connect_failures, local_fallback_rounds (resilience), rounds_n_out_cut (rounds sent with n_out < N), breaker_trips, breaker_recoveries, fallback_cooldown (rounds skipped by an open breaker).
    • breaker: state (closed / open / half_open), consecutive_failures, cooldown_left, trips, and the configured trip / cooldown (CONTRACT §10). Log lines: remote DSpark circuit breaker open (…) and … closed: the probe round succeeded.
    • last_bad_reply: why the last reply was refused. fallback_bad_reply counts replies refused for their framing or their values (CONTRACT §6.3 v1.4: draft ids outside the vocabulary, confidences not in [0, 1], bad window or status, nonzero n_out tail). Any nonzero count is a worker bug to report to lane O2; the round ran target-only (capacity) or local (resilience) and the slots resynced.
    • stale_generation_new_sessions (and req_status_stale_generation): the worker reported a slot generation newer than rank 0's. Rank 0 never sends an older one, so this means the worker's state diverged; rank 0 started a new session (the worker dropped every window) and resynced (CONTRACT §9.2, v1.5). Expected 0; report any other value.
    • latency (µs, p50/p90/p99/max):
      • rank0_round_us: rank 0's whole remote round. This is the remote cost the depth policy should get.
      • gather_us: device gather + D2H + sync.
      • rtt_us: network + worker.
      • worker_us: the worker's own report.
      • local_propose_us: shadow only; the local drafter, CUDA events.
    • shadow: agree_rate_by_position, first_mismatch_hist (index 5 = whole block agrees), full_block_agree_rate, max_conf_abs_diff, examples (local vs remote ids, worker margins).
    • worker, policy, endpoint, rank0_buffers (45 MiB device, 15 MiB pinned), async / async_fallback_reason / async_handoff (mechanism, library path, spin/nap), and fused_gather (on, check).
    • Async counters: fallback_resync_pending (a resync waited one round for its staged windows), resync_windows_staged (slot windows the engine stream copied for the proxy); rank0_submit_us (the engine thread's enqueue cost per round).
      • The one-launch Triton row gather (remote_dspark/fused_gather.py) replaces ~40 eager torch ops per round on rank 0's stream.
      • It is self-checked at startup against the torch path on the GPU (bit-identical rows, ring and metadata) and turned off with a warning on any mismatch or error. It was verified offline under TRITON_INTERPRET=1.
      • VLLM_DSPARK_REMOTE_FUSED_GATHER=0 forces the torch path.
    • n_out_by_reqs, n_out_reason (CONTRACT 1.3, §6.3): the draft positions requested from the worker by request count, and why.
      • In capacity and resilience, with the live conf policy and schedule, this is [0, 5, 5, 4, 4]: n_out 4 at 3–4 requests, which saves the worker ≈ 0.55 ms per round at 4 requests (O2).
      • Shadow always shows null (all N, so parity covers every position).
      • VLLM_DSPARK_REMOTE_N_OUT=0 turns it off.
      • A VLLM_DSPARK_DEPTH_FILE, or a fixed:/off depth policy, also turns it off.
  • Serve logs:
    • remote DSpark (<policy> policy) rank 0 -> <endpoint>
    • remote DSpark worker … ready … (session …, epoch N): the epoch rises on every reconnect.
    • remote DSpark disabled for this engine run: …: identity, config or version refused. Fix it and restart.
    • In remote modes, the DSpark depth policy conf (k=5), round cost ms by depth […] line must show the remote table.
  • vLLM /metrics: as bench2 reads them: vllm:spec_decode_num_drafts_total, …num_draft_tokens_total, …num_accepted_tokens_total, …num_accepted_tokens_per_pos_total.
  • Worker side: O2's STATS op (tools/probe.py --hello prints it before the run) and its own logs.

4. Depth-policy costs in remote modes (recalibration)

The live VLLM_DSPARK_ROUND_MS=33.26,… is "verify forward + drafter" by depth, with VLLM_DSPARK_DRAFT_MS=0. Its cost already includes the local drafter. In capacity and resilience the remote round must be an explicit input:

  • VLLM_DSPARK_REMOTE_ROUND_MS replaces VLLM_DSPARK_ROUND_MS with a verify-only table. First estimate: each live entry minus shadow local_propose_us p50 (in ms). Better: measure with fixed depths, the way the live table was made.
  • VLLM_DSPARK_REMOTE_DRAFT_MS replaces VLLM_DSPARK_DRAFT_MS with the remote round: shadow rank0_round_us p50 in ms (it includes gather, network and worker; add ~0.05 ms for the TP broadcast).
  • The overlay writes both into the process env before the model runner builds the depth policy. Both ranks get the same env, so the tables stay identical across TP ranks.
  • With the depth policy on (live: conf), capacity and resilience refuse to start unless both REMOTE_DRAFT_MS and REMOTE_ROUND_MS are set. The policy prices depth d as ROUND_MS[d] + DRAFT_MS, so either one alone mixes the live table (local drafter included) with a remote cost. The error names what is missing, and nothing is overridden. Checked at startup:
    • REMOTE_DRAFT_MS: one finite value ≥ 0;
    • REMOTE_ROUND_MS: exactly k + 1 = 6 finite values > 0 (depth 0..5), non-decreasing. The launch configs' placeholders pass.
  • VLLM_DSPARK_DEPTH_POLICY=confr:<rows ms> is refused in capacity and resilience. Its rows table is part of the policy string, measured with the local drafter, and the remote inputs cannot replace it. Use conf (live) or confb.
  • A VLLM_DSPARK_DEPTH_FILE can replace the cost table at runtime (conf:<ms…>, confb:<ms…>, confr:<ms…>). In remote modes, any table in that file must be a remote-mode measurement. The overlay cannot check it.
  • fixed:<d> never prices a depth, so it needs neither value unless a VLLM_DSPARK_DEPTH_FILE is set (the file can switch the mode to conf at runtime). This lets you measure the remote verify table with fixed depths without dummy costs.
  • With the depth policy off, nothing reads either value. They are only parsed: a value given alone is written, and a malformed one is refused.
  • Shadow ignores both and keeps the local measurements.

5. Capacity mode: what disappears, and how to measure it

Not allocated on either Spark:

  • the drafter weights. mtp.* is 3.77 GiB on disk, TP-sharded, so roughly 1.9 GiB per rank plus the replicated main_proj (47 MiB) and Markov head (126 MiB);
  • the Markov bias cache (4,096 rows);
  • the draft CUDA graphs (FULL_DECODE_ONLY captures of the query block);
  • the draft workspace lane;
  • the three draft attention layers. They are never registered, so get_kv_cache_spec has no draft SWA layers and the KV-cache groups change.

Measure, A vs C-capacity, both ranks:

  1. Log lines: Model loading took X GiB, Graph capturing finished … took X GiB, GPU KV cache size: N tokens, Maximum concurrency …, and the KV-cache group printout (count of groups/layers).
  2. free -g / MemAvailable on each Spark after warmup, plus nvidia-smi per-process usage.
  3. With --kv-cache-memory-bytes unchanged (4026531840), the token capacity should rise a little: the same bytes now serve fewer layers. The freed non-KV memory is what KV_BYTES_OVERRIDE can then take. Keep the margin the production fit was sized with; Sparks run at 5–7 GiB MemAvailable.
  4. Retained for the remote path: rank 0 holds 45 MiB of device buffers (15 MiB ring + two 15 MiB stages), a 15 MiB pinned message buffer and the ofl send slot (16 MiB). Rank 1 holds a < 1 KiB broadcast tensor. The stats file reports rank0_buffers.

Also note:

  • KV group annotation and prefix-cache hits (check in the capacity window).
    • What happens:
      • SPEC still says dspark, so use_eagle_block_drop() stays true (config/speculative.py:1947-1949).
      • For deepseek_v41, _uses_trailing_mtp_layers turns on _annotate_eagle_groups' positional fallback (v1/core/kv_cache_utils.py:2155-2171, :2234-2242). That fallback flags the group holding the last registered layer as the "eagle" group.
      • Under A, that group is the drafter's SWA group (the three mtp.* layers register last). In capacity mode the draft layers do not exist, so it is the group holding the last target layer: a target group.
      • If the partition check fails, no group is flagged. The coordinator then flags every group (v1/core/kv_cache_coordinator.py:104-110).
      • A flagged group loses its trailing scheduler block from prefix-cache hits, the "EAGLE last-block drop" (comment at kv_cache_coordinator.py:112-122). Hits land on scheduler-block boundaries, so a hit can come out up to one scheduler block shorter than under A, and the request prefills that much more.
      • Output is never affected, only cached tokens and prefill time.
    • How to check, in the window. Run the same steps under A (A-local.env) and under C-capacity.env:
      1. Startup log of both ranks: kv cache group sizes [...] and kv lcm block sizes N. Record how capacity differs from A; the drafter's layers are gone, so a group may disappear or merge. N is the scheduler block, the most a hit can lose. No log line names the flagged group, so step 2 is the real test.
      2. Send one long legal prompt (≥ 32K tokens, from bench2) twice in a row on an idle serve. Read the second response's usage.prompt_tokens_details.cached_tokens; production returns it.
      3. Read the deltas of vllm:prefix_cache_queries_total and vllm:prefix_cache_hits_total on /metrics across those two requests.
      4. Compare A with capacity. Equal cached tokens: nothing to do. Capacity lower by about N tokens per hit: the fallback flagged a target group.
    • Remedy, capacity mode only: launch-configs/C-capacity-nodrop.env, which is C-capacity with "disable_eagle_block_drop": true added to SPEC_OVERRIDE.
      • The drop exists so that a prefix hit never reuses a draft-KV slot written with the chunk's lookahead token (kv_cache_coordinator.py:112-122). Capacity mode has no local draft KV, so dropping a target block protects nothing.
      • Rerun step 2 under it: cached tokens should match A.
      • Never in resilience or shadow: they keep the local draft KV and need the drop.
  • Wasted 160 MiB per rank in all modes, not touched here. The target allocates _mtp_hidden_buffer (4096 × 20480 bf16) because use_eagle() includes dspark (nvidia/model.py:874-886), and copies the hidden states into it every step. DSpark never reads it: the aux taps exist (dflash/speculator.py:376-381). Freeing it needs a model-side change: skip the buffer for dspark, and return a zero-width view from get_mtp_target_hidden_states, which model_runner.py:2336-2338 slices. That would reclaim 160 MiB per rank in A as well.

6. Failure policies (CONTRACT §10)

  • Circuit breaker, all policies (CONTRACT §10 v1.6).
    • After VLLM_DSPARK_REMOTE_TRIP failed attempts in a row (default 3), the next VLLM_DSPARK_REMOTE_COOLDOWN rounds (default 256) skip the network. A failed attempt is a STEP that timed out, was refused or got a frame error, or a STEP where every request that needed drafts failed. Then one probe round.
    • Rounds without a connection do not count, and neither do successful rounds with nothing to draft (prefill appends only, v1.8). A round where only some requests failed is a success.
    • Why it exists: without it, a worker that answers but fails every round costs each round its RTT, and a worker that misses every deadline costs one 50 ms stall per reconnect (≈ 5 %). With it, the cost is one attempt per ≈ 9 s.
    • VLLM_DSPARK_REMOTE_TRIP=0 turns it off. That is useful in a failure drill when every attempt should hit the worker.
    • Drill expectation (step 7): with the worker stopped, rounds are fallback_not_ready while the reconnect backoff runs and never trip the breaker, since no STEP is sent. A worker that is up but broken trips it after 3 rounds.
  • capacity.
    • A failed round sends zero drafts with confidence 0, so the depth policy verifies nothing that round.
    • Rounds never wait on a connect: a daemon thread reconnects with backoff (1 s doubling to 30 s), and while it is down rounds fail in ~µs.
    • On recovery: HELLO, then the whole window of every live slot is resent from rank 0's ring. Drafts are exact again from the next round.
    • VLLM_DSPARK_REMOTE_ON_FAIL=raise, benchmarks only: any failed round raises on both ranks (engine-fatal), and connect + HELLO must succeed at startup.
    • A clean failure of a single request is not possible from inside the speculator.
  • resilience. After a failed round both ranks run the local drafter that round, as decided by the broadcast status. Requests that failed on the worker get local drafts; the others keep remote drafts. The local context KV is kept current every round with an eager precompute of the new rows. Nothing is freed.
  • shadow. Both run. Local drafts are used unless VLLM_DSPARK_REMOTE_SHADOW_USE=remote is set; with it, failed remote requests fall back to local.
  • Never a hang: every network wait has a deadline (VLLM_DSPARK_REMOTE_TIMEOUT_MS, default 50 ms), and an exception on rank 0 still broadcasts a failed status so rank 1 never waits forever.

7. Rollback

  • Immediate: ./start.sh (production config; no overlay, no remote env).
  • In an experiment: start the same config without OVERLAYS/EXP_DOCKER_ENV, e.g. A-local.env. Without VLLM_DSPARK_REMOTE the overlay is inert, but the plain image is the reference.
  • Worker: stop O2's unit. A connected engine then runs per its policy: capacity → target-only, resilience → local drafter, shadow → unchanged.

8. Known limits

  • Greedy drafts only (live: local argmax). Probabilistic drafting, dspark_draft_topk, watermarking, adaptive verification (enable_adaptive_verification, off live), PP, DP and context parallelism are refused at startup with a message.
  • Sync rounds are a host round trip on rank 0. The rank-0 CPU waits for the target step, then for the network and the worker, so the next step's CPU preparation is no longer overlapped with GPU work, as it is with the local drafter. That is a fixed per-step cost of ≈ 0.5–2 ms on top of the drafter time.
    • In shadow it is visible as rank0_round_us minus rtt_us.
    • Async rounds (VLLM_DSPARK_REMOTE_ASYNC=1, capacity only) remove it. Lane A2 design, v2 after review S9 (remote_dspark/streamsync.py, hostflag.c): one C-owned page of registered, device-mapped host memory holds three 32-bit words (gathered, released, claimed) and the reply. The engine stream writes gathered = t after the gather's copies (cuStreamWriteValue32, fenced), waits for released >= t (cuStreamWaitValue32), then copies the reply from the page (cuMemcpyHtoDAsync) and broadcasts it. The proxy thread acquire-spins on gathered, runs the round on the CPU and the network, builds the reply in a private buffer and publishes it in one GIL-free C call: claim t, copy the reply into the page, release t (monotonic). It makes no CUDA or torch call: the first async design (side-stream H2D + device flag written by the proxy thread) deadlocked on GB10 against a Triton module load on the engine thread while the stream was parked (offload/results/C/lazy-repro/RESULT.md); a host word is released by a CPU store, which needs no CUDA lock. The engine's Engram spin reader uses the same handshake on GB10.
    • A native watchdog thread (C: no Python, no CUDA) fails and releases a round that was gathered but not released within the deadline; the claim word makes it and the proxy's publish mutually exclusive per round. Releases never move backwards (cyclic max), so a late proxy cannot undo a later release (S9 F3). If the proxy thread ends for any reason other than close(), every pending and later round is failed and released at once (far-ahead claim) and later rounds skip the hand-off (zero drafts) (S9 F2). close() fails everything still pending the same way and logs an error if the thread did not stop within 5 s.
    • Resync rows in async mode come from ring windows the engine stream copies to pinned host memory on request: the first round that needs a resync asks for them and fails (zero drafts), the next one is exact. fallback_resync_pending counts those rounds (one per failure/reconnect event; two if the engine had already submitted the next round).
    • The C helper libdsv41_hostflag.so (v2: page allocation + mlock, acquire wait with GIL released, monotonic release, claim, publish, fail-release, watchdog). Production staging (spark/stage_overlay.sh, S9 F8) copies a pinned aarch64 build next to libofl.so and refuses when async is configured and the build is missing or not the pinned one (the pins: source HOSTFLAG_C_SHA, build HOSTFLAG_SO_SHA, set after building on the head node: cc -O2 -Wall -shared -fPIC -o ~/dsv41-offload-run/ofl/libdsv41_hostflag.so offload/dspark/spark/remote_dspark/hostflag.c -lpthread). Experiments may leave it out: rank 0 then compiles the overlay's hostflag.c with cc into a private temp directory at startup (async_handoff.lib says which). No C compiler and no library: async is refused with a warning (synchronous rounds). VLLM_DSPARK_REMOTE_HOSTFLAG_LIB names an explicit path. A v1 library is refused (version check).
    • The proxy thread spins on one rank-0 core while a round is pending (as cudaEventSynchronize did); VLLM_DSPARK_REMOTE_ASYNC_NAP_US > 0 sleeps between checks after VLLM_DSPARK_REMOTE_ASYNC_SPIN_US.
    • Startup self-test (HostFlagSync.self_test): the CPU sees the copied data with gathered; the stream does not pass the wait before the publish, and its reply copy after the wait reads the bytes the publish wrote (the production path). Its rescue is a CPU store; on failure the mode turns off with the reason. If the stream still does not drain after the rescue (a device that never observes host stores) it raises StreamStuckError, which stops startup instead of falling back to a stream that is parked for good (S9 F4).
    • Tested (lane A2, 2026-10-23): CPU suites (a profiler test fails on any torch call from the proxy thread), and on an RTX 3090 the GPU harness tests_gpu/gpu_async_harness.py with the reference worker on localhost: the first design deadlocked on a fresh cuModuleLoadData during a parked round (killed at 90 s); this one completed 30/30 such rounds (each load waited ~41 ms = the injected worker delay, then returned) and a 10,000-round soak pipelined 3 deep with 270 module loads during parked rounds and 49 injected faults: 19,082 drafts bit-exact, 9,319 failed rows zero, 73 resyncs from staged windows (124 resync_pending rounds), proxy round p50 664 µs vs 861 µs for the first design without module loads. Evidence: $CAMPAIGN_LOGS_ROOT/DSV41-OFFLOAD/A2-evidence/. GB10 is first run by the coordinator.
    • CUDA_MODULE_LOADING=EAGER stays in the async configs (S9 F1). The proxy needs the GIL, and pybind11 extension launches (every vllm_exl3_moe extension: plain m.def, no gil_scoped_release) hold it; under lazy loading such a launch waits for the parked stream while holding the GIL (S9 P1). EAGER moves those loads to startup.
    • The native watchdog (hostflag v2, S9 F2/F5) is the backstop for what EAGER cannot cover (an extension first imported while serving, a GC finalizer on the proxy thread making a CUDA call, proxy death, interpreter exit): a round the stream gathered but nobody released within VLLM_DSPARK_REMOTE_ASYNC_WATCHDOG_MS (auto max(4 x TIMEOUT_MS, 200)) is failed (zero drafts) and released from C, with no Python and no CUDA. Counters: async_watchdog.fires, watchdog_preempted (the proxy found its round already failed), fallback_proxy_dead / async_dead (the proxy thread ended: every later round is target-only, no hand-off).
    • The proxy thread shares the GIL with the engine thread, so async mode lowers the interpreter switch interval to 500 µs (VLLM_DSPARK_REMOTE_SWITCH_INTERVAL_US) to bound the waits.
  • No local timing claims. The overlay ran only offline (CPU, Controller). Every number on GB10 comes from the coordinator's runs.