Skip to content

Commit 2bcc8f7

Browse files
committed
finishes rest of ADM stage details on local machine
1 parent a8a25b2 commit 2bcc8f7

17 files changed

Lines changed: 2277 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,24 @@ Version numbers follow [Semantic Versioning](https://semver.org/).
99

1010
## [Unreleased] — 2.0.0
1111

12+
### Fixed
13+
- `adm_map_decoupled_u16`/`adm_map_thrust_u16`/`adm_map_decoupled_u32`/`adm_map_thrust_u32`: added `#ifndef NDEBUG` overflow sentinel — kernels call `atomicOr(d_overflow_flag, 1)` when a thread's `bit_offset` exceeds `kChunk × kMaxSignalBytes × 8`; host checks the flag after `cudaStreamSynchronize` and throws a `std::runtime_error` to catch inputs that violate the algorithm's bounded-diff assumption
14+
- `adm_map_decoupled_u16`/`adm_map_decoupled_u32`: `__shared__ excl_sum` was uninitialized for the first warp block (warp=0), causing non-deterministic writes to `d_concat_signals`; initialize to 0 at kernel entry
15+
- `tests/stages/test_adm.cpp` AD2 (`U32RoundTrip`): `make_u32_data` amplitude (±12000) exceeded the algorithm's per-thread `local_bits` capacity (64 bytes, supports max diff ≤ 4032); reduced amplitude to ±250
16+
- `tests/stages/test_adm.cpp` AD7 (`SerializeDeserialize`): used `adm_encode<uint16_t>` with `dtype=U32`, causing the U32 kernel to mis-interpret uint16_t bytes as uint32_t values with huge diffs; changed to `adm_encode<uint32_t>` with matching `make_u32_data`
17+
1218
### Added
1319
- CLI `-v`/`-vv`/`-vvv` and `--verbose[=N]` flags: route library log output (INFO/DEBUG/TRACE) to stderr via `fz::Logger::enableStderr()`
1420
- CLI `--profile`: now prints the full per-stage GPU timing table (`PipelinePerfResult::print()`) in compress, decompress, and benchmark modes; benchmark captures both compress and decompress stage breakdowns from the last timed run
1521
- CLI `--print-pipeline`: calls `pipeline->printPipeline()` after finalize to display stage topology and connections
1622
- CLI `--bounds-check`: enables `pipeline->enableBoundsCheck(true)` for runtime buffer overrun detection
1723
- CLI `--report` now includes peak device memory usage (`pipeline->getPeakMemoryUsage()`) for compress and benchmark modes
1824
- CLI: TOML config path now respects `--warmup`, `--profile`, `--bounds-check`, and `--print-pipeline` flags (previously only the dynamic builder path applied these settings)
25+
- `ADMStage`: Adaptive Data Mapping stage adapted from the MANS project (Huang et al., BSD-3-Clause); remaps `uint16_t[]`/`uint32_t[]` streams into a compact 8-bit symbol domain before entropy coding; `isGraphCompatible()=false`; 12-byte FZM header stores dtype + `num_elements`; two encode paths: decoupled look-back prefix sum for gsize ≤ 1024 and Thrust `exclusive_scan` fallback for larger arrays; 9 persistent scratch device buffers managed via `MemoryPool::allocatePersistentDevice`
26+
- `ADMStage` kernel files: `modules/transforms/adm/mapping_uint16.cu` and `mapping_uint32.cu` — TU-private `__global__` kernels with exported host wrappers (`compress_u16`/`decompress_u16`/`get_max_u16_payload_bytes` and u32 equivalents); all per-call `cudaMalloc`/`cudaFree` from the MANS reference replaced with `AdmScratch` pool pointers
27+
- `ADMStage` registered in `stage_factory.h` (`case StageType::ADM:`) and `config.cpp` (`addADMStage`/`saveADMStage`/`kStageRegistry` entry with TOML type `"ADM"` and `dtype` key)
28+
- `ADMStage` added to `fzgpumodules.h` public include
29+
- `tests/stages/test_adm.cpp`: 12 tests (AD1–AD12) covering u16/u32 round-trip, small input (< one warp block), large input (Thrust fallback path), zero input, compression ratio, header serialization, save/restore state, graph-compatibility, Pipeline integration, file round-trip, and LorenzoQuant→ADM→ANS end-to-end pipeline
1930
- `modules/coders/ans/dietgpu/`: vendored dietGPU rANS headers (Meta Platforms, MIT license); namespace adapted to `fz::ans`, histogram functions stripped in favor of shared `fz::module::GPU_histogram_generic`
2031
- `ANSStage`: full class definition in `ans_stage.h` — `ANSConfig` (12-byte FZM header with prob_bits + original_bytes_), 7 persistent scratch pointer fields, `isGraphCompatible()=false`, `estimateOutputSizes()`, `serializeHeader()`/`deserializeHeader()`, `saveState()`/`restoreState()`, `getRequiredInputAlignment()=4`, `estimateDeviceFootprintBytes()`, `estimateScratchBytes()`, `onFinalize()` pre-allocation path
2132
- `ANSStage::execute()` in `ans_stage.cu`: forward path (histogram → `ansCalcWeights` → `ansEncodeBatch<10,4096>` → `batchExclusivePrefixSum` → `ansEncodeCoalesceBatch<64>` → D2H header readback); inverse path (D2H header peek → `ansDecodeTable<256>` → occupancy-based `ansDecodeKernel<128,10,4096>`); `initScratch()`/`onFinalize()`/`estimateDeviceFootprintBytes()`/`estimateScratchBytes()` implementations

‎CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ add_library(fzgmod_modules
257257
modules/coders/huffman/huffman_stage.cu
258258
modules/coders/ans/ans_stage.cu
259259
modules/coders/ans/dietgpu/GpuANS.cu
260+
modules/transforms/adm/adm_stage.cu
261+
modules/transforms/adm/mapping_uint16.cu
262+
modules/transforms/adm/mapping_uint32.cu
260263
)
261264

262265
target_link_libraries(fzgmod_modules

‎THIRD_PARTY.md‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,18 @@ URL: https://github.com/facebookresearch/dietgpu
180180
**Used by:** `ADMStage` (`modules/transforms/adm/`), `MANSStage` (`modules/fused/mans/`)
181181

182182
**Relationship:**
183-
- The Adaptive Data Mapping (ADM) kernels in `ADMStage` and the fused
184-
ADM+ANS logic in `MANSStage` are algorithm-faithful reimplementations
185-
adapted from the MANS project by Huang et al. The GPU rANS component
186-
used inside these stages is covered by the dietGPU entry above.
183+
- `ADMStage` (`modules/transforms/adm/mapping_uint16.cu`,
184+
`mapping_uint32.cu`) — GPU kernels are a direct port of
185+
`nv/adm/mapping_uint16.cu` and `nv/adm/mapping_uint32.cu` from the MANS
186+
repository. Kernel logic is unchanged. Changes from the original: unused
187+
`MansParams` parameter removed; per-call `cudaMalloc`/`cudaFree` replaced
188+
by pool-allocated `AdmScratch`; `check_cuda()` replaced by `FZ_CUDA_CHECK`;
189+
namespace changed from `mans::nv::adm` to `fz::adm`; kernels renamed with
190+
`_u16`/`_u32` suffix to avoid TU-level naming conflicts; inline Chinese
191+
comments translated to English.
192+
- `MANSStage` (`modules/fused/mans/`) — to be added in a future release;
193+
will follow the fused ADM+rANS design from the MANS repository. The GPU
194+
rANS component is covered by the dietGPU entry above.
187195

188196
**License:**
189197

‎docs/stages/adm.md‎

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# ADMStage {#stage_adm}
2+
3+
**Header:** `transforms/adm/adm_stage.h`
4+
**Class:** `fz::ADMStage`
5+
**Category:** Transform (lossless)
6+
7+
**Common instantiation:**
8+
```cpp
9+
auto* adm = p.addStage<fz::ADMStage>();
10+
adm->setDtype(fz::ADMDtype::U16); // match upstream output type
11+
```
12+
13+
---
14+
15+
## What it does
16+
17+
Remaps a `uint16_t[]` or `uint32_t[]` integer stream into a compact 8-bit symbol
18+
domain, dramatically improving the compression ratio of a downstream entropy coder
19+
(typically `ANSStage`).
20+
21+
- **Forward:** `uint16_t[]` or `uint32_t[]` → opaque ADM payload (`uint8_t[]`)
22+
- **Inverse:** ADM payload → original integer array (exact reconstruction)
23+
24+
**Algorithm:** ADM partitions the input into 512-element warp blocks and computes
25+
a per-block center (mean). Each element is then encoded as a *(code, unary-signal)*
26+
pair relative to the center. The resulting byte codes have a highly skewed,
27+
low-entropy distribution ideal for GPU rANS (`ANSStage`) or Huffman coding.
28+
29+
The output is an opaque ADM payload — `getOutputDataType()` returns `UNKNOWN` to
30+
opt out of downstream type checking.
31+
32+
---
33+
34+
## Stage settings
35+
36+
| Setting | Type | Default | Purpose |
37+
|---|---|---|---|
38+
| `setDtype(ADMDtype)` | `ADMDtype` | `U16` | Input element type: `U16` or `U32` |
39+
40+
`ADMDtype::U16` expects `uint16_t` input; `ADMDtype::U32` expects `uint32_t` input.
41+
The dtype must match the upstream stage's output element type.
42+
43+
---
44+
45+
## Typical pipeline
46+
47+
### Standalone (integer array input)
48+
49+
```cpp
50+
Pipeline p(in_bytes, MemoryStrategy::PREALLOCATE);
51+
52+
auto* adm = p.addStage<ADMStage>();
53+
adm->setDtype(ADMDtype::U16);
54+
p.finalize();
55+
56+
p.compress(d_in, in_bytes, stream);
57+
```
58+
59+
### cuSZ-style Lorenzo + ADM + ANS (recommended)
60+
61+
```cpp
62+
Pipeline p(in_bytes, MemoryStrategy::PREALLOCATE);
63+
64+
auto* lrz = p.addStage<LorenzoQuantStage<float, uint16_t>>();
65+
lrz->setErrorBound(1e-3f);
66+
lrz->setQuantRadius(512);
67+
lrz->setZigzagCodes(true);
68+
69+
auto* adm = p.addStage<ADMStage>();
70+
adm->setDtype(ADMDtype::U16);
71+
p.connect(adm, lrz, "codes"); // connect to "codes" port
72+
73+
auto* ans = p.addStage<ANSStage>();
74+
p.connect(ans, adm);
75+
76+
p.finalize();
77+
```
78+
79+
**Why ADM before ANS:** `ANSStage` operates on 8-bit symbols (256-entry alphabet).
80+
Raw `uint16_t` quantization codes have a 65536-symbol alphabet, which ANS cannot
81+
encode directly. ADM acts as a symbol-domain adapter — it remaps the wide integer
82+
stream into the 8-bit domain while preserving all information losslessly.
83+
84+
---
85+
86+
## TOML configuration
87+
88+
```toml
89+
[[stage]]
90+
name = "adm"
91+
type = "ADM"
92+
dtype = "uint16" # "uint16" (default) or "uint32"
93+
inputs = [{from = "lrz", port = "codes"}]
94+
```
95+
96+
---
97+
98+
## Execution flow (CPU–GPU movement pattern) {#adm-execution}
99+
100+
### Forward pass
101+
102+
ADM uses one of two prefix-sum strategies depending on the number of warp blocks
103+
(`gsize = ⌈N / 512⌉`):
104+
105+
**Decoupled look-back path** (`gsize ≤ 1024`):
106+
107+
```
108+
GPU ←input uint16_t[] output ADM payload uint8_t[]→
109+
1. adm_map_decoupled_u16 — per-warp center + encode, decoupled prefix sum
110+
2. adm_concat_u16 — pack (code, signals) into contiguous payload
111+
└─ cudaMemcpyAsync D2H + cudaStreamSynchronize ◄── HOST BARRIER
112+
(reads actual output_lengths to determine actual_output_size_)
113+
```
114+
115+
**Thrust fallback path** (`gsize > 1024`, ~512 K+ elements for uint16_t):
116+
117+
```
118+
GPU ←input uint16_t[] output ADM payload uint8_t[]→
119+
1. adm_map_thrust_u16 — per-warp center + encode, Thrust exclusive_scan
120+
2. adm_concat_u16 — pack (code, signals) into contiguous payload
121+
└─ cudaMemcpyAsync D2H + cudaStreamSynchronize ◄── HOST BARRIER
122+
```
123+
124+
### Inverse pass
125+
126+
```
127+
GPU ←input ADM payload uint8_t[] output uint16_t[]→
128+
1. adm_decompress_u16 — decode (code, signals) → original values using stored centers
129+
```
130+
131+
**Consequence:** one host barrier per compress call — the stage is not CUDA Graph
132+
compatible. `isGraphCompatible()` returns `false`.
133+
134+
---
135+
136+
## Scratch buffers / device footprint
137+
138+
`ADMStage` pre-allocates 10 persistent device buffers from the pipeline
139+
`MemoryPool` at `finalize()` time (PREALLOCATE mode) or on the first `execute()`
140+
call (MINIMAL mode). All buffers are grow-only.
141+
142+
Let `gsize = ⌈N / 512⌉` and `sig = kMaxSignalBytes` (2 for U16, 4 for U32):
143+
144+
| Buffer | Size formula |
145+
|---|---|
146+
| `d_signal_length_` | `gsize × 4` B |
147+
| `d_output_lengths_` | `(gsize + 1) × 4` B |
148+
| `d_centers_` | `gsize × sizeof(T)` (2 or 4 B per element) |
149+
| `d_block_flags_` | `⌈gsize × 512 / 32⌉ × 4` B |
150+
| `d_codes_` | `N` B |
151+
| `d_concat_signals_` | `N × sig` B |
152+
| `d_bit_signals_` | `N × sig` B (Thrust fallback path) |
153+
| `d_loc_offset_` | `(gsize + 1) × 4` B |
154+
| `d_prefix_state_` | `(gsize + 1) × 4` B |
155+
| `d_overflow_flag_` | `4` B (debug overflow sentinel; always allocated) |
156+
157+
For 4096 uint16_t elements: ~68 KiB total device footprint.
158+
For 1M uint16_t elements: ~10 MiB total device footprint.
159+
160+
---
161+
162+
## Serialized header
163+
164+
The FZM stage header is 12 bytes:
165+
166+
```
167+
[0] dtype (0 = U16, 1 = U32)
168+
[1..3] reserved (zero)
169+
[4..11] num_elements (uint64_t LE; element count of the uncompressed input)
170+
```
171+
172+
`num_elements` is stored so that `estimateOutputSizes()` can return the exact
173+
decompressed byte count for inverse-pass output buffer allocation.
174+
175+
---
176+
177+
## Limitations {#adm-limitations}
178+
179+
**Not CUDA Graph compatible.** A device-to-host synchronization occurs in every
180+
forward call to read the actual payload size. `isGraphCompatible()` returns `false`.
181+
182+
**Bounded-diff constraint.** Each GPU thread encodes `kChunk = 16` elements into a
183+
fixed `kChunk × kMaxSignalBytes` byte buffer (`32` B for U16, `64` B for U32). The
184+
number of signal bits required per element is `⌈diff / 126⌉`, where `diff` is the
185+
element's absolute deviation from the warp-block center. If the total per-thread
186+
signal bits exceed the buffer size, the kernel writes out-of-bounds. In debug
187+
builds (`NDEBUG` not defined) an `atomicOr` sentinel detects this condition and
188+
`compress_u16`/`compress_u32` throw `std::runtime_error` rather than silently
189+
corrupting data. ADM is designed for bounded quantization codes (output of
190+
`LorenzoQuantStage` or `QuantizerStage`) — arbitrary integer arrays with large
191+
value ranges are not supported.
192+
193+
**Opaque output.** The ADM payload is not a self-describing format — it cannot be
194+
decoded without the FZM header (which stores `num_elements`). The payload must
195+
always be wrapped in a `Pipeline` with `writeToFile`/`decompressFromFile` or paired
196+
with the pipeline `decompress()` that restores the header.
197+
198+
**Dtype must be set before `finalize()`.** `setDtype()` must be called before
199+
`pipeline.finalize()` so the correct element width is used for scratch sizing and
200+
type checking.
201+
202+
---
203+
204+
## Acknowledgements
205+
206+
`ADMStage` is a direct port of the ADM encode/decode kernels from **MANS**
207+
(Wenjing Huang, Jinwu Yang, JingKai Huang, Haoquan Long, Dingwen Tao,
208+
Guangming Tan) from `nv/adm/` in the MANS repository. Kernel logic is
209+
unchanged; changes from the original are documented at the top of
210+
`modules/transforms/adm/mapping_uint16.cu` and `mapping_uint32.cu`.
211+
212+
> Wenjing Huang, Jinwu Yang, JingKai Huang, Haoquan Long, Dingwen Tao,
213+
> Guangming Tan. *MANS: Multidimensional Adaptive Numerical Compressor for
214+
> Scientific Data.* https://github.com/hpdps-group/MANS
215+
216+
See `THIRD_PARTY.md` for the full BSD-3-Clause license text.

‎docs/stages/ans.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,8 @@ one in every inverse call (to read the header before decoding).
176176

177177
**Byte-level encoding only.** `ANSStage` operates on `uint8_t` symbols (256-entry
178178
alphabet). For multi-byte integer streams (e.g., `uint16_t` quantization codes),
179-
pair it with `ADMStage` (coming in Phase 2) which remaps the wide symbol space into
180-
the 8-bit domain before ANS coding — or use `MANSStage` (Phase 3) for the fused
181-
path.
179+
pair it with `ADMStage` (\ref stage_adm) which remaps the wide symbol space into
180+
the 8-bit domain before ANS coding — or use `MANSStage` for the fused path.
182181

183182
**Compression ratio depends on upstream symbol compactness.** ANS achieves its
184183
theoretical Shannon entropy bound only when the symbol distribution is known at

‎docs/stages/transforms.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
|---|---|
55
| \subpage stage_zigzag | Zigzag encode/decode (signed <-> unsigned) |
66
| \subpage stage_negabinary | Negabinary encode/decode (signed <-> unsigned) |
7+
| \subpage stage_adm | Adaptive Data Mapping — remaps u16/u32 into compact 8-bit domain (MANS) |

‎include/fzgpumodules.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@
3131
#include "shufflers/bitshuffle/bitshuffle_stage.h"
3232
#include "coders/huffman/huffman_stage.h"
3333
#include "coders/ans/ans_stage.h"
34+
#include "transforms/adm/adm_stage.h"

‎include/fzm_format.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ enum class StageType : uint16_t {
9191
BITSHUFFLE = 17,
9292
RZE = 18,
9393
ANS = 20, ///< TODO: describe this stage
94+
ADM = 19, ///< TODO: describe this stage
9495
};
9596

9697
/**
@@ -313,6 +314,7 @@ inline std::string stageTypeToString(StageType type) {
313314
case StageType::RZE: return "RZE";
314315
case StageType::LORENZO: return "Lorenzo";
315316
case StageType::ANS: return "ANS";
317+
case StageType::ADM: return "ADM";
316318
default: return "Unknown";
317319
}
318320
}

‎include/stage/stage_factory.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "coders/bitpack/bitpack_stage.h"
1919
#include "coders/huffman/huffman_stage.h"
2020
#include "coders/ans/ans_stage.h"
21+
#include "transforms/adm/adm_stage.h"
2122

2223
#include <memory>
2324
#include <stdexcept>
@@ -247,6 +248,13 @@ inline Stage* createStage(StageType type, const uint8_t* config, size_t config_s
247248
break;
248249
}
249250

251+
case StageType::ADM: {
252+
auto* s = new ADMStage();
253+
s->deserializeHeader(config, config_size);
254+
stage = s;
255+
break;
256+
}
257+
250258
default:
251259
throw std::runtime_error("Unknown stage type: "
252260
+ std::to_string(static_cast<uint16_t>(type)));

0 commit comments

Comments
 (0)