Seyed Masoud Hosseini · Overview · Study log · Weekly summaries · Ideas · Search · Transcript · RSS feed

Deep Learning Systems · Lecture 14 of 25 · 50:08

Lecture 13: Hardware Acceleration Implementation

Lecture 13 - Hardware Acceleration Implemention on YouTube

Study guide

What this lecture covers

This is a live-coding lecture that puts the previous two lectures on CPU and GPU acceleration into practice by walking through needle's new NDArray backend, the library built for the course's third homework to replace NumPy as the array backend. The instructor works in a Google Colab notebook with GPU support enabled, builds the CUDA extension with pybind11, and traces through the code path executed when creating an array and running simple arithmetic on it.

The lecture explains the NDArray class's fields (shape, strides, offset, device, handle), how the same one-dimensional memory buffer can represent reshaped, sliced, transposed, or broadcast views by changing only shape and strides, and why a compact operation is needed before certain arithmetic operations. It ends by adding a new elementwise/scalar division operator to the library as a worked example. After watching, you should be able to explain how NDArray maps multi-dimensional array operations onto a flat memory buffer, and why the same view-based logic underlies reshape, slicing, transpose, and broadcasting.

Key ideas

  • NDArray fields: an NDArray is defined by shape, strides, offset, device, and a handle pointing to a flat one-dimensional memory buffer allocated on the CPU or GPU.
  • Strided indexing maps multi-dimensional access to flat memory: an element [i,j] is located at offset + i*strides[0] + j*strides[1], generalizing across CPU and GPU backends.
  • Views share memory without copying: reshape, slicing, and transpose can all be implemented by constructing a new NDArray with different shape/strides/offset over the same underlying handle, rather than copying data.
  • Broadcasting via zero strides: broadcasting a new dimension is implemented by giving that dimension a stride of 0, so indexing along it always reads the same underlying value.
  • Compact vs. non-compact arrays: an array is "compact" when its strides follow normal row-major order with zero offset; non-compact arrays (after slicing, transposing, or broadcasting) generally cannot be operated on by directly iterating the flat buffer, so a compact operation copies them into normal layout first when needed.
  • Backend dispatch: array creation and arithmetic operations dispatch through a BackendDevice object to either a NumPy, CPU (pybind11-wrapped C++), or CUDA implementation, keeping needle's array API consistent across devices.
  • Adding an operator requires kernel plus host-side function: implementing elementwise or scalar division follows the same pattern as existing operators (add, multiply): define a CUDA kernel, a host-side launcher function, and register it, mirroring the existing code for other operators.
  • Compositional design: needle's tensor and autodiff layer is built on top of the NDArray array API, so the array backend can be developed and tested independently before being used to power full automatic differentiation.

Walkthrough

Setting up the environment and repository (0:00)

The instructor sets up a Colab notebook with a GPU runtime (an NVIDIA T4 in this session), clones the lecture's version of needle, and installs pybind11 to build the new CUDA extension. The new code base adds a src folder with CUDA and C++ source files and a backend_ndarray folder that implements the new array backend intended to replace NumPy.

Creating an NDArray and tracing array creation (4:05)

After importing backend_ndarray, the instructor creates an array on the CUDA device and traces the code path: NDArray.make calls into the device's array function to allocate a flat buffer of the requested size (through the CPU, CUDA, or NumPy backend module), then from_numpy copies data from a source NumPy array into that allocated buffer. On CPU this uses an aligned allocation and a memory copy; on GPU it uses cudaMalloc and cudaMemcpy from host to device.

Tracing arithmetic: y = x + 1 (16:19)

Adding a scalar to an NDArray dispatches through an overloaded operator to a scalar_add function, which allocates a new output array via make and then calls the backend's scalar-add kernel, passing the input handle, the scalar value, and the output array view. On GPU, this compiles down to a CUDA kernel launched with a computed grid and block size; on CPU, the equivalent function is a straightforward loop over the array's elements.

Shape, strides, and offset: reshape, slicing, transpose, broadcasting (25:44)

The instructor demonstrates that reshaping a 1D array into a 2D array, slicing out a submatrix, transposing an array, and broadcasting a new dimension can all be implemented by constructing a new NDArray with different shape, strides, and offset values over the same underlying handle — no data is copied. Transposition swaps the corresponding strides; slicing adjusts the offset and shape; broadcasting sets the new dimension's stride to 0 so every index along it reads the same value.

Why compaction is needed (33:52)

Because arithmetic kernels generally iterate directly over the flat underlying buffer, an array whose strides and offset don't correspond to plain row-major order (for example, after transposing or slicing) cannot be operated on directly without producing wrong results. The compact function checks whether an array's layout is already in standard order and, if not, allocates a new buffer and copies the data into standard layout. The lecture recommends always calling compact before arithmetic to keep the homework implementation simple, while noting that production libraries sometimes implement operations that work directly on non-compact, strided arrays for efficiency.

Live-coding a new operator: scalar and elementwise division (40:58)

As a worked example, the instructor adds elementwise and scalar division to the CUDA backend by copying the structure of the existing multiply kernel and its host-side launcher function, changing the operation and function names accordingly, then rebuilding the extension. After hitting an import-caching issue in the notebook (fixed by running a separate Python script rather than reusing the notebook's already-imported module) and one bug in the first attempt, the division kernel is shown working correctly.

Connecting the array backend to needle's tensor layer (47:07)

The lecture closes by showing that needle's tensor and operator implementation files now import backend_ndarray as the array API instead of NumPy, so the same array logic developed and tested independently in this lecture now powers needle's automatic differentiation and neural network layers, completing the transition away from NumPy as the backend.

Before you watch

  • Watch the two previous lectures on CPU acceleration and GPU acceleration in this course, since this lecture directly implements the tiling and CUDA kernel concepts they introduced.
  • Basic familiarity with C++ and CUDA syntax will help with following the kernel code shown on screen.
  • This lecture assumes access to the course's needle repository and a CUDA-enabled environment (such as Google Colab), which the instructor sets up at the start.

Check your understanding

  1. What fields make up an NDArray, and how does an element's flat memory location get computed from its shape, strides, and offset?
  2. Why can reshaping, slicing, transposing, and broadcasting all be implemented without copying any data?
  3. What makes an array "non-compact," and why does that matter for arithmetic kernels that iterate over the flat buffer?
  4. Walk through what happens, function by function, when you write y = x + 1 on a CUDA-backed NDArray.
  5. Why does needle's tensor and automatic differentiation layer import the array API from backend_ndarray instead of NumPy after this lecture's changes?

Vocabulary

live-coding (adjective)
Writing and running code in real time while explaining it.
This is a live-coding lecture on building a new backend.
backend (noun)
The underlying system that performs the real computation behind an interface.
NDArray is a new backend that replaces NumPy.
buffer (noun)
A block of memory used to store data temporarily.
The handle points to a flat memory buffer.
handle (noun)
A reference used to access a piece of allocated memory.
Every NDArray has a handle to its data.
offset (noun)
The distance from the start of memory to where an array's data begins.
Slicing changes an array's offset.
stride (noun)
The number of memory steps needed to move to the next element along a dimension.
Strides map multi-dimensional indices onto flat memory.
view (noun)
A way of looking at existing data with a new shape, without copying it.
Reshape and transpose can both be done as a view.
reshape (verb)
To change an array's dimensions without changing its underlying data.
We reshape a 1D array into a 2D array.
slicing (noun)
Selecting a smaller part of an array using index ranges.
Slicing adjusts the offset and shape of the array.
transpose (verb)
To flip an array's rows and columns.
Transposing an array swaps its strides.
broadcasting (noun)
Automatically expanding a smaller array to match a larger array's shape.
Broadcasting is done by setting a stride to zero.
compact (adjective)
Stored in standard, gapless order in memory.
An array must be compact before some operations run.
dispatch (verb)
To send a request to the correct piece of code that will handle it.
Operations dispatch to the CPU or CUDA backend.
kernel (noun)
A small piece of code that runs directly on the GPU.
We write a CUDA kernel for scalar addition.
host-side (adjective)
Running on the CPU rather than on the GPU.
The host-side function launches the GPU kernel.
launcher function (noun)
Code that starts a GPU kernel running with the right settings.
Each operator needs a launcher function.
register (verb)
To add a new function so it can be found and used by the system.
We register the new division operator.
compositional (adjective)
Built by combining separate, independent pieces.
Needle has a compositional design with separate layers.
extension (noun)
Additional compiled code added to a program to give it new features.
We build the CUDA extension using pybind11.
trace through (phrasal verb)
To follow a piece of code step by step to see what it does.
The lecture traces through the code path for array creation.

From the YouTube description

This lecture demonstrates the implementation of hardware acceleration both on CPU and GPUs. The lecture walks through how the linear algebra backends for the needle library work.

Note: Captions are delayed but will be finalized for this video this week.

← Lecture 12: GPU Acceleration · Lecture 14: Implementing Convolutions →