Concepts / Timing Code with %timeit in Notebooks

Timing Code with %timeit in Notebooks

The %timeit magic is a notebook command.

  • Programming

Why Saying a GPU Is Faster Is Not Enough

A GPU is often described as the fastest way to run the heavy arithmetic inside neural networks, and matrix multiplication is one of the most common of those operations. Knowing that a GPU is faster in general is not the same as knowing how much faster it is for one specific operation, on one specific machine, with specific matrix sizes. This article shows how to answer that question directly. You will move two tensors onto a GPU, time the same multiplication on both devices, and reason about what the two timings tell you.

The experiment has three parts, and their order matters. First, two random matrices are created on the CPU. Second, both are moved to the GPU. Third, the same matrix multiplication is timed on each device. The timing result only makes sense once you know which device held the data at the moment the multiplication ran, so keep that question in mind as you read: when a GPU result differs from what you expected, where in this sequence of steps would you look first?

Moving a Tensor onto the GPU

Moving a tensor to a CUDA device is a single method call: a.to("cuda"). The call takes a tensor that currently lives on the CPU and places its data on the CUDA device, which is the GPU that PyTorch uses for this work. The call does not change the values in the tensor. It changes where those values are stored and therefore which processor can work on them directly.

python
Notes on the code
  • a.to("cuda") returns a tensor whose data is on the CUDA device
  • The source performs the same move for the second matrix with b.to("cuda") on the same line
  • Matrix sizes here are an illustrative choice, not values taken from the source
avalues in CPU memoryavalues on CUDA devicea.to(cuda)
Which device holds the values of a before the move, and which device holds them after the call?

Reading the Output of %timeit

The %timeit magic is a notebook command. You place it before a statement, and it runs that statement several times and reports the typical time for one run. The result is written as a mean plus or minus a spread, followed by per loop. The mean is the central estimate of how long one run takes. The value after the plus-minus sign shows how much the individual runs varied around that estimate. A small spread means the timing was stable from run to run, and a large spread means you should be cautious about trusting the mean.

python
Notes on the code
  • %timeit runs the statement a @ b several times
  • The reported line has the form mean plus or minus spread, per loop

Two timing results are only comparable when they measure the same statement on the same inputs. In this experiment the statement is a @ b in both cases, and the inputs are the same two matrices. The only intended difference between the two measurements is the device on which the tensors reside when the statement runs. If you changed the matrix sizes, or used different data between runs, the two numbers would no longer be a fair comparison.

Timing the Multiplication on Both Devices

Now put the pieces together. You create the matrices on the CPU and time the multiplication there first. Then you move both matrices to the GPU and time the same statement again. The transfer happens before the second %timeit line, so the timed statement is only the multiplication itself. The flowchart below shows the order of the steps on the GPU path.

python
Notes on the code
  • The statement a @ b is identical in both timings
  • Only the device holding a and b differs between the two timings
  • Reassigning a and b keeps the same variable names for the GPU run
Create a and bon CPUMove a and ba.to(cuda), b.to(cuda)Time a @ b%timeit measures thisstatementRead mean and spreadper loop
Which steps happen before the timer starts, and which step is the one that %timeit measures?

On the CPU path the move step is skipped, because both matrices already live on the CPU. The timed statement is the same in both cases, so the only thing that changed between the two measurements is which processor runs the multiplication and where its operands sit. The comparison below sets the two paths side by side.

a and btensors on CPUa and btensors on CUDA devicea @ bruns on CPUa @ bruns on GPUmultiplymultiply
How do the two timed paths differ in where the operands sit and which processor runs the multiplication?

Calculating the Speedup Ratio

A speedup is usually reported as a ratio of the two times. In this article the ratio is the CPU time divided by the GPU time, so a value above one means the GPU run finished in less time. The relationship is written as follows, where t_{CPU} is the mean time on the CPU and t_{GPU} is the mean time on the GPU.

Rule

Worked example

Speedup from two mean timings

Suppose the CPU timing reports a mean of 40 ms per loop and the GPU timing reports a mean of 10 ms per loop for the same statement on the same inputs. What is the speedup?

  1. 1
    Identify the two times

    The CPU mean is 40 ms and the GPU mean is 10 ms. Both are means for the statement a @ b on the same matrices.

  2. 2
    Divide CPU time by GPU time

    Using the formula, the speedup is $\frac{40}{10}$, which is the CPU time divided by the GPU time.

  3. 3
    Read the result

    The quotient is 4, so the GPU run is about four times faster than the CPU run for this experiment.

Result

The speedup is 4 on this hardware, for these matrix sizes. The numbers 40 ms and 10 ms are illustrative and not taken from any real machine.

The source summarizes its own experiment by saying the computation was approximately four times faster on a V100. That is a rounded statement about one experiment. The way to check it is to calculate the ratio from the timing values yourself, using the means reported on your own machine.

The ratio depends on the hardware because it is a measurement of one processor pair on one operation. The same code can give a different ratio on a different GPU or a different CPU, because each machine has its own processors and its own timing behaviour for this statement. The ratio also does not say that every operation will speed up by the same factor, and it does not predict what another GPU will do. A speedup number is a property of a specific comparison, not a fixed property of GPUs.

Mistakes That Make GPU Timings Misleading

  • Common mistake

    Moving only one operand to the GPU before timing

    What it looks like
    a = a.to("cuda") is called, but b stays on the CPU, and then %timeit a @ b is reported as a GPU result.
    Why it fails

    Both operands have to be on the same device for the multiplication to run on the GPU, so the measurement does not describe the GPU path.

    Do this instead

    Move both a and b to the CUDA device before timing the GPU statement.

  • Common mistake

    Comparing timings made on different inputs

    What it looks like

    The CPU timing uses one pair of matrices and the GPU timing uses a larger pair.

    Why it fails

    Two timing results are only comparable when they measure the same statement on the same inputs, so the difference mixes device with input size.

    Do this instead

    Keep the statement and the matrices identical, and change only the device the tensors reside on.

  • Common mistake

    Inverting the speedup ratio

    What it looks like

    Dividing the GPU time by the CPU time and calling the result the speedup.

    Why it fails

    The ratio is defined as CPU time divided by GPU time, so the inverted value would be below one when the GPU is faster.

    Do this instead

    Divide the CPU time by the GPU time, and read a value above one as the GPU finishing sooner.

  • Common mistake

    Reading the mean without looking at the spread

    Why it fails

    The mean is only a central estimate. A large plus-minus spread means the runs varied a lot, so the single number is less reliable.

    Do this instead

    Read the spread alongside the mean before drawing a conclusion.

  • Common mistake

    Treating one speedup as a fact about all GPUs

    What it looks like

    Concluding that every GPU will be four times faster than every CPU after one experiment.

    Why it fails

    The ratio describes one operation with one pair of matrix sizes on one machine, and it does not predict other hardware.

    Do this instead

    Report the speedup together with the hardware and matrix sizes it was measured on.

Checking Your Reading of a GPU Timing

MEDIUM

A colleague moved a to the GPU with a.to("cuda") but left b on the CPU. They then ran %timeit a @ b, got a much smaller mean than on the CPU, and reported a GPU speedup. Explain what is wrong with that measurement, and describe the change you would make before timing again.

Hints
  • Look at which device each operand lives on at the moment the multiplication runs.
  • The source states that both operands have to be on the same device for the multiplication to run on the GPU.
  • Once both operands are moved, what has to stay the same between the two timings?
EASY

Using the speedup formula, a CPU mean of 25 ms and a GPU mean of 5 ms for the same statement on the same inputs give what ratio? Then state one reason the same code might give a different ratio on another machine.

Hints
  • Divide the CPU time by the GPU time.
  • Think about what the ratio describes: one operation, one pair of matrix sizes, one machine.

Key Takeaways

  • Calling a.to("cuda") places a tensor's data on the CUDA device, so the multiplication can run on the GPU only when both operands have been moved there.
  • %timeit runs a statement several times and reports a mean plus or minus a spread per loop, and you should read the spread as well as the mean.
  • Two timings are comparable only when they measure the same statement on the same inputs, with the device as the only intended difference.
  • Speedup is the CPU time divided by the GPU time, and it is a hardware-specific measurement that describes one operation on one machine with specific matrix sizes.

Related concepts

More concepts starting with T