Repository navigation
[Engine] Windows HIP: keep the pinned arena's GPU aliases clear of the process's memory (#3431) - #4787
Open
GoldenLeafBird wants to merge 1 commit into
Open
[Engine] Windows HIP: keep the pinned arena's GPU aliases clear of the process's memory (#3431)#4787GoldenLeafBird wants to merge 1 commit into
GoldenLeafBird wants to merge 1 commit into
Conversation
…e process's memory (Niko1221#3431) On Windows the HIP runtime gives every cudaHostRegister'ed range a second address for the GPU and takes any host pointer inside it for the registered memory, but it does not reserve that range in the process's address space. In about one start in ten the stack of the thread that pins the expert arena lay there: small cudaMemcpy calls from local variables then read (and wrote) the arena, and every long prompt was read wrong until a restart (Niko1221#3431, Niko1221#2960). - pinned.cu: each registered range's alias is reserved (MEM_RESERVE); a range whose alias already covers memory in use is unregistered and treated as refused by the driver, so it stays resident but unpinned (logged). - generate.cpp: main keeps the free address space below 1 TiB reserved for the life of the process and runs the engine on a new thread, so its stack and later allocations lie above the aliases (STRATA_STACK_ISOLATE=0 turns it off). - pinned_alias_test, docs/AMD_HIP.md. Windows HIP only; other backends keep their behaviour.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem and result
Fixes #3431 and #2960, which report the same fault. On Windows with the HIP engine, about one engine start in ten answers every long prompt wrong until the engine restarts. The answers are fluent text about something that is not in the prompt, with HTTP 200 and no error in the log. The only trace is
KV streaming: 100.00% of 0 block readsfor each request. This line shows on short prompts too, also when their answer is correct.Cause (measured on an RX 7900 XTX with engine 0.1.54, v0.2.8):
PinnedArenaregisters the expert arena withcudaHostRegister(Portable | Mapped), here in 36 slices. On Windows the HIP runtime gives each registered range a second address for the GPU (cudaHostGetDevicePointer). That address differs from the host address and lies low in the address space (seen between 8 and 300 GiB).hipPointerGetAttributeson a local variable returns type 1 (host registered), withhostPointer= slice host address + (variable − alias).cudaMallocmemory (VirtualQuery:MEM_RESERVE), but not the range under an alias (MEM_FREE). So a thread stack can be placed there.cudaMemcpyfrom a local variable then read the arena, and a device-to-host copy into a local variable wrote into the arena. The long-prompt paths pass small values through the stack, so they compute with wrong values. Short prompts mostly do not.This confirms the analysis that @giacomobord-prog posted in #3431.
Result: 0 bad starts in 110 starts of the final change. The same build without the change had 4 bad starts in 30. On current main (
c62d5e3a1), the result is the same: 0 bad starts in 70 with this change merged in, and 5 bad starts in 70 without it.Related discussion
STRATA_ARENA_PIN_GIB=noneas a stopgap.0 block readsmarker.Approach
The change has three parts. All of them apply only to Windows HIP (
#if defined(_WIN32) && defined(STRATA_USE_HIP)). CUDA, Linux HIP, SYCL and Metal keep their behavior.src/core/pinned.cu,claim_alias).VirtualAlloc(MEM_RESERVE). This is address space only, nothing is committed. The runtime already does the same forcudaMalloc.strata pin: the GPU alias [0x4e4840000, +637 MiB) of the range at 0000016AB69E0000 covers memory in use at 0x503ce0000 (this thread's stack, from 0000000503CE0000); that range stays unregistered (#3431)src/program/generate.cpp).mainreserves the free address space below 1 TiB for the life of the process (address space only) and runs the engine on a new thread.cudaMallocreservation above 1 TiB (measured:0x10100000000instead of0x300000000).mainstays low:STRATA_STACK_ISOLATE=0turns part 3 off.Part 3 is the fix that @giacomobord-prog measured in #3431 (0 bad starts in 210). Parts 1 and 2 cover two cases that the diff does not cover: an alias above 1 TiB, and memory that existed before
main.A first version of this PR released the reservation right after it created the engine thread. Then every later thread stack lay low, next to the first one, and part 2 refused a range in 7 of 60 starts. The answers were correct, but prompts in those starts were slower. Keeping the reservation lowered the refusals to 7 in 110 starts. Everything allocated after
mainnow lies above 1 TiB, so the remaining refusals hit memory that existed beforemain. In the diagnostic runs of the first version, the refusals hit two kinds of memory: the first thread's stack (now exempt) and a private block next to it. The final series ran without the thread listing, so the memory of its 7 refusals is not identified.A less strict variant would refuse only a thread stack. This PR refuses any memory in use, because it is not known which memory the runtime or a thread pool uses for small copies later.
A regression test,
pinned_alias_test(Windows HIP, skips elsewhere), checks two things:A short note goes into
docs/AMD_HIP.md.Validation
Environment:
870aedd9e) with the flags oftools/hip/build_windows.bat,CMAKE_HIP_ARCHITECTURES=gfx1100, and ROCm 10.2.0a20260930 (the ROCm of the release package). The DLLs came from the 0.1.54rdna3package.--kv int8 --kv-resident 32768 --spec 4 --mtp --expert-cache auto --max-context 131072, prompt switches fromstrata tune).Start loop: for each start,
serve/server.pyran with the config. The loop sent a 7,118-token prompt with a hidden code word (thinking off, greedy) and a short prompt, and then read the engine log of that start. A start counts as bad when the answer misses the code word. "Diag" builds add aSTRATA_PIN_DIAG=1log of every slice's alias, a 16-byte round trip through a stack variable andhipPointerGetAttributesof it. They are not part of this PR.(1) The 37 fully pinned starts outside a slow period (39 starts, 2 of them refused). From start 31 to 71 the whole PC was slow: a 22-token prompt took 3.4 s instead of 0.3 s. Those starts were correct and are counted above. Their speed is left out of the median. The cause of the slow period was not found.
Passed:
The v0.2.8 local build with diag, 30 starts: in the 4 bad starts, and only in those, (a) the alias of one slice covered the pinning thread's stack, (b) the 16-byte round trip through a stack variable returned zeros, (c)
hipPointerGetAttributesof a stack variable returned type 1, and (d) the log showed0 block reads.The final change, 110 starts: 0 bad. The engine thread's stack was at
0x10000000000in every diag start.A separate check with the same exe, 12 more starts: 0 bad, 0 refused, 36 slices pinned each time, 1116–1192 prompt tok/s.
The
0 block readsmarker identifies a bad start from its first request, also a short one:A standalone program (not in this PR) reproduced the fault every time. It registered a range, placed a buffer at its alias, and registered the range again. The runtime returned the same alias, and the round trip through the buffer returned zeros. The device-to-host copy wrote into the registered range instead.
pinned_alias_test: OK. The test exercised the refusal.pin_budget_test: OK.CUDA compile (nvcc 13.4, MSVC 14.44,
-DSTRATA_ENABLE_CUDA=ON): the objects ofpinned.cu,generate.cppandpinned_alias_test.cppbuild.tools/strata_bench.py --config <config> --long, stock against this PR, alternating twice (each start fully pinned, 36 slices). The speed is unchanged within the spread of the runs:The time to a ready server is unchanged too: median 27.7 s (stock) and 27.7 s (this PR) in the start loop.
Current main. The branch is based on v0.2.8 (
870aedd9e). It merges into mainc62d5e3a1without conflicts. In the merged tree, all four registration sites inpinned.custill callregister_range. Two builds of main used the same flags as above: main, and main with this branch merged in. Both builds ran with theserve/server.pyof main.pinned_alias_testandpin_budget_testof the merged build: OK.Start loop, the two builds in alternation, in blocks of 5 starts:
(2) At slice 16 (1078 tok/s) and at slice 2 (1048 tok/s). One more test start before the series refused slice 0. That start was correct.
The 5 bad starts of main look like the bad starts of v0.2.8: all 36 slices pinned,
0 block readsfor every request, and fluent answers about something else.tools/strata_bench.py --longof main, alternating twice (each start fully pinned, 36 slices):Failed:
Not run:
pinned_alias_testhas its ownmain). Only the start loop covers it.strata-dsv41):ds4_model.curegisters host memory too and is not changed.Limits
main. That range and the ranges after it stay unpinned. So the cost depends on the first refused slice. Measured outside the slow period, the prompts of those starts read 2–10 % slower: 1161 tok/s with a refusal at slice 30, down to 1048 tok/s at slice 2, against 1170–1181. A refusal at slice 0 leaves the whole arena unpinned. That costs as much asSTRATA_ARENA_PIN_GIB=none, which made prompts 9–12 % slower on this PC. A follow-up could pin around a refused slice, but the consumers inexpert_source.cppexpect a pinned prefix.PinnedArenais not covered: the opt-in partial pin ofFileExpertSource(STRATA_PARTIAL_PIN=1) and the DeepSeek engine.cudaHostAllocmemory has identical host and device addresses here, so it has no separate alias.claim_aliasreserves only the alias of the current device.