Seyed Masoud Hosseini · Overview · Study log · Ideas · Transcript · RSS feed
Parallel Computing & CUDA · Lecture 15 of 19 · 1:18:53
Lecture 15: Domain-Specific Programming Languages
Study guide
What this lecture covers
Having spent the course on low-level parallel programming in C/C++, ISPC, and CUDA, the lecture turns to higher-level domain-specific languages (DSLs) that let non-experts get good performance without hand-writing loops, vectorization, and scheduling. It sits between the earlier "how do you build this from scratch" lectures and the course's closing material on specialized hardware, and the instructor frames exam questions on this topic as conceptual rather than implementation-heavy.
The lecture works through two case studies in detail: Halide, a language for image processing pipelines used in production at Google and Meta, and Liszt, a Stanford research language for mesh-based scientific simulation. After watching, you should be able to explain why DSLs trade generality for productivity and performance, and describe how a compiler can safely reorder and parallelize code when it restricts what programs can express.
Key ideas
- Productivity vs. performance vs. generality: general-purpose languages like C++ or Cuda let you write anything but require you to make every performance decision yourself; DSLs give up generality to let the compiler make those decisions for you.
- Halide: a functional DSL for image processing that separates the algorithm (what to compute, as a dependency graph of functions) from the schedule (how to compute it: loop order, tiling, vectorization, parallelization).
- Arithmetic intensity and recompute tradeoffs: splitting a 2D blur into two 1D passes cuts total math but adds a temporary buffer; shrinking that buffer to fit in cache forces some recomputation, and chunking the recomputation across larger tiles balances footprint against wasted work.
- Schedule as loop transformation: Halide schedule directives (like
tile,vectorize,parallel,compute_at) manipulate an implicit loop nest without changing the algorithm's output, since the algorithm is purely functional. - Auto-scheduling: because Halide schedules form a structured search space (loop order plus where to fuse computation), search algorithms can automatically find schedules competitive with expert-written ones.
- Liszt: a DSL for mesh-based simulation (used for problems like jet-engine fluid dynamics) where the only way to touch data is through accessors for mesh elements (edges, vertices, faces), so the compiler infers parallelism, locality, and needed synchronization without seeing raw pointers.
- Backend-specific compilation from one program: the same Liszt or Halide program compiles differently for a distributed cluster (message passing between nodes, ghost cells) versus a GPU (per-thread work, graph coloring instead of atomics to avoid write conflicts).
- Good DSL design: a small number of composable primitives, matched to how domain experts (physicists, image processing engineers) already think about their problem, tends to outlast and out-generalize systems that try to support every possible request.
Walkthrough
Why domain-specific languages (0:05)
The instructor motivates DSLs by noting how few programmers can write genuinely high-performance parallel code, even among students who have completed the course's assignments. He lays out three axes languages are traditionally judged on: productivity, performance, and generality, and observes that widely used general-purpose languages tend to sit at different points along performance vs. productivity while sacrificing one or the other. DSLs like SQL, PyTorch, and MATLAB deliberately give up generality in exchange for both high productivity and, because the compiler understands the semantics of a narrow set of operations, high performance.
Halide and the cost of a blur (11:17)
The instructor introduces Halide, the language behind Google's Android camera pipeline and, at one point, many Instagram filters. Using a simple 3x3 box blur as the running example, he shows that a naive 2D convolution costs nine multiply-adds per pixel, but separating the blur into a horizontal pass followed by a vertical pass drops that to six operations per pixel (and much more for larger filters), at the cost of allocating a full-size temporary buffer and roughly doubling the number of memory reads and writes, which lowers arithmetic intensity.
Balancing recomputation against footprint (20:29)
Working through student suggestions, the lecture shows how to shrink the temporary buffer to just a few rows so it fits in cache, which restores memory locality but forces recomputing overlapping rows for every output row, pushing the cost up to 12 operations per pixel. Generalizing this to process the image in chunks of several rows at a time (rather than one row at a time) trades a small, tunable amount of recomputation for much better arithmetic intensity, converging toward the two-pass cost as chunk size grows, as long as a chunk still fits in cache. The same idea, extended to 2D tiles, is what a real Halide-generated implementation does, with SIMD vectorization and thread parallelism applied on top.
Expressing algorithm and schedule separately (34:46)
The lecture shows actual Halide code: a purely functional description where each named function (such as blurX or blurY) is defined in terms of other functions, forming a dependency graph with no explicit loops. A separate schedule, using directives like tile, vectorize, parallel, and compute_at, tells the compiler how to transform an implicit loop nest to realize that algorithm efficiently, without altering its result. Because Halide guarantees changing the schedule never changes the output, programmers can rapidly try many optimization strategies that would otherwise take hours to hand-code and debug.
Auto-scheduling and where DSLs are heading (56:03)
The instructor describes research, including work led by Andrew Adams, that treats the schedule search space as structured enough for automated search algorithms to find schedules competitive with expert Halide programmers, illustrated with a comparison of an automatic scheduler against two of Google's best human schedule-writers. He connects this to the broader trend of DSL compilers targeting hardware more directly, including work that compiles image-processing-like languages straight to FPGA circuits instead of CPU instructions.
Liszt: a DSL for mesh-based simulation (1:05:06)
The second case study is Liszt, a Stanford research language for simulating physical systems on meshes, such as jet-engine fluid dynamics. A Liszt program can only access mesh data through accessors like "the vertices around this edge," never through arbitrary indexing, which lets the compiler determine all data dependencies at compile time. The instructor shows how the same program compiles differently depending on target hardware: for a cluster with no shared memory, Liszt partitions the mesh across nodes and manages ghost-cell exchange between iterations; for a GPU, instead of using atomics to handle multiple edges writing to a shared vertex, it precomputes a conflict graph and applies graph coloring so that same-colored edges can run fully in parallel without locks.
What makes a DSL last (1:15:14)
The lecture closes with design principles: a DSL works well when its primitives match how domain experts already think about their problem (pixels for image processing, meshes for physics, tensors for machine learning), and durable systems tend to have a small number of primitives that compose cleanly rather than growing feature by feature to satisfy every request. The instructor notes that the best sign a DSL succeeded is when people use it for problems its designers never anticipated.
Before you watch
- Review the earlier lectures on ISPC, CUDA, and cache/memory locality concepts such as blocking and arithmetic intensity, since the Halide case study builds directly on them.
- Recall the producer-consumer loop fusion idea from the data-parallel thinking and matrix multiplication lectures, since it reappears here as Halide's chunking strategy.
Check your understanding
- Why does separating a 2D blur into two 1D passes reduce arithmetic work but also reduce arithmetic intensity, and how does chunking address that tradeoff?
- In Halide, what is the difference between the algorithm and the schedule, and why can changing the schedule never change the program's output?
- Why can a Liszt compiler infer parallelism and required synchronization automatically, when equivalent C++ code operating on arbitrary pointers cannot?
- How does Liszt avoid using atomic operations when multiple mesh edges write to the same vertex on a GPU?
- What tradeoff does a DSL designer accept when restricting a language to a small set of composable primitives instead of adding requested features freely?
From the YouTube description
Performance/productivity motivations for DSLs, case studies on several DSLs
To follow along with the course, visit the course website:
https://gfxcourses.stanford.edu/cs149/fall23/
Kayvon Fatahalian
Associate Professor of Computer Science, Stanford University
https://graphics.stanford.edu/~kayvonf/
Kunle Olukotun
Cadence Design Systems Professor, Professor of Electrical Engineering and of Computer Science, Stanford University
https://engineering.stanford.edu/people/oyekunle-olukotun
Learn more about the online course and how to enroll: https://online.stanford.edu/courses/cs149-parallel-computing
To view all online courses and programs offered by Stanford, visit: https://online.stanford.edu/
← Lecture 14: Midterm Review · Lecture 16: Transactional Memory 1 →
