Overview
- Purpose: Explains how the HRM model implements a two-tier memory hierarchy (L-level ≈ SRAM, H-level ≈ DRAM) in both PyTorch and Triton, and how the two implementations map to each other.
Files to reference
PyTorch ref: softmax.pyTriton ref: softmax_triton.pyMemory tier manager: test-env/HRM_optimised/models/memory_tier.pyTriton kernels: test-env/HRM_optimised/models/triton_kernels.pySparse embedding + optimizer: test-env/HRM_optimised/models/sparse_embedding.py
Conceptual summary
- Two-tier idea: L-level (fast, frequently-updated state) is treated as SRAM: kept resident and reused heavily inside a compute tile. H-level (large, infrequently-updated state) is treated as DRAM: loaded from global memory when needed.
- Why two implementations: PyTorch expresses algorithmic intent using tensors, streams and explicit copies; Triton encodes the micro-kernel behavior (register/shared-memory reuse, explicit loads/stores) so we can enforce the intended locality and measure latency/bandwidth differences.
PyTorch implementation (what to look for)
MemoryTierManager(memory_tier.py): central piece for the PyTorch-side memory-tiering.- Allocation:
alloc_sramandalloc_dramallocate tensors and track sizes. SRAM allocation is guarded by a capacity limit (spill-to-DRAM on overflow). - Contexts / Streams:
sram_contextanddram_contextprovide CUDA stream scopes to suggest where operations should run (helps overlap/ordering and can hint locality). - Transfers:
transfer_sram_to_dramandtransfer_dram_to_sramperform explicitclone()copies and record timing via CPU timers withtorch.cuda.synchronize()for correctness. - Tracking & metrics:
MemoryEventlog andTierStatscollect hit/miss counts, transfer counts, and timing statistics used for benchmarking.
- Allocation:
- Pure-PyTorch ops: Higher-level routines (e.g. a softmax demo in
softmax.py) use standardtorch/torch.nn.functionalops. These rely on the allocator/streams above to approximate tier behavior but cannot force register/shared-memory residency the way Triton kernels do. - Sparse embedding (
sparse_embedding.py): shows a training path where local slices are copied into a local buffer for computation and a distributed SignSGD step reduces gradients and writes back slices — this mirrors a “local fast working set + global parameter store” design.
Triton implementation (what to look for)
- Kernels are explicit:
triton_kernels.pycontains JIT-ed kernels that declare which data is intended to remain in SRAM (registers/shared-memory) vs. be sourced from DRAM (global memory).constexprparameters (e.g.N,BLOCK_N) cause the Triton compiler to tile loops and keep whole tiles in registers/shared memory. This is how L-level residency is enforced.- SRAM kernels:
_rms_norm_residual_fused_kerneland_swiglu_fused_sram_kernelload tile data into registers/shared memory, perform fused compute (residual add, RMS-norm, SwiGLU) entirely in-register, then store results — minimal global memory traffic. - DRAM kernels:
_rms_norm_residual_dram_kerneluses the same math but is intended to be invoked without tile reuse; each invocation does full DRAM round-trips. - State transfer:
_state_transfer_kernelis an explicit copy kernel used to move state between tiers (H↔L). This is the Triton analogue oftransfer_*inMemoryTierManagerbut compiled down to efficient device copies. - Latency probes:
_memory_latency_probe_kernelbuilds dependent load chains to measure effective latency and expose the practical difference between SRAM-like reuse and DRAM round trips. - Wrappers (Python functions at the bottom of
triton_kernels.py): reshape inputs, pick grid/block sizes, and launch the kernels (e.g.triton_rms_norm_residual_sram,triton_state_transfer). These are the call sites you'd use in place of the plain PyTorch ops where you need the explicit micro-architecture behavior.
How the mapping works: PyTorch ↔ Triton
- Operator mapping: Where PyTorch would call
x + residualfollowed byF.layer_normorF.softmax, the Triton path provides fused kernels that implement the same math but with different memory patterns (fused to avoid intermediate writes and to increase register/shared-memory reuse). - Memory behavior: In PyTorch the best you can do is allocate tensors on particular streams and do explicit copies; residency in registers/shared memory is controlled by the backend and cannot be guaranteed. Triton lets you express the exact dataflow within a kernel so you can keep a tile in registers/shared memory across many operations.
- Performance implications: Triton kernels reduce global memory bandwidth by reusing tile data (L-level) and reduce kernel-launch overhead by fusing ops; PyTorch code is simpler and relies on library kernels but may incur extra memory traffic and intermediate buffers.
Practical notes & where to change things
- To use Triton kernels in place of PyTorch ops, replace the PyTorch call sites with the wrappers in
triton_kernels.py(for example, usetriton_rms_norm_residual_sram(...)for L-level paths andtriton_rms_norm_residual_dram(...)for H-level). - Tune tile sizes (
BLOCK_N,BLOCK_INTER) by settingconstexprarguments and the_next_power_of_2helper. These control register/shared-memory usage vs parallelism. - Use
triton_state_transferto implement the same explicit copies thatMemoryTierManager.transfer_*performs; Triton copy kernels can be more efficient than.clone()in tight loops.
References / next steps
- Inspect the call sites in the model that route L-level vs H-level computation and swap in the Triton wrappers to validate behavior and measure latency.
- Use
MemoryTierManager.get_events()and the Tritonmemory_latency_probeto compare the effective behavior end-to-end.
If you want, I can:
- add small call-site examples showing the PyTorch call and the Triton replacement, or
- run a micro-benchmark that compares the two paths and append results to this doc.