[llvm] [NVPTX] Promote non-standard integer global widths to .u{8,16,32,64} (PR #197135)
via llvm-commits
llvm-commits at lists.llvm.org
Tue May 12 02:21:53 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-backend-nvptx
Author: Schropim (DingchenZhu)
<details>
<summary>Changes</summary>
## Summary
NVPTX globals whose IR type is a non-standard integer width (anything other
than `i1`, `i8`, `i16`, `i32`, `i64`) currently emit invalid PTX. For example
`@<!-- -->g = addrspace(1) constant i2 1` lowers to:
```
.global .align 1 .u2 g = 1;
```
which `ptxas` rejects with `Parsing error near '.u2': syntax error`. PTX only
defines `.u{8,16,32,64}` as fundamental integer storage types.
This PR fixes the asm printer to round such widths up to the next supported
PTX storage type, and to zero-extend the initializer literal to match. Both
sides have to move together — a type-only fix would turn the current
compile-time error into a silent miscompile for any value whose top bit was
set in the pre-promotion width.
Fixes #<!-- -->154337.
## Two commits
1. **`[NVPTX] Promote non-standard integer global widths to .u{8,16,32,64}`**
Introduces a file-local helper `getPromotedPtxStorageBits` that encodes the
"round up to next supported PTX storage type" rule in a single place, and
uses it from `getPTXFundamentalTypeStr` for the type specifier. `i1` still
maps to `.pred` (other callers of this helper depend on it); the two storage
paths that would otherwise see `i1` short-circuit it to `.u8` before the
call.
2. **`[NVPTX] Zero-extend literal when integer global is width-promoted`**
Reuses the same helper from `printScalarConstant`. The old code printed the
`ConstantInt` via `APInt`'s default signed formatter, so any value whose
top bit was set in the original width became a negative literal in the
promoted storage:
| Input | Before fix | After fix |
|-------------------|-------------------------------|---------------------------------|
| `i12 -1` (0xFFF) | `.u16 g = -1;` (cubin 0xFFFF) | `.u16 g = 4095;` (cubin 0x0FFF) |
| `i40 -1` | `.u64 g = -1;` (cubin all 1s) | `.u64 g = 1099511627775;` (low 40 bits set) |
The fix zero-extends the `APInt` to the promoted width and prints unsigned.
For natively supported widths (`i8`/`i16`/`i32`/`i64`) the cubin byte
pattern is unchanged — `ptxas` reinterprets `-1` and `2^N - 1` to the same
target-typed unsigned representation.
The two commits can be squashed at merge time if reviewers prefer.
## `i1` behaviour change (disclosure)
`i1 true` globals previously emitted `.u8 g = -1` (cubin byte `0xFF`). With
this PR they emit `.u8 g = 1` (cubin byte `0x01`), matching LLVM IR's
canonical zero-extension of `i1 true`. Both encodings are truthy, so this has
no functional impact on standard boolean uses (conditional branches, `zext`
to a wider integer, etc.). The change only affects byte-level inspection of
the storage, such as `__builtin_memcpy(dst, &g_i1, 1)` followed by reading
the destination byte. No such pattern observed in the LLVM or MLIR test
suites.
A grep confirms no in-tree test locked in the prior `0xFF` byte pattern:
```bash
grep -rE '\.u(8|16|32|64) +\S+ *= *-[0-9]' llvm/test/ mlir/test/ # no matches
```
## Test coverage
New test: `llvm/test/CodeGen/NVPTX/int-globals-promotion.ll`. Each case pins
down an independent dimension so future regressions surface immediately:
| Case | Pins |
|--------------------|-------------------------------------------------------------------|
| `i1 true` | `i1`-zero-extension lock-in |
| `i2 1` | original #<!-- -->154337 repro, lower edge of `.u8` bucket |
| `i7 5` | upper edge of `.u8` bucket |
| `i12 -1` | `.u16` bucket + value zero-extension |
| `i24 -1` | `.u32` bucket + value zero-extension |
| `i34 8589934591` | `.u64` bucket pinned from the value side, value > 2^32 (positive — sign-bit pinning is covered by `i40`) |
| `i40 -1` | `.u64` bucket + value zero-extension |
The test runs `%ptxas-verify` when available so the emitted PTX is also
end-to-end checked against the real assembler.
## Risk & impact
Low. The two changed functions are NVPTX AsmPrinter helpers used only
within `llvm/lib/Target/NVPTX/`:
- `getPTXFundamentalTypeStr` — 3 callers, all in `NVPTXAsmPrinter.cpp`
(global definition, extern global declaration, kernel scalar param).
- `printScalarConstant` — 1 caller, in `NVPTXAsmPrinter.cpp` (global
initializer emission).
No IR semantics, no DAG/SDAG logic, no MIR. Compile-time cost is a handful
of additional integer comparisons per emitted global or kernel scalar param,
unmeasurable.
## Local verification
Built with `clang;lld;mlir` projects, `X86;NVPTX` targets, `RelWithDebInfo`
+ assertions on Ubuntu 24.04 with CUDA 12.6.20 (`ptxas` from CUDA 12.6
toolkit, PTX ISA 8.5):
- `ninja check-llvm-codegen-nvptx`: **500 / 500 pass** (baseline on
unmodified `origin/main`: 499 / 499 — the new
`int-globals-promotion.ll` is the 500th test, added in this PR).
- `ninja check-mlir`: **3583 pass / 0 unexpected fail / 591 unsupported /
1 XFAIL** (pre-existing on main, verified against baseline before
applying the patch).
- `git clang-format HEAD~2`: no reformatting.
- `%ptxas-verify` step inside the new test successfully assembles all 7
emitted PTX globals.
## Out of scope
This PR targets the global-initializer emission path (`EmitVariable` →
`printScalarConstant`). Two related paths are intentionally left alone:
- **`bufferLEByte`** (used by array / struct globals) operates byte-by-byte
via `getTypeStoreSize`, never goes through `printScalarConstant`. Sub-byte
array elements (e.g. `[N x i2]`) are a separate codegen question not
addressed here.
- **`emitFunctionParamList`** (kernel scalar params) emits only the type
specifier — already routes through `getPTXFundamentalTypeStr` and so
picks up the type-side fix automatically. There is no parameter-side
initializer literal to print.
---
Full diff: https://github.com/llvm/llvm-project/pull/197135.diff
2 Files Affected:
- (modified) llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp (+30-5)
- (added) llvm/test/CodeGen/NVPTX/int-globals-promotion.ll (+55)
``````````diff
diff --git a/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp b/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
index 7d75cd8cebd6a..7f01477d45378 100644
--- a/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
+++ b/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
@@ -96,6 +96,25 @@ using namespace llvm;
#define DEPOTNAME "__local_depot"
+// PTX only defines .u{8,16,32,64} as fundamental integer storage types.
+// Round any other width (i2, i3, i7, i12, i24, i33, ...) up to the next
+// supported size. This must be applied consistently to both the emitted
+// type specifier and the emitted initializer literal so the cubin's byte
+// pattern matches the original IR ConstantInt.
+static unsigned getPromotedPtxStorageBits(unsigned NumBits) {
+ if (NumBits <= 8)
+ return 8;
+ if (NumBits <= 16)
+ return 16;
+ if (NumBits <= 32)
+ return 32;
+ if (NumBits <= 64)
+ return 64;
+ // Caller handles >64 bits separately (e.g. i128 globals are emitted as
+ // .b8 byte arrays in emitPTXGlobalVariable).
+ return NumBits;
+}
+
static StringRef getTextureName(const Value &V) {
assert(V.hasName() && "Found texture variable with no name");
return V.getName();
@@ -1259,10 +1278,8 @@ NVPTXAsmPrinter::getPTXFundamentalTypeStr(Type *Ty, bool useB4PTR) const {
unsigned NumBits = cast<IntegerType>(Ty)->getBitWidth();
if (NumBits == 1)
return "pred";
- if (NumBits <= 64) {
- std::string name = "u";
- return name + utostr(NumBits);
- }
+ if (NumBits <= 64)
+ return "u" + utostr(getPromotedPtxStorageBits(NumBits));
llvm_unreachable("Integer too large");
break;
}
@@ -1612,7 +1629,15 @@ void NVPTXAsmPrinter::printFPConstant(const ConstantFP *Fp,
void NVPTXAsmPrinter::printScalarConstant(const Constant *CPV, raw_ostream &O) {
if (const ConstantInt *CI = dyn_cast<ConstantInt>(CPV)) {
- O << CI->getValue();
+ // The emitted PTX storage type for this ConstantInt may have been
+ // width-promoted by getPromotedPtxStorageBits (e.g. an i2 global is
+ // emitted as .u8). Zero-extend the value so its bit pattern matches the
+ // IR ConstantInt's, and print as unsigned so that values whose top bit
+ // is set in the original width do not become signed-extended negative
+ // literals in the promoted storage.
+ CI->getValue()
+ .zextOrTrunc(getPromotedPtxStorageBits(CI->getBitWidth()))
+ .print(O, /*isSigned=*/false);
return;
}
if (const ConstantFP *CFP = dyn_cast<ConstantFP>(CPV)) {
diff --git a/llvm/test/CodeGen/NVPTX/int-globals-promotion.ll b/llvm/test/CodeGen/NVPTX/int-globals-promotion.ll
new file mode 100644
index 0000000000000..dbaa87c965c2b
--- /dev/null
+++ b/llvm/test/CodeGen/NVPTX/int-globals-promotion.ll
@@ -0,0 +1,55 @@
+; RUN: llc < %s -mtriple=nvptx64 -mcpu=sm_20 | FileCheck %s
+; RUN: %if ptxas %{ llc < %s -mtriple=nvptx64 -mcpu=sm_20 | %ptxas-verify %}
+
+; Test that NVPTX promotes globals with non-standard integer widths
+; (anything other than i1/i8/i16/i32/i64) to the next supported PTX
+; integer storage type (.u8/.u16/.u32/.u64), and that the emitted
+; initializer literal zero-extends to that storage width so the cubin
+; bit pattern matches the IR ConstantInt. See issue #154337.
+
+target triple = "nvptx64-nvidia-cuda"
+
+; ----------------------------------------------------------------------------
+; i1: pin down the latent fix. Previously emitted `.u8 g_i1 = -1` (cubin byte
+; 0xFF). The byte pattern now matches the IR's canonical zero-extension of
+; `i1 true` (cubin byte 0x01). Both are truthy, so this is functionally
+; transparent for boolean uses; the fix matters for byte-level inspection.
+; CHECK: .global .align 1 .u8 g_i1 = 1;
+ at g_i1 = addrspace(1) constant i1 true, align 1
+
+; ----------------------------------------------------------------------------
+; Sub-byte: lower and upper edges of the .u8 bucket.
+; CHECK: .global .align 1 .u8 g_i2 = 1;
+ at g_i2 = addrspace(1) constant i2 1, align 1
+; CHECK: .global .align 1 .u8 g_i7 = 5;
+ at g_i7 = addrspace(1) constant i7 5, align 1
+
+; ----------------------------------------------------------------------------
+; Sign-bit case in the .u16 bucket: i12 -1 = 0xFFF must zero-extend to 4095,
+; not emit as -1 (which ptxas would silently reinterpret as the 16-bit
+; unsigned representation 0xFFFF, corrupting the top 4 bits).
+; CHECK: .global .align 2 .u16 g_i12_neg = 4095;
+ at g_i12_neg = addrspace(1) constant i12 -1, align 2
+
+; ----------------------------------------------------------------------------
+; Sign-bit case in the .u32 bucket: i24 -1 = 0xFFFFFF must zero-extend to
+; 16777215, not emit as -1 (which would corrupt the top 8 bits of the .u32
+; storage to 0xFFFFFFFF).
+; CHECK: .global .align 4 .u32 g_i24_neg = 16777215;
+ at g_i24_neg = addrspace(1) constant i24 -1, align 4
+
+; ----------------------------------------------------------------------------
+; Positive value at the .u64 bucket: value > 2^32 means it would truncate
+; in any smaller bucket, pinning the bucket choice from the value side too.
+; i34 (not i33) is used because i33's signed range cannot represent a value
+; > 2^32 without setting its sign bit -- a hazard exercised by the next case.
+; CHECK: .global .align 8 .u64 g_i34 = 8589934591;
+ at g_i34 = addrspace(1) constant i34 8589934591, align 8
+
+; ----------------------------------------------------------------------------
+; Sign-bit case in the .u64 bucket: i40 -1 = 0xFF_FFFF_FFFF must zero-extend
+; to 1099511627775 (low 40 bits = 1, top 24 bits = 0). Pre-fix this would
+; emit `.u64 g = -1` (cubin byte pattern 0xFFFF_FFFF_FFFF_FFFF), silently
+; corrupting the top 24 bits.
+; CHECK: .global .align 8 .u64 g_i40_neg = 1099511627775;
+ at g_i40_neg = addrspace(1) constant i40 -1, align 8
``````````
</details>
https://github.com/llvm/llvm-project/pull/197135
More information about the llvm-commits
mailing list