Memory-safety checking on devices
Choose the relevant Compute Sanitizer instrument and test boundary cases without confusing clean dynamic runs with a proof.
On this page
The question for today
What you’ll understand
- Match memory, race, initialization, and synchronization tools to their supported scope.
- Derive launch-tail guards and byte bounds for tensor kernels.
- Design reproducible device checks and state the limits of dynamic coverage.
A tensor kernel may produce plausible outputs while reading outside a logical view, using uninitialized values, or racing over shared storage. Device instrumentation helps identify particular classes of executed errors, but its scope and supported hardware change with releases. This lesson uses the Compute Sanitizer 2026.3 manual and release notes with an Ampere SM80 target. That tool version is stated separately from the CUDA 12.8.1 mechanism baseline used elsewhere in this module.
Choose a checker that covers the suspected error
Memcheck can report supported invalid or misaligned device accesses, CUDA API errors, and leaks when the relevant checking options are enabled. An access within a large underlying allocation can still violate a smaller tensor view’s logical bounds without crossing an allocation boundary visible to the checker. Keep shape and stride assertions in the application as well as tool runs. Precise reports depend on supported instructions, compilation information, and tool compatibility; preserve the actual command, binary, driver, toolkit, and GPU identity for reproduction.
Racecheck targets supported on-chip shared-memory hazards; it is not a universal detector of global-memory races between devices or arbitrary host threads. Initcheck in the stated release supports the documented address-space selection: global checking is the default, while shared-memory checking is selected with --initcheck-address-space shared, or both spaces with all. That shared capability was introduced in a later release and must not be projected onto every older tool. Synccheck identifies supported synchronization misuse, but a clean report is not a proof of all progress, barrier, or memory-order properties.
Boundary arithmetic is part of the safety argument
For a contiguous tensor, derive both the element index and the accessed byte interval. A valid first byte is insufficient if a vectorized access extends beyond the end. For views, include offset, stride, element width, and allocation extent rather than reusing a contiguous-array proof. Test empty and small shapes through the appropriate launch policy, nonmultiples of block size, and dimensions that change vectorization or shared-memory participation. Use index types that cannot overflow at supported problem sizes before an otherwise correct bounds comparison.
Run memory-access checks before interpreting other tool findings, because an earlier corruption can produce misleading later symptoms. Then select the checks relevant to the program, including shared-memory initialization and synchronization where supported. Instrumentation changes timing and can add substantial overhead, so do not use sanitized runs as normal throughput measurements. It also cannot inspect every host memory access outside its CUDA-related coverage. Release notes identify capability changes and known limitations; compatibility is part of the experiment rather than an assumed constant.
A clean dynamic run covers its executed cases
Construct adversarial but valid inputs that exercise boundary tiles, different strides, repeated buffer reuse, and concurrent streams. Check numeric outputs against a trusted implementation, because an in-bounds access to the wrong tensor element may satisfy the memory checker while violating the algorithm. A passing run reports no covered violation in that execution; it does not prove every possible interleaving or untested shape safe. Treat nondeterministic failures as evidence to investigate rather than accepting a later passing run as a correction.
The probability lab models only a deliberately assumed input-trigger probability. Reset, set probability to 0.1 and trials to 10, predict the chance of at least one trigger, then Step. Its exact binomial distribution is not a stream of sanitizer executions and says nothing about detector sensitivity. Independent identical trigger probabilities are a hypothesis, often unrealistic for correlated test shapes. Use the model to question coverage claims, not to infer a real kernel’s bug probability or certify correctness.
Work it through
A launch tail beyond a float array
A kernel processes 1,000 contiguous FP32 elements with 256 threads per block. Assume four-byte elements and sufficient integer range. Calculate the launch and explain the final thread’s invalid access without an index guard.
The grid needs ceil(1,000/256) = 4 blocks, launching 1,024 threads. There are 24 extra indices, from 1,000 through 1,023.
The allocation contains 4,000 bytes. The last valid element, index 999, starts at offset 3,996 and occupies bytes 3,996 through 3,999.
An unconditional access by index 1,023 touches offsets 4,092 through 4,095, outside the allocation. Guard every relevant access with i < 1,000; also preserve valid synchronization participation for threads that perform no data access.
A tail guard prevents this scalar out-of-bounds pattern, while other view, vector-width, initialization, and synchronization contracts still need checking.
The living notebook
Explore the model
Interactive lab · probability
An assumed input-trigger model
Reset, set trigger probability p = 0.1 and trials n = 10, predict the probability of at least one trigger, then Step: approximately 0.6513. Playback highlights exact binomial outcomes; it does not execute a kernel, generate independent tests, or model sanitizer sensitivity.
- Mean · np
- 5
- Variance · np(1−p)
- 2.5
- Total probability
- 1
These are exact binomial probabilities for independent trials with one fixed success probability. Playback tours the distribution; it does not resample outcomes. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forFinite clean runs do not prove absence of a bug, and a binomial coverage calculation depends on a declared input model.
Pause & reason
Check your understanding
Your turn
Try it yourself
For 513 FP32 elements and 128 threads per block, calculate grid size, launched threads, extra indices, and the last valid element’s byte interval. Explain one limitation of a clean sanitizer run.
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Round the block count upward, then use zero-based element 512 for the last valid access.
Compare with the solution
Five blocks launch 640 threads, leaving 640 − 513 = 127 extra indices. The allocation is 513 × 4 = 2,052 bytes, and element 512 occupies offsets 2,048 through 2,051. Guard scalar accesses against i ≥ 513 while keeping required synchronization valid. A clean run still does not test every shape or interleaving, and an in-allocation access to the wrong logical tensor element may evade allocation-bound checks.
Keep the idea
Take it with you
- Choose tools according to their version, architecture support, and actual coverage.
- Prove complete byte intervals and tail-thread behavior rather than checking only a starting address.
- Dynamic checking complements numeric tests and reasoning without replacing them.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.