Skip to content

fix(cuda-macros): correct gpu_printf default float and unsigned formatting - #1353

Merged
nihalpasham merged 8 commits into
NVIDIA:mainfrom
YeonwooSung:fix/gpu-printf-default-format
Oct 19, 2026
Merged

nihalpasham merged 8 commits into
NVIDIA:mainfrom
YeonwooSung:fix/gpu-printf-default-format

Conversation

@YeonwooSung

@YeonwooSung YeonwooSung commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

What this adds

gpu_printf! now chooses the C conversion from the argument's type for every placeholder without a type character. Before, {} and {:.N} packed every argument as i64 and printed %lld, so floats printed as truncated integers, {:.2} on a float printed a zero-padded integer, and unsigned values above i64::MAX printed negative. The per-type table cuda-device has carried since the first release, GpuPrintfArg, is now the single source of that decision.

let big: u64 = u64::MAX;
let x: f32 = 3.14159;
gpu_printf!("{} {} {:.2}\n", big, x, x);

before:  -1 3 03
after:   18446744073709551615 3.141590 3.14
#[kernel]
pub fn show(data: &[f32]) {
    let tid = thread::index_1d().get();
    gpu_printf!("thread {}: data[{}] = {}\n", tid, tid, data[tid]);
    // thread 0: data[0] = 1.100000
}

What we fixed

  • {}, {:8}, {:08}, {:+} and {:.N} without a type character took the argument through as i64 and %lld, whatever its type. They now resolve the conversion from the argument's GpuPrintfArg impl at compile time: %d or %lld for signed integers, %u or %llu for unsigned, %f for floats, %d for bool, %p for raw pointers.
  • The compile-time format string is returned as &'static [u8; N]. The importer rejects an anonymous slice constant, so the first version of this change failed device codegen on every {} over a binding and broke the printf, dynamic_smem, debug-tests and toolchain_pairing_smoke examples.
  • The format buffer is sized from the format skeleton instead of a 48-byte-per-placeholder estimate, which overflowed const evaluation at 54 literal % and padded every format global.
  • All arguments, runtime width included, are evaluated left to right, as println! does.
  • Sign flags on an unsigned argument and runtime precision {:.*} are compile-time errors; device printf leaves the first undefined and does not support the second.

Default conversion contract

  • A placeholder without a type character accepts any GpuPrintfArg type: i8 to i64, isize, u8 to u64, usize, f32, f64, bool, *const T, *mut T.
  • Literals follow Rust's rules: an unsuffixed integer is i32, an unsuffixed float is f64.
  • Precision on an integer argument is C's minimum digit count; on a float it is the number of digits after the point.
  • + and space flags need a signed integer or float argument.
  • char takes {:c}; a fieldless enum needs an as i32 cast; i128 and u128 are not accepted.
  • Explicit type characters ({:x}, {:d}, {:.2f}, {:e}, ...) keep their previous casts and conversions.
Implementation
  • Each placeholder without a type character becomes a generic parameter __Ti: GpuPrintfArg of a per-call-site #[inline(always)] helper. Its inline const calls cuda_device::debug::__gpu_printf_format::<N>, a const fn next to __gpu_assert_c_string, with the literal parts and per-placeholder specs as data; the trait's FORMAT_CHAR, IS_64BIT and IS_FLOAT select ll and the type character after monomorphization.
  • Arguments are packed through GpuPrintfArg::promote into a #[repr(C)] struct whose fields are the promoted types at natural alignment, the layout vprintf reads.
  • N is computed by the macro: literal parts with % doubled, fixed specifiers, prefix plus four bytes per typed placeholder, one NUL. The builder asserts the fit and NUL-terminates.
  • The byte-literal fast path remains for call sites whose placeholders all carry a type character.
  • The builder is unit-tested on the host (conversion per type, %% doubling, fit, rejection of + on unsigned); the macro tests check the expansion; the printf example checks the device output.

Verification

Checked at a61a7e965 (nightly-2026-10-11, RTX 5090):

  • ✅ cargo test -p cuda-macros --all-targets (143 unit tests plus the trybuild cases) and cargo test -p cuda-device --all-targets (124) pass, builder and expansion tests included. Clippy with warnings as errors on both, rustdoc, cargo oxide fmt --check, the 18 guards and the lock check are clean.
  • ✅ Fails before, passes after: with main's macro, 8 of the 11 rewritten macro tests fail; the submitted head fails device codegen on the printf example (Unsupported constant type in translate_constant).
  • ✅ cargo oxide run printf: === ALL PRINTF TESTS PASSED === with the new Test 8 lines matching the README, memcheck 0 errors; the same values through the libNVVM route.
  • ✅ dynamic_smem, debug-tests and toolchain_pairing_smoke build again; device_ffi_test's fixed-route format globals are unchanged.
  • ✅ On main the same box printed 03 for {:.2} of 3.14159, -1 for {} of u64::MAX and 3 for {} of an f32 binding.

Caveats

  • Code that relied on the old as i64 path changes: {} on char and on a fieldless enum no longer compiles (use {:c} or as i32), {} on a raw pointer prints %p hex instead of a decimal, {} on a float prints six decimals where it printed a truncated integer.
  • Precision on integers keeps C semantics ({:.3} on -42 prints -042); Rust ignores precision on integers.
  • The importer still rejects anonymous slice constants in device code; that is a separate compiler gap.
  • Device printf formats on the host, so each conversion's output follows the host C library.

`{:.N}` has no type character, so the default arm packed the argument
as i64 and emitted %.Nlld. C precision on an integer conversion is a
minimum digit count, which turned 3.14159 into 03 instead of 3.14.

Signed-off-by: YeonwooSung <neos960518@gmail.com>
Default `{}` packed every argument as i64 and emitted %lld, so a u64
above i64::MAX printed as a negative number. Visible unsigned arguments
now cast to u64 and use %llu. Bindings whose type the macro cannot see
go through GpuPrintfArg, which also keeps floats off the integer path.

Signed-off-by: YeonwooSung <neos960518@gmail.com>
…printf-default-format

Signed-off-by: YeonwooSung <neos960518@gmail.com>

# Conflicts:
#	cuda-oxide/crates/cuda-macros/src/tests/printf.rs
@copy-pr-bot

copy-pr-bot Bot commented Oct 5, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@nihalpasham nihalpasham added the cuda-oxide SIMT programming model: rustc backend, cargo oxide, cuda-device, cuda-host, book label Oct 11, 2026
# Conflicts:
#	cuda-oxide/crates/cuda-macros/src/tests/mod.rs
…ment type

Every placeholder without a type character now takes its C conversion
from the argument's GpuPrintfArg impl, through a generic helper whose
inline const assembles the format string. The token classifier and the
`as f64` precision arm are gone, so the trait is the single policy:
`{}` on a u64 above i64::MAX prints %llu, on a float %f, and `{:.0}` on
u64::MAX keeps all 20 digits.

The helper returns `&'static [u8; N]`. The device importer rejects an
anonymous slice constant, which made every `{}` on a binding fail device
codegen and broke the printf, dynamic_smem, debug-tests and
toolchain_pairing_smoke examples.

The format string is assembled by a const fn in cuda-device from the
literal parts and per-placeholder specs the macro passes as data, and N
is computed from the same data, so the buffer no longer overflows at 54
literal percent signs and is no longer padded by 48 bytes per
placeholder. The builder is unit-tested on the host, and one reachable
expansion test keeps the inline const evaluated at build time.

Every argument, runtime width included, is evaluated into a local in
source order, so evaluation stays left to right. The locals carry
mixed-site spans, so an argument expression cannot see an earlier one.

Sign flags on an unsigned or pointer argument are a const-evaluation
error, and runtime precision `{:.*}` is a macro error: device printf
takes `*` for the width only. The macro doc table states the contract,
including that char, fieldless enums, i128 and u128 are not accepted.

Signed-off-by: nihalpasham <nihalp@nvidia.com>
…ample

Add a kernel with the cases that printed wrong values through %lld:
`{}` on u64::MAX, on f32 and f64 bindings, on a slice element and on a
raw pointer, `{:.2}` on a float binding, `{:08}` on a u64 and one mixed
line. The float kernel's "default" lines now use `{}` and `{:.2}`
instead of `{:.6f}` and `{:.2f}`, so the example exercises the default
route that the smoketest and examples-compile lanes build.

Correct the README's u64 line: the source prints 1311768467463790320
for 0x1234_5678_9ABC_DEF0. Add the expected output of the new kernel.

Signed-off-by: nihalpasham <nihalp@nvidia.com>
The cuda-device README and the API quick reference showed a C-style
call with `%d` and `%f`, two arguments and no placeholder, which the
macro's argument count check rejects. Both now use `{}` placeholders.
The README and the book's debugging page say that a placeholder without
a type character prints its argument by type, and that a type character
casts the argument instead.

Signed-off-by: nihalpasham <nihalp@nvidia.com>
The README still showed the C-style two-argument form that the macro's
argument-count check rejects; use placeholders and state the default
conversion rule in one sentence.

Signed-off-by: nihalpasham <nihalp@nvidia.com>
@nihalpasham nihalpasham added bug Something isn't working device-apis User-facing device-side and kernel-authoring APIs (cuda-device, cuda-macros) labels Oct 19, 2026
@nihalpasham

Copy link
Copy Markdown
Collaborator

/ok to test a61a7e9

@nihalpasham

Copy link
Copy Markdown
Collaborator

Thanks for tracking this down; the defaults were wrong and the trait route is the right idea. As submitted, every {} on a binding failed device codegen (the format helper returned a slice constant the importer rejects), so I took the fix the rest of the way:

  • The format helper returns &'static [u8; N], sized from the format skeleton, and the assembly moved into a const fn in cuda-device that is tested on the host.
  • GpuPrintfArg is now the one source of the default conversion; the token classifier and the as f64 precision arm are gone.
  • Arguments evaluate left to right; + or space on an unsigned argument and {:.*} are compile-time errors; char, fieldless enums and i128 are documented as not accepted.
  • The printf example carries the cases that were wrong, and the docs state the contract.

At a61a7e965, {} on u64::MAX, on floats and on data[tid] print correctly on the 5090 through both routes, memcheck is clean, dynamic_smem and the other three examples build again, and all CI checks passed.

@nihalpasham
nihalpasham merged commit 82b63f4 into NVIDIA:main Oct 19, 2026
98 of 99 checks passed
@YeonwooSung
YeonwooSung deleted the fix/gpu-printf-default-format branch October 20, 2026 03:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cuda-oxide SIMT programming model: rustc backend, cargo oxide, cuda-device, cuda-host, book device-apis User-facing device-side and kernel-authoring APIs (cuda-device, cuda-macros)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants