# Multi-Card Communication-Computation Fusion Operator Precision Verification > Precision verification for "communication + computation" fusion operators in multi-card (NPU) environments. > Core approach: **Decompose fusion operators into communication and computation parts for independent verification**, then **compare against PyTorch distributed official implementation as the overall baseline**, refining "end-to-end success" into "every link is correct". ## 1 Background and Problem Fusion operators compress cross-card data movement (collective) and local computation (gemm / element-wise / reordering) into a single kernel in `forward()`. This approach performs well but creates difficulties in precision determination: 1. **Difficulty identifying error source**: When overall output doesn't match golden, it's hard to directly determine whether communication moved wrong data or computation produced incorrect results. 2. **Single-pass cannot reproduce**: Multi-card scenarios may exhibit inter-process timing-dependent errors that single-card tests mask communication path issues. 3. **Baseline must be trustworthy**: PyTorch distributed implementation is our reference baseline. If the baseline itself is misconfigured (world_size, grouping, backend), all comparisons are invalid. Therefore, precision verification should not only perform "final result comparison", but should be layered: ``` Layer 1: Communication path verification — Only concerned with whether data arrives, assembles, and reduces as expected Layer 2: Single-card computation semantics — Only concerned with whether NPU kernel numerical results are correct Layer 3: Fusion overall baseline comparison — On real multi-card, full comparison against PyTorch distributed implementation ``` Only when all three layers pass can the precision conclusion for the fusion operator be given. ## 2 Overall Verification Framework ### 2.1 Three Execution Entries | Layer | Verification Target | Execution Method | Determination Goal | |------|----------|----------|----------| | Communication verification | Communication sub-module (isolated) | Single or multi-core, sending without computing is core | Move/assemble/reduce results correct | | Computation verification | Local computation kernel | Single card, fed fixed input | Numerical semantics correct (including NaN/boundary) | | Overall comparison | Fusion operator | Real multi-card distributed | Consistent with PyTorch distributed baseline | ### 2.2 Common Verification Tools The following tools are recommended to be fixed in the project to reduce manual comparison: - **Golden generation**: Use PyTorch distributed official APIs as truth source (`dist.all_gather` / `all_reduce` / `reduce_scatter` / `all_to_all_single` etc.). - **Fixed seed**: All random inputs use fixed seed to ensure reproducibility. - **Relaxed + strict dual threshold**: Numerical comparison uses `allclose` (default relative/absolute tolerance), and separately performs **bit-wise or extremely strict** checks when input happens to be binary-exactly representable. ## 3 Precision Comparison and Error Analysis All three layers ultimately fall to the same action: numerically compare "calculation result" with "reference result (Golden)". This section defines the unified implementation, evaluation criteria, and considerations for this action. ### 3.1 Obtaining Golden Golden is the source of truth determination, prioritized from high to low: 1. **Equivalent Torch operator** results computed on CPU / GPU at high precision (`fp32` / `fp64`); 2. **Same Triton operator** computation results on CPU / GPU (for verifying implementation semantic consistency). > Note: Golden should be computed at higher precision when possible to avoid including reference implementation's own rounding errors in tolerance. If target dtype is `fp16`/`bf16`, it's recommended to compute golden in `fp32` then convert back to target dtype. ### 3.2 Comparison Determination Entry > In distributed environments, fusion operators are no longer just "local computation" on a single card, but **first move data across cards, then complete reordering/computation locally** (communication + computation fusion). The following uses **HCCL Reverse All-to-All** fusion operator as an example: each rank holds a local shard, and moves all ranks' shards across cards through symmetric memory, reordering them into complete results. Example from `04-ascend-reverse-all2all.py`. Distributed Reverse All-to-All example (fused communication + computation): https://gitcode.com/Ascend/Triton-distributed-ascend/blob/master/tutorials/ascend/04-ascend-reverse-all2all/04-ascend-reverse-all2all.py > Note (example key points): > - Use `dist.init_process_group(backend="hccl")` to initialize process group, and complete cross-card data movement through ACL symmetric memory (`aclshmem_*`); > - Fusion kernel `kernel_hccl_reverse_a2a_pipelined` completes both cross-card movement and local reordering within a single `@triton.jit` kernel (Producer/Consumer phases); > - Golden constructed using PyTorch distributed official implementation `torch_reverse_a2a` (`dist.all_to_all_single`), then compared with fusion operator output using `assert_close`; > - Precision comparison collects pass flags from each rank via `dist.all_gather`, only passes when all pass. Corresponding run command (multi-card launch, `LOCAL_RANK` automatically injected by torchrun): ```bash torchrun --nproc_per_node=2 --master_port=29500 04-ascend-reverse-all2all.py ``` ### 3.3 Precision Comparison Function (Unified Entry) ```python def compare_precision(cal, ref, rtol=1e-3, atol=1e-3): """ Precision comparison function: select appropriate comparison strategy based on data type. Args: cal: Calculation result ref: Reference result rtol: Relative error tolerance atol: Absolute error tolerance Raises: AssertionError: Raised when precision is insufficient """ assert cal.dtype == ref.dtype, f"dtype mismatch: {cal.dtype} vs {ref.dtype}" tensor_dtype = cal.dtype if tensor_dtype == torch.float16: torch.testing.assert_close(ref, cal, rtol=rtol, atol=atol, equal_nan=True) elif tensor_dtype == torch.bfloat16: torch.testing.assert_close(ref, cal, rtol=5e-3, atol=5e-3, equal_nan=True) elif tensor_dtype == torch.float32: torch.testing.assert_close(ref, cal, rtol=1e-5, atol=1e-5, equal_nan=True) elif tensor_dtype in [torch.int64, torch.int32, torch.int16, torch.int8]: assert torch.equal(cal, ref), f"Integer tensors are not equal for dtype {tensor_dtype}" elif tensor_dtype == torch.bool: assert torch.equal(cal, ref), "Boolean tensors are not equal" else: raise ValueError(f"Unsupported tensor dtype: {tensor_dtype}") print(f"dtype: {tensor_dtype} — Precision check passed.") ``` ### 3.4 Determination Criteria - `torch.testing.assert_close` / `torch.equal` **not raising exception → pass**, otherwise fail. - `torch.testing.assert_close`: Passes if tensors are approximately equal within specified tolerance, otherwise raises `AssertionError`. - `torch.equal`: Returns `True` only if shapes are identical and all elements are absolutely equal at binary level. - `torch.testing.assert_close` internal criterion: ``` |cal - ref| <= atol + rtol * |ref| ``` That is, absolute error must fall within the dynamic boundary formed by "absolute tolerance + relative tolerance × reference value magnitude". When `ref` is close to 0, `atol` dominates; when `ref` is large, `rtol` dominates. Recommended tolerances by data type: | Data Type | rtol | atol | Description | |----------|------|------|------| | float32 | 1e-5 | 1e-5 | Strict | | float16 | 1e-3 | 1e-3 | Lower precision, appropriately relaxed | | bfloat16 | 5e-3 | 5e-3 | Lower precision, appropriately relaxed | | int8/16/32/64 | — | — | Must be completely consistent (`torch.equal`) | | bool | — | — | Must be completely consistent (`torch.equal`) | > Note: The above tolerances are engineering defaults. Fusion operators may slightly relax `fp16`/`bf16` due to stacked communication and computation errors, but **must be explicitly documented in test documentation**, not silently relaxed. ### 3.5 Considerations - **NaN / Inf handling**: `equal_nan=True` treats NaN as equal. If strict detection of NaN differences is needed, should be set to `False`. Whether NaN propagates is operator semantics, testing should choose this switch according to semantics. - **Integer types**: `int` / `bool` do not allow any error, must be strictly consistent. - **Device alignment**: When comparing across devices, must move `cal` and `ref` to **same device (e.g., CPU)** before comparison to avoid misjudgment from underlying representation differences. Note `triton_cal.cpu()` should be placed before comparison. - **dtype alignment**: Assert `cal.dtype == ref.dtype` before comparison, direct failure if dtype mismatch, error message must be clear. ## 4 Communication Path Verification The goal of communication verification is to **prove cross-card data movement itself is correct**, independent of computation. Therefore, communication behavior should be isolated from the fusion kernel and constructed and compared separately. ### 4.1 Isolation Method - Construct a "communication only, no computation" test kernel: it only `load`/`store` from peer rank's symmetric memory, assembles shards, or performs pure `identity` reduction (no gemm / element-wise transformation). - If operator structure makes it difficult to export communication kernel separately, can use "identity computation" substitute: replace computation part with `x = x`, retain communication and reordering structure, thereby separately exposing communication behavior. ### 4.2 Verification Points For each collective type, lock down its key invariants: | Communication Primitive | Must-Check Invariants | |----------|-----------| | `all_gather` | Concatenated result received by each rank is local shards from each rank concatenated in rank order; length = world_size × local length | | `all_reduce` | Each rank's result is consistent; value = sum of corresponding elements from each rank (for SUM) | | `reduce_scatter` | Shard held by rank i = reduced result of corresponding shard from each rank | | `all_to_all_single` | Block received by rank i from rank j = block rank j wants to send to i; block content, block order, block size all correspond one-to-one | ### 4.3 Determination - For each rank, independently compare communication output with PyTorch corresponding collective's golden, **only passes when every rank passes**. - At communication layer, don't use relaxed element-wise replacement to mask error paths; prioritize using **sufficiently distinctive input** (e.g., each rank uses blocks with different values), so that any misalignment is immediately exposed. ### 4.4 Focus Points - **rank order / block granularity**: Most error-prone is "who sent to whom, spliced at which position". - **Symmetric memory layout**: Once offset / stride / shard shape doesn't match launcher parameters, cross-rank misalignment occurs without process errors. - **Synchronization**: Communication correctness also depends on barrier / wait / notify order; communication verification simultaneously observes whether hanging or data not ready occurs. ## 5 Computation Semantics Verification The goal of computation verification is to **prove local numerical computation is correct**, independent of communication. On single card, directly give kernel deterministic input for comparison, can quickly locate numerical errors. ### 5.1 Isolation Method - In single-card environment, use "identity communication" to replace real communication: data doesn't cross cards, directly use local memory, making kernel's computation path testable separately. - Maintain **same tile / block / num_stages / layout parameters** as fusion kernel to avoid "unit test passes, fusion fails" parameter drift. ### 5.2 Verification Points - **Numerical semantics**: Allclose with `torch` corresponding operation (`matmul` / `elementwise`) under same input. - **NaN / Inf propagation**: If operator semantics require NaN propagation, use input containing NaN to confirm propagation behavior meets expectations. - **Boundary and tail**: Focus on non-divisible blocks, whether mask / boundary padding correctly handles the last block. - **Precision level**: `fp16`/`bf16` need to explain tolerance selection, `fp32` can be tightened. ### 5.3 Determination - Only after computation layer single-card all pass, proceed to overall comparison, avoiding bringing computation bugs to multi-card troubleshooting. ## 6 Fusion Overall Baseline Comparison Execute fusion operator in real multi-card environment, perform full comparison with PyTorch distributed baseline. This is the sole adjudication entry for final precision conclusion. ### 6.1 Baseline Configuration Key Points - **Baseline implementation**: Use PyTorch distributed official API (`dist.init_process_group(backend="hccl")` + corresponding collective) to build reference implementation completely consistent with operator input/output. - **world_size / rank alignment**: Baseline and fusion operator must use same process group size and grouping; any inconsistency will invalidate comparison. - **Input completely consistent**: For comparison, give baseline and fusion operator **same input tensor** (or copy to independent memory by rank but content bit-wise identical), eliminate input differences. - **Layout and dtype consistent**: Input/output memory layout (contiguous/non-contiguous, NCHW/NHWC), dtype must all be consistent. ### 6.2 Comparison Process ``` 1. Initialize multi-card process group, barrier ensures each rank is ready 2. Each rank generates input with fixed seed 3. Separately run PyTorch baseline and fusion operator 4. Each rank independently compares output allclose 5. Aggregate: collect pass flags from each rank, only passes when all pass ``` ### 6.3 Determination - **Per-rank independent allclose**: Each rank's output is compared with that rank's golden. - **Cross-rank consistency check (optional)**: For operators requiring consistent output across ranks (like all_reduce), additionally confirm each rank's result is consistent. - **Determination criteria**: Only when all ranks all allclose, and no hanging, no crash, does overall precision pass. ## 7 Common Problem Troubleshooting | Phenomenon | Possible Cause | Recommended Action | |------|----------|----------| | Overall inconsistent, but communication layer passes | Computation semantics bug | Return to Layer 2 to troubleshoot computation | | Overall inconsistent, but computation layer passes | Communication move/assembly error | Return to Layer 1 to troubleshoot communication | | Single rank passes, multi rank fails | rank order/block/symmetric memory layout issue | Focus on Layer 1 Section 4.4 | | Results consistent but differ at extremes | Precision/tolerance selection inappropriate | Explain numerical precision level, adjust tolerance | | Process hanging | Synchronization (barrier/wait/notify) out of order | Check synchronization order, add barrier if necessary | ## 8 Implementation Checklist - [ ] Fixed seed, reproducible input generator - [ ] Communication isolation test (identity substitute, per-collective verification) - [ ] Single-card computation semantics test (including NaN / boundary) - [ ] Multi-card overall vs PyTorch distributed baseline comparison - [ ] Allclose pass flag collection and aggregation for each rank - [ ] Precision level and tolerance explanation written into documentation ## 9 Summary The correct approach to precision verification is **layered**: 1. **Separate communication and computation**, each using corresponding minimal unit to falsify/verify correctness; 2. **Finally on real multi-card**, use PyTorch distributed implementation as authoritative baseline comparison. Layering can quickly attribute "overall mismatch" to communication or computation, avoiding needle-in-haystack search in multi-card, hard-to-reproduce environments.