Repository navigation
Description
The label files committed with NPP+/batchedLabelMarkersAndCompression are wrong: touching pixels of the same value get different labels, so one region comes back as several. All 10 committed label files are affected. Building the sample against NPP+ 0.10.0 gives wrong files too, and the count it prints for CT_skull changes between runs (434 to 438, for 407 regions).
The problem looks like it's in nppiLabelMarkersUF itself rather than in the sample. NPP 11.8.0 to 13.2.0 do the same thing, down to a 3 x 3 image, and NPP+ 0.10.0 (which reports itself as NPP 12.4.0) fails in exactly the places NPP 12.4.1 does.
- Write-up, every file, and a script that reruns it all: https://github.com/Acero-AI/npp-labelmarkers-bug
- Forum: https://forums.developer.nvidia.com/t/nppilabelmarkersuf-splits-connected-regions-npp-11-8-to-13-2-and-npp-0-10-3-x-3-repro-the-cuda-samples-outputs-are-affected-too/386425
- NVIDIA bug 6963973
npp_min.cu: one call on the 3 x 3 image, exits with status 2 when NPP is wrong (71 lines)
nvcc -O2 npp_min.cu -lnppif -lnppc -o npp_min && ./npp_min; echo "exit $?"
// npp_min: the smallest input on which nppiLabelMarkersUF gives a wrong answer (NPP 12.4.1, 13.x).
//
// nvcc -O2 npp_min.cu -lnppif -lnppc -o npp_min && ./npp_min
// It has no GPU code of its own, so it also builds with g++ against any NPP and CUDA runtime (see run_clean.sh):
// g++ -O2 -x c++ npp_min.cu -I<npp include> -I<cudart include> -L<libs> -l:libnppif.so.N -l:libnppc.so.N -l:libcudart.so.N
//
// The image is 3 x 3, 8 neighbours: 0 2 0
// 0 2 0
// 1 0 1
// The five 0s touch (the bottom-middle 0 touches both top 0s through corners), so there are four
// regions: the 0s, the 2s, and each 1. NPP is called the way NVIDIA's sample
// batchedLabelMarkersAndLabelCompressionNPP calls it: 8-bit input, the NULL stream, and a stream
// context filled from the device properties. Unlike the sample, the scratch buffer is zeroed; that
// makes no difference.
#include <cstdio>
#include <set>
#include <cuda_runtime.h>
#include <npp.h>
#define CK(x) do { cudaError_t e = (x); if (e != cudaSuccess) { printf("CUDA error %s, line %d\n", cudaGetErrorString(e), __LINE__); return 1; } } while (0)
int main() {
const int W = 3, H = 3;
const Npp8u img[W * H] = {0, 2, 0,
0, 2, 0,
1, 0, 1};
NppStreamContext ctx = {};
ctx.hStream = 0;
CK(cudaGetDevice(&ctx.nCudaDeviceId));
CK(cudaDeviceGetAttribute(&ctx.nCudaDevAttrComputeCapabilityMajor, cudaDevAttrComputeCapabilityMajor, ctx.nCudaDeviceId));
CK(cudaDeviceGetAttribute(&ctx.nCudaDevAttrComputeCapabilityMinor, cudaDevAttrComputeCapabilityMinor, ctx.nCudaDeviceId));
CK(cudaStreamGetFlags(ctx.hStream, &ctx.nStreamFlags));
cudaDeviceProp p;
CK(cudaGetDeviceProperties(&p, ctx.nCudaDeviceId));
ctx.nMultiProcessorCount = p.multiProcessorCount;
ctx.nMaxThreadsPerMultiProcessor = p.maxThreadsPerMultiProcessor;
ctx.nMaxThreadsPerBlock = p.maxThreadsPerBlock;
ctx.nSharedMemPerBlock = p.sharedMemPerBlock;
NppiSize roi = {W, H};
Npp8u *src, *scratch;
Npp32u* dst;
int bytes = 0;
if (nppiLabelMarkersUFGetBufferSize_32u_C1R(roi, &bytes) != NPP_SUCCESS) { printf("buffer size failed\n"); return 1; }
CK(cudaMalloc((void**)&src, W * H));
CK(cudaMalloc((void**)&dst, W * H * sizeof(Npp32u)));
CK(cudaMalloc((void**)&scratch, bytes));
CK(cudaMemset(scratch, 0, bytes));
CK(cudaMemcpy(src, img, W * H, cudaMemcpyHostToDevice));
NppStatus st = nppiLabelMarkersUF_8u32u_C1R_Ctx(src, W, dst, W * sizeof(Npp32u), roi, nppiNormInf, scratch, ctx);
CK(cudaDeviceSynchronize());
if (st != NPP_SUCCESS) { printf("NPP status %d\n", st); return 1; }
Npp32u lab[W * H];
CK(cudaMemcpy(lab, dst, sizeof lab, cudaMemcpyDeviceToHost));
const NppLibraryVersion* v = nppGetLibVersion();
int rt = 0, drv = 0;
CK(cudaRuntimeGetVersion(&rt));
CK(cudaDriverGetVersion(&drv));
printf("NPP %d.%d.%d, %s (sm_%d%d), CUDA runtime %d, driver supports %d\n\n", v->major, v->minor, v->build, p.name, p.major, p.minor, rt, drv);
for (int y = 0; y < H; y++) printf(" %d %d %d | %u %u %u\n", img[3 * y], img[3 * y + 1], img[3 * y + 2], lab[3 * y], lab[3 * y + 1], lab[3 * y + 2]);
std::set<Npp32u> distinct(lab, lab + W * H);
// Pixels 0, 3, 7, 2 and 5 are the five 0s and must share one label; pixels 1 and 4 are the 2s and must share
// another; pixels 6 and 8 are the 1s, each a region of its own. Four regions, four different labels.
const bool zeros = lab[0] == lab[3] && lab[3] == lab[7] && lab[7] == lab[2] && lab[2] == lab[5];
const bool twos = lab[1] == lab[4];
const std::set<Npp32u> four = {lab[0], lab[1], lab[6], lab[8]};
const bool ok = zeros && twos && four.size() == 4 && distinct.size() == 4;
printf("\nexpected 4 regions, NPP gave %zu: %s\n", distinct.size(), ok ? "correct" : "WRONG");
return ok ? 0 : 2;
}Two small suggestions for the sample, whatever the fix turns out to be: it prints the label count but never checks the labels, so it can't catch this, and the committed output files should be regenerated once the library is fixed.
A check that needs no GPU (8 neighbours; both numbers are 0 when the labels are right):
import numpy as np
from scipy import ndimage
def check(img, labels):
"""Count splits (a region given an extra label) and merges (a label given an extra region), 8 neighbours. Both are 0 when right."""
truth, n = np.zeros(img.shape, np.int64), 0
for v in np.unique(img):
t, k = ndimage.label(img == v, structure=np.ones((3, 3)))
truth[t > 0], n = t[t > 0] + n, n + k
pairs = np.unique(np.stack([truth.ravel(), labels.ravel()]), axis=1)
return len(pairs[0]) - len(np.unique(pairs[0])), len(pairs[1]) - len(np.unique(pairs[1]))For each image, img is the *_8u.raw input (uint8, H x W) and labels the matching *_32u.raw output (uint32).

Activity
FrozenWolf-Cyber commentedon Nov 1, 2026
Update: NVIDIA has fixed the underlying NPP bug (6963973), and the fix is scheduled for CUDA 13.6. The label files committed here were produced by the affected NPP and are still wrong. I am leaving this open until they are regenerated with a fixed NPP, and I will check them against 13.6 when it is released.