5.3 KiB
Phase 1 Training Objective: Writing Kernels
Goal
This phase has one primary task:
Write kernel files for the existing operators.
All other code (descriptor lifecycle, Python bindings, tests, build system) is already implemented. Your only job is to fill in the computation logic.
Default requirements:
- Priority: get the kernel correct and running.
- You do NOT need to pursue peak performance initially.
- Complete the TODOs in
kernel.cuh/kernel.pyfirst, then consider aggressive optimizations.
If you've completed the basic goals and want to push performance further, you can modify the wrapper layer (e.g., .cu files for the NVIDIA backend), adjust launch policy, switch between kernels, or add more aggressive specializations. These are advanced topics and not required for Phase 1.
Files to Write
Each operator has two backends, corresponding to two kernel files:
| Operator | NVIDIA kernel | TileLang kernel |
|---|---|---|
copy |
ops/copy/nvidia/kernel.cuh |
ops/copy/tilelang/kernel.py |
vector_add |
ops/vector_add/nvidia/kernel.cuh |
ops/vector_add/tilelang/kernel.py |
reduce_sum |
ops/reduce_sum/nvidia/kernel.cuh |
ops/reduce_sum/tilelang/kernel.py |
softmax |
ops/softmax/nvidia/kernel.cuh |
ops/softmax/tilelang/kernel.py |
Recommended order (easy to hard): copy → vector_add → reduce_sum → softmax.
NVIDIA Kernel (.cuh files)
What to Write
A __global__ function placed under the operator's namespace. The function handles only computation logic, taking raw pointers as input/output. It does NOT involve any descriptor or public API.
By default, you do NOT need to modify the .cu files in the same directory this phase. The repository already provides compatibility-focused launch boilerplate. This phase only requires you to make kernel.cuh correct and runnable.
If you've completed the basic goal and want to push performance, treat the .cu file as an advanced optimization layer: adjust thread counts there, switch kernel variants, or add specialized paths. These are outside the scope of Phase 1.
Difficulty Progression
copy / vector_add: Element-wise operations using the grid-stride loop pattern. Each thread handles several independent elements with no inter-thread communication.
reduce_sum: Row-wise reduction. One block per row. Threads within the block first independently accumulate their assigned columns, then perform a tree reduction via shared memory. __syncthreads() is required for synchronization.
softmax: The basic version uses a three-pass flow: first pass finds the row max (numerical stability), second pass computes exp(x - max) sum and writes intermediate results to output, third pass divides by the sum. Each pass requires intra-block synchronization.
Concepts to Understand
- grid-stride loop: Why this pattern handles arbitrarily-sized tensors
- shared memory reduction: What each step of tree reduction does, and why
__syncthreads()is necessary - softmax minus max: Why computing
exp(x)directly causes problems, and why subtracting the row max does not change the result
TileLang Kernel (.py files)
What to Write
A Python function decorated with @tilelang.jit that describes tile-level computation using the TileLang DSL. TileLang compiles it into real CUDA kernels.
As with NVIDIA, you only need to complete the TODOs in kernel.py this phase — no need to modify outer adapter code.
Difficulty Progression
copy: Use the built-in T.copy to move data between two tiles. No manual loop needed.
vector_add: Use a T.Parallel loop to compute element-wise within a tile. Understand the difference between T.Parallel and T.Serial.
reduce_sum: Block-wise accumulation required. The outer loop uses T.Serial to traverse column-direction blocks sequentially (because state accumulates), and the inner loop uses T.reduce_sum to reduce over a fragment.
softmax: Two-pass scan using the online softmax algorithm. The first pass maintains a rolling log-sum-exp state; the second pass normalizes with the final LSE. Uses exp2 / log2 instead of exp / log.
Concepts to Understand
T.ParallelvsT.Serial: When can loop iterations run in parallel, and when must they be sequentialT.alloc_fragment: A tile-level local buffer, corresponding to registers or shared memoryT.copy: Moves a chunk of global memory into a fragment — not element-by-element assignment- online softmax: Why a single pass can produce the correct normalization, and the rolling update logic for log-sum-exp
exp2/log2: Why TileLang uses these instead ofexp/log
Verification
After writing each kernel, verify with the corresponding test:
# NVIDIA kernel (requires rebuild)
bash scripts/build_nvidia.sh build
PYTHONPATH=python:. CAMP_BUILD_DIR=build-nvidia pytest tests/op_tests/test_<op>.py -v --backend nvidia
# TileLang kernel (no build needed)
PYTHONPATH=python:. CAMP_BUILD_DIR=build-nvidia pytest tests/op_tests/test_<op>.py -v --backend tilelang
# Both correctness + benchmark
PYTHONPATH=python:. CAMP_BUILD_DIR=build-nvidia python tests/run_ops.py --op <op> --backend nvidia --mode all
All four operators passing both backends = Phase 1 complete.