Is the job waiting for sums or memory?
A job is memory-bound when moving its data takes longer than doing its sums, and compute-bound when the sums take longer. Which one it is decides whether a faster chip helps at all.
A GPU can do trillions of sums a second but can only fetch so many bytes a second. Many everyday jobs spend their time waiting for the bytes.
By the end of this lesson
Note
I can tell whether a job is limited by compute or by memory.
Key words
| Word | What it means | Example |
|---|---|---|
| Bandwidth | how many bytes a second a memory can move | 200 GB a second |
| Compute rate | how many sums a second a chip can do | |
| Memory-bound | limited by the time to move the data | |
| Compute-bound | limited by the time to do the sums | |
| Transfer | moving data from one memory to another | |
| Coalesced | neighbouring threads reading neighbouring memory |
The cells students fill in are left blank in this preview.
A device has two limits
A chip can do so many sums a second, its compute rate. Its memory can move so many bytes a second, its bandwidth.
A job needs both. The time to move its bytes is bytes divided by bandwidth. The time to do its sums is sums divided by the rate. Whichever is longer is the bottleneck, and the job takes at least that long.
Worked example: a made-up device
| Job | Bytes moved | Sums | Memory time | Compute time | Limited by |
|---|---|---|---|---|---|
| Double 5 million numbers | 40 million | 5 million | 200 microseconds | 1.25 microseconds | Memory |
| Multiply two 1000 by 1000 matrices | 12 million | 2000 million | 60 microseconds | 500 microseconds | Compute |
The cells students fill in are left blank in this preview.
Run the memory against compute sum
# A made-up device: 200 gigabytes a second, 4 teraflops (4 million million sums a second).
bandwidth = 200 * 10**9
rate = 4 * 10**12
# Scaling 5 million numbers by 2: read 1 vector, write 1 vector of 4-byte numbers.
n = 5000000
bytes_moved = 2 * n * 4
sums = n
memory_us = bytes_moved / bandwidth * 10**6
compute_us = sums / rate * 10**6
print("microseconds to move the bytes:", round(memory_us, 2))
print("microseconds to do the sums:", round(compute_us, 2))
if memory_us > compute_us:
print("limited by: memory")
else:
print("limited by: compute")
# Try sums = n * 2000, then run again.
More sums per byte means compute-bound
The number of sums for each byte moved is a job's arithmetic intensity. Doubling a number does one sum for 8 bytes of traffic. Multiplying two 1000 by 1000 matrices does about 167 sums for each byte, because each number is used again and again.
Low intensity means memory-bound. High intensity means compute-bound.
The lab compares two jobs
The lab's transfer.py works out both times for two jobs on another made-up device. It prints the bytes moved, the sums done, the two times in microseconds and a last line saying what limits the job.
Read the two last lines. One job is limited by memory and the other by compute, though both run on the same device.
Compare two jobs
docker exec jupyter python3 steps/transfer.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Neighbouring threads should read neighbouring memory
Memory is fetched in lines, here of 32 bytes. If 8 threads read 8 neighbouring 4-byte numbers, one line serves them all.
If each thread reads every 8th number, each needs a line of its own and 8 lines are fetched. This is coalescing. The lab's coalesce.py prints the addresses touched and the lines fetched for both patterns.
Compare two reading patterns
docker exec jupyter python3 steps/coalesce.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Moving data between memories costs time too
A GPU has memory of its own, so data from the main memory must be copied across first, and the answer copied back. That transfer goes over a slower link than the GPU's own memory.
A small job can spend longer in transfer than in sums. The usual cure is to move the data once and do a lot of work on it before moving it back.
A common mistake
Warning
Many people think a faster processor always speeds a job up. If the job is memory-bound, the processor spends its time waiting. First find the bottleneck, then spend effort on that. For a memory-bound job, move fewer bytes or read them in a tidier pattern.