Task 4 of 6

Life on the Edge

The moment a gather reads a neighbor, the edges bite. Take the forward difference — out[i] = signal[i+1] − signal[i], "how much does the signal jump here?". Thread 63 asks for signal[64], which does not exist.

What comes back is whatever the backend decides — and the three this course runs on decide three different things. Run the starter, then switch Mode and run it again: the CPU backend gives you NaN, WebGL gives you a garbage texel from elsewhere in the texture, and WebGPU quietly clamps the index and gives you signal[63] — a perfectly plausible number that you never asked for. That last one is the dangerous one: nothing looks wrong, so nothing gets fixed.

Reading off the end isn't wrong, it is undefined: every platform is free to answer differently, and they do. So never rely on the read. Decide what the edge means, and write that down. The usual convention — the one every image filter uses — is replicate: the last cell repeats the last real difference, signal[63] − signal[62]. You get it by clamping the index you start from, so the pair you read is always a pair that exists.

signal[64] doesn't exist — clamp before you knock
Goal: compute the forward difference with a clamped base index, so the last cell repeats the last real difference — signal[63] − signal[62] — instead of depending on what this backend happens to do with a read past the end.

Requirements

Hint 1 — which read is out of range

Only thread 63 misbehaves: this.thread.x + 1 is 64, one past the end. Every other thread's pair is fine, so the fix has to leave 0 … 62 exactly as they are and hand 63 a pair that exists.

Careful which index you pin. Clamping the neighborMath.min(this.thread.x + 1, n - 1) — makes thread 63 read itself twice and return 0, which is the answer WebGPU was already inventing for you. Clamp the base instead.

Hint 2 — the clamped base
const i = Math.min(this.thread.x, this.constants.n - 2);
return signal[i + 1] - signal[i];

For thread 63, i is 62, so the answer is signal[63] - signal[62] — the last real jump, repeated. Cells 62 and 63 come back holding the same number, which is exactly what "replicate" means.

Same idea elsewhere

Edge conventions are shipped as sampler settings on real hardware — clamp-to-edge address mode in WebGPU and Metal, cudaAddressModeClamp on CUDA texture objects, with repeat and mirror sitting beside them as the alternatives. Reading a raw buffer instead of a texture? Then you pick the convention by hand, exactly like here — and you do pick one, because an unguarded read past the end is undefined everywhere: CUDA will happily hand you another allocation's memory, and WGSL leaves out-of-range buffer access loose enough that two implementations can disagree. The three answers you just got from three backends are that fact, one level up.

All tasks in Thinking in Parallel

  1. Map: One Thread, One Value
  2. Gather: Read Anywhere
  3. No Scatter Allowed
  4. Life on the Edge
  5. Smooth a Signal
  6. The Two-Pass Blur

This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.