Repository navigation
Optimized PFPL Performance for ROCm - #7
Open
Seth-Wolfgang wants to merge 8 commits into
Open
Seth-Wolfgang wants to merge 8 commits into
Seth-Wolfgang wants to merge 8 commits into
Conversation
Work in progress, on a branch for testing.
- fz::pfpl (modules/fused/chunk_fusion/pfpl_stream.{h,cu}): compress a float field from one device buffer into another and
back, in one kernel each, ordered on a stream with no host synchronization. The archive is byte-identical to the PFPL preset's
in ABS mode.
- AMD wave64 kernels for it (pfpl_stream_amd.inl), used on HIP. CUDA uses the generic chunk harness.
- include/backend/fp.h: fmulRounded(). On HIP, clang fused the bound check's multiply into its subtraction, so the check tested
the exact product and passed values that decode up to half an ulp past the bound.
- backend/warp.h whole-wavefront primitives, chunk_fusion.cuh on the backend wrappers, and no NVRTC fusion on HIP.
Tested on MI250X with the HIP backend on development only. main configures for CUDA alone, so this branch has not been built yet.
Still missing for a PR: CHANGELOG entries, an in-tree test, a CN-* note, and removing two device-side printf calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-DFZGMOD_BACKEND=HIP configures and builds again. The root CMakeLists.txt, presets and package config take their HIP hunks from development, without its profiling target, which main does not have. Module and test CMake files already branch on the backend. NVRTC specialization is not built on HIP, so every stage runs staged. Four tests need it or include cuda_runtime.h directly, so they are CUDA-only. On one MI250X GCD, 50 of 53 tests pass. test_huffman, test_log_transform and test_stage_registry fail on HIP, and they are not changed here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plan::reserve cleared its ticket counter and look-back state with a stream-less cudaMemset, which is not ordered with a non-blocking stream's kernels. The first encode could read old page contents as its ticket, compute a wild chunk id and stop with a memory aperture violation. The clears now run on the calling stream, ahead of that call's kernel. See CN-PFPLSTREAM-2. tests/stages/test_pfpl_stream.cpp compares fz::pfpl with the staged pipeline. On HIP, CTest runs it with the AMD kernels and with the generic harness kernels. The device-side printf in the decode error paths is gone, and the measurements moved from code comments to CN-PFPLSTREAM-1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fzgmod_modules calls fz::MemoryPool but never declared the dependency, so libfzgmod_modules.so had no DT_NEEDED entry for libfzgmod_mem.so. A consumer that links the library without calling fzgmod_mem itself failed with undefined references on toolchains that default to --as-needed, such as Ubuntu's GCC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CN-PFPLSTREAM-3 has the 64-bit chain, the radius, how the float-sized header records the bound, and the throughput on one MI250X GCD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compressHost and decompressHost read and write the same stream as the kernels, byte for byte, in plain C++, with no device, Plan or GPU. A caller off the GPU path no longer has to stage a stream through the device: in QCLAB++ that ran out of memory reading back double tiles of 512 MiB, and a rank with no GPU could not code at all. PfplHostCase holds them to the kernels on both kernel sets, both types and every size class, including a hostile field for conversions out of range. compress() and compressHost() now share their checks and quantizer parameters through quantParams(). See CN-PFPLSTREAM-4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CN-PFPLSTREAM-4 named the caller it came from. Say what any caller needs, and call the measured data a field, as the rest of the library does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Restore the HIP backend on main; stream-ordered two-array PFPL with AMD kernels; fix FMA contraction in the ABS bound check
Base branch:
main(e84726f)Head:
mainofSeth-Wolfgang/FZGPUModules(74855a4)Author: Seth Wolfgang (seawolfg@iu.edu)
Summary
This PR makes five changes:
main:-DFZGMOD_BACKEND=HIPconfigures and builds again. This is CMake only, and it's a separate commit.fz::pfpl: a float or double field compresses from one device buffer straight into another and decompresses straight back, in one kernel each. Calls are ordered on a stream and need no host synchronization. A float stream's archive is byte-identical to the PFPL preset's in ABS mode. Host functions write and read the same stream with no GPU. On an A100 it runs about 2x the staged pipeline at 8 MiB and above.fzgmod_modulescallsfz::MemoryPoolbut didn't linkfzgmod_mem, so consumers failed to link on toolchains that default to--as-needed.I'm opening this as a draft because
fz::pfplis a new API and becausemaincurrently rejects HIP on purpose ("Stable main supports CUDA only"), so I'd like your view on both. My fork'smaincarries all of them:f826460fz::pfpl, the AMD kernels and the bound fix6d22386main, CMake only07919bfPlanrace fix below, tests and notes6ce75f5eef094a1811d45c7b048a74855a4docs/codebase_notes.md, no codeThey stack in this order. The link fix also stands alone:
fix-modules-mem-linkhas it on yourmain. The host coder and the wording commit after it are the last two, so they're easy to leave out.Why
A staged pipeline is too heavy for a caller that compresses and decompresses many fields from several streams on each GPU, thousands of times per run: one pipeline per stream ran out of device memory or crashed. Fields also need to be ordered with the kernels around them on the same stream, without a host round trip.
The host coder is for reads that have no device to use: a process with no GPU, or a card too full to stage on. With 0.15 GiB of 64 free on a GCD, decodes staged through the device stalled inside ROCm, with stacks in
libhsa-runtime64: 8 of 12 runs stuck.The bound fix
__fmul_rnis a plain multiply on HIP, so clang's default-ffp-contract=fast-honor-pragmasfusesx - q * ebx2into onev_fma_f32on gfx90a. The check then tests the exact product instead of the rounded one the decoder stores, and passes values that miss the bound by up to half an ulp.include/backend/fp.haddsfz::backend::fmulRounded(), which turns contraction off on HIP and stays__fmul_rnon CUDA (__dmul_rnfor doubles). The staged quantizer (quantizer.cu) and the chunk harness (chunk_fusion.cuh) both use it now.To reproduce (HIP backend, gfx90a, without this fix):
verify_reconstructionon.abs_ebby up to half an ulp. With the fix, the largest error is withinabs_eb.The link fix
fzgmod_modulescallsfz::MemoryPoolbut never declared the dependency, solibfzgmod_modules.sohad noDT_NEEDEDentry forlibfzgmod_mem.so. A consumer that listsfzgmod_membeforefzgmod_modulesfails under--as-needed, which Ubuntu's GCC defaults to.With GNU ld 2.45, the old library gives 6 undefined
fz::MemoryPoolreferences, and the fixed one links and runs. ROCm'slldlinks either way, but with the old librarylibfzgmod_mem.sois then never loaded.The two-array API
abs_eb,n) followed by the LC/RZE archive. The pipeline is Quantizer in-place zigzag, Difference negabinary, Bitshuffle, then RZE over 16 KiB chunks.Plan.capacity. The header, when it fits, records the length the stream needed.Plan::reservesynchronizes the device, and only when it has to replace a smaller state. New memory is cleared on the calling stream (CN-PFPLSTREAM-2).compressHostanddecompressHostwrite and read the same stream, byte for byte, with no device,Planor GPU (CN-PFPLSTREAM-4).nelements of that type stops the kernel with a device trap, and makesdecompressHostreturn false, instead of decoding garbage.Changes by area
CMakeLists.txt:fzgmod_moduleslinksfzgmod_mem.pfpl_stream.h,pfpl_stream.cu,pfpl_stream_amd.inlandpfpl_stream_host.inlinmodules/fused/chunk_fusion/, andinclude/backend/fp.h.FZ_PFPL_HARNESS=1selects the generic harness kernels for comparison. CUDA always uses the generic harness.include/backend/warp.h: whole-wavefront primitives (kWaveSize,wave_mask_t,ballotWave,ffsWave,waveSum) andactiveMask32.include/backend/api.h: aliases for cooperative launch.chunk_fusion.cuh:__shfl*and ballot intrinsics now go through thebackend/warp.hwrappers.chunk_inverse_pfpl_at.loadChunkBytesreplaces the byte-wise payload copy, so the CUDA decode path changes here too. On an A100 the specialized decode gets 7-16% faster (table below).const void*cast forcudaFuncSetAttributein Bitshuffle.tests/stages/test_pfpl_stream.cpp: comparesfz::pfplwith the staged pipeline, and the host coder with the kernels. On HIP, CTest runs it twice, once with the AMD kernels and once with the generic harness kernels.CN-PFPLSTREAM-1holds the measurements,CN-PFPLSTREAM-2the first-kernel race below,CN-PFPLSTREAM-3doubles andCN-PFPLSTREAM-4the host coder.Restoring HIP on
mainThis commit changes CMake only:
CMakeLists.txt,CMakePresets.json,cmake/FZGPUModulesConfig.cmake.in: the HIP hunks fromdevelopment, without itsprofiling/target, whichmaindoesn't have. Module and test CMake files already branch on the backend, andinclude/backendalready supports HIP.modules/fused/common/CMakeLists.txt: the NVRTC fused sources aren't built on HIP.findFusedImplreturns nothing andplanAndInstallFusionrecordsunsupported_backend, so every stage runs staged.test_fusion_f64andtest_fusion_plannercall the NVRTC generators,test_fusion_fixedneeds an installed specialization, andtest_lorenzo_quant_shared_initincludescuda_runtime.hdirectly.A first-kernel race in
PlanClearing a new
Planwith a stream-lesscudaMemsetisn't ordered with a non-blocking stream's kernels. The first encode could read the page's old contents as its ticket counter, compute a wild chunk id, and stop withHSA_STATUS_ERROR_MEMORY_APERTURE_VIOLATION. The clear now runs on the calling stream.PfplStream.FreshPlanIgnoresStaleMemoryfaults in its first round without the fix, on both kernel sets, and passes with it. Details are inCN-PFPLSTREAM-2.Results
MI250X
One GCD, ROCm 7.1.1, the
polarfield fromtest_pfpl_stream.cpp,abs_eb1e-4, ratio about 1.7. In GB/s, measured on this branch:Doubles, on one MI250X GCD with a 128 MiB
polarfield, in GB/s. This is a separate run, so compare within this table:The host coder, on one Frontier login core with no GPU in the process and a 128 MiB polar field, encodes at 350-565 MB/s and decodes at 550-656 MB/s.
A100
A100-SXM4-40GB, CUDA 12.5, one stream. The field and bound are the MI250X table's, and the two-array columns are the generic harness, the only kernels CUDA has. Specialized is the same pipeline with
SpecializationPolicy::Auto, which HIP doesn't have. In GB/s:Two-array is 2.1-2.4x staged at 8 MiB and above, and 3.4-4.0x at 1 MiB. Against specialized it encodes 1.1-3.4x faster, and decodes 1.8x faster at 1 MiB, 1.08x at 32 MiB and up to 3% slower at 128 MiB and above.
Each number is the median of 7 rounds of 11 to 41 calls, on fresh buffers each round. A second process agrees within 1% at 128 MiB and above, and within 9% below. This card rests at 1095 MHz between runs of work and takes a few tenths of a second of work to reach 1410 MHz, so every row warms up first; without that the large sizes read about 21% low.
Doubles, with the same 128 MiB field, in GB/s. The ratios match MI250X's, and every decoded value is within its bound:
The host coder, on one EPYC 7713 core of the same VM and the same field, encodes at 204-380 MB/s and decodes at 308-333 MB/s (float at 1e-3, double at 1e-3 and 1e-9), and its streams equal the kernels' byte for byte.
The CUDA decode path against your
main, specialized pipeline, in GB/s. Encode and the staged pipeline don't move beyond the noise above:e84726fTesting
74855a4changes onlydocs/codebase_notes.md, so the results atc7b048abelow stand for it.On
c7b048a, HIP, ROCm 7.1.1, MI250X (gfx90a), from a clean clone:cmake --preset hipconfigures.ctest: 50 of 53 pass, on one GCD.test_pfpl_streampasses all 488 of its stream tests with the AMD kernels and again with the generic harness kernels. They check:Plan;Planon dirty pages.check_doc_cpp.pyskipped itself because this machine has no CUDA headers.On
c7b048a, CUDA 12.5 (nvcc from NVHPC 24.7, GCC 13.3), A100 (sm_80), from a clean clone:ctest: 57 of 57 pass. That includes the four CUDA-only tests, andtest_huffman,test_log_transformandtest_stage_registry, which fail on HIP for the reasons in the table below.test_pfpl_streampasses all 488, so doubles and the host coder hold on CUDA's 32-lane warps, including the host coder against the kernels byte for byte.scripts/run_sanitizers.sh --mode asan, theasanpreset's configuration at-DCMAKE_CUDA_ARCHITECTURES=native): all 57 test binaries pass with no reports, in 353 s.scripts/run_sanitizers.sh --mode compute, memcheck, initcheck, racecheck and synccheck over all 57 test binaries): no errors and no hazards. Two things to know:test_pfpl_streamis too slow under racecheck to finish whole, so racecheck covers 420 of its 488 tests. It leaves out the 2 concurrent-stream tests, 2 many-round tests, and the largest fields except one polar case per type. Memcheck, initcheck and synccheck run all 488.DeviceBuffer.OwnedBufferFreesOnDestructionfailed once under memcheck when another process started on the card during it, because it reads the whole card's free memory. It passes with the card to itself.decodeChunk64andchunk_inverse_pfpl_at, and encoding from a short one inencodeChunk64.check_doc_cpp.py, which skipped itself on Frontier, runs here with CUDA's headers onCPLUS_INCLUDE_PATH: no example references stale API (68 of 112 compile as they stand, 44 are fragments and 5 are skipped). The note and TOML checks pass too.CUDA, compile only, nvcc 13.1 and GCC 12.2: at
07919bf, forsm_80andsm_86, the library and every test build excepttest_jit_cache, which this branch doesn't change. ItscuCtxCreate(&ctx, 0, device)call has too few arguments for CUDA 13.1'scuCtxCreate_v4. Atc7b048a, the library andtest_pfpl_streambuild forsm_80.backend/fp.his in the embedded NVRTC headers.The 3
ctestfailures on HIP are outside PFPL, and I left them alone:test_huffmanphf_headerhas interior padding that{}leaves undefined on clang, so the host-coordinated path writes stale bytes into the archive and they differ from the device path'stest_log_transformRelativeBoundHoldsexp2fandlog2fmiss the round-trip slack on 1 value in 1536. The stage stores it as a lossless outlier, as designed, but the test requires 0 outlierstest_stage_registryEveryShippedStageTypeHasAFactoryNot run:
FZGPUModulesConfig.cmake.inwasn't exercised, because the only consumer tested usesadd_subdirectory.