[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