C.W.K.
Stream
Lesson 04 of 05 · published

Operator Variations & Capacity Zones

~10 min · vectors, operators, vram, capacity

Level 0Beginner
0 XP0/38 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

One kernel, four flavors — and a reality check on size

Once vector add works, swapping the operator gets you sub, mul, and div for free. The launch geometry, memory layout, and verification stay identical — only the ALU op changes. But the cost isn't always identical:

OpBody changeNotes
AddC[i] = A[i] + B[i]Baseline.
Sub+-Same throughput as add — single FP unit.
Mul+*Uses FMA pipelines, same throughput.
Div+/5–10× slower due to non-pipelined reciprocal. Guard against B[i] == 0 to avoid NaN/inf.

VRAM Capacity Zones — what happens when you ask for too much

ZoneWhat happensEscape hatch
🟢 Green: fits in VRAMFull-speed, data resident on the GPUNo action needed
🟡 Yellow: UVM paging10–50× slowdown as Unified Memory migrates pagesProcess in chunks
🔴 Red: hard alloc failcudaMalloc returns cudaErrorMemoryAllocationSlice-and-stream loop
🟣 Crimson: grid limitgridDim.x exceeds 2³¹⁻¹Outer host loop, base-index arg

Apple Silicon shifts the zones: there's no separate VRAM, so 'Green' extends to the entire unified pool minus what the OS and other apps are holding. The Yellow zone (paging to swap) still exists but kicks in much later.

Code

Generic 1-kernel-per-op pattern (CUDA)·cuda
// Use a template + functor or the preprocessor.
// Functor version is debugger-friendly:

struct AddOp { __device__ float operator()(float a, float b) const { return a + b; } };
struct SubOp { __device__ float operator()(float a, float b) const { return a - b; } };
struct MulOp { __device__ float operator()(float a, float b) const { return a * b; } };
struct DivOp { __device__ float operator()(float a, float b) const {
    return b == 0.f ? 0.f : a / b;   // guarded
} };

template <typename Op>
__global__ void elementwise(const float* A, const float* B, float* C, size_t N, Op op) {
    size_t i = (size_t)blockIdx.x * blockDim.x + threadIdx.x;
    if (i < N) C[i] = op(A[i], B[i]);
}

// Launch with whichever Op you want; nvcc inlines the call:
// elementwise<<<blocks, threads>>>(dA, dB, dC, N, MulOp{});
Slice-and-stream when data exceeds VRAM·cuda
// Process N elements in chunks of CHUNK to fit in VRAM.
const size_t CHUNK = 1 << 24;        // 16M elements ≈ 64 MB per buffer
for (size_t off = 0; off < N; off += CHUNK) {
    size_t this_n = std::min(CHUNK, N - off);

    cudaMemcpyAsync(dA, hA + off, this_n*sizeof(float),
                    cudaMemcpyHostToDevice, stream);
    cudaMemcpyAsync(dB, hB + off, this_n*sizeof(float),
                    cudaMemcpyHostToDevice, stream);

    int threads = 256;
    int blocks  = (this_n + threads - 1) / threads;
    vec_add<<<blocks, threads, 0, stream>>>(dA, dB, dC, this_n);

    cudaMemcpyAsync(hC + off, dC, this_n*sizeof(float),
                    cudaMemcpyDeviceToHost, stream);
}
cudaStreamSynchronize(stream);

External links

Exercise

Time the four operators (add, sub, mul, div) on the same 64M-element vectors. You should see add ≈ sub ≈ mul, and div noticeably slower (often 3–7× depending on GPU generation). Then pick a vector size 2× your VRAM and watch the kernel either crash with an alloc error (CUDA) or slow to a crawl (Apple Silicon paging). Implement the slice-and-stream version and confirm it runs at the same per-element rate as the in-VRAM case.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.