Python bindings for the Ramulator DRAM simulator.
A thin, event-driven Python wrapper over Ramulator 1.x for DRAM-side simulation of custom hardware accelerator architectures: cycle-accurate latency, bandwidth and row-buffer behaviour without modifying a single line of Ramulator.
- Cycle-accurate DRAM timing — DDR3/4, LPDDR3/4, GDDR5, WideIO/2, HBM, SALP, with channel/rank scaling and per-standard timing
- Event-driven API — non-blocking request send with backpressure, async
completion callbacks (
RequestInfowith latency and core id), write barrier (flush()), drain (run_until_idle()) - Unmodified Ramulator — only its public API is used, exactly like
gem5's
Gem5Wrapper; Ramulator is a pinned git submodule - Fast — batched C++ simulation (
run,send_batch,send_range) keeps per-request Python overhead low (~150K req/s sustained) - Programmable statistics —
get_stats()exposes all 170+ Ramulator counters (latency, row hits/misses, queue depths, bandwidth) plus derived metrics helpers (avg_read_latency,row_hit_rate,measured_bandwidth) - Helpers — address-stream generators, one-call benchmarks, capacity estimation, theoretical bandwidth
Ramulator is an external dependency, registered as a git submodule at
third_party/ramulator (pinned to a fixed commit), so the build needs
network access.
git clone --recurse-submodules <repo-url> # or: git submodule update --init
pip install .When building from an sdist (which has no submodule), CMake falls back to
fetching ramulator from its upstream repository via FetchContent.
from pyramulator import Config, MemorySystem, RequestType
cfg = Config(standard="DDR4", speed="DDR4_2400R", org="DDR4_4Gb_x8")
mem = MemorySystem(cfg, cacheline=64)
completed = []
mem.send_read(0x1000, callback=lambda info: completed.append(info))
mem.run(1000) # or mem.tick() per cycle
print(completed[0]) # RequestInfo(addr=4096, type=READ, arrive=.., depart=.., core_id=0)
print(completed[0].latency) # latency in DRAM clock cyclesThe wrapper follows the same approach gem5 uses for Ramulator, and only calls Ramulator's public API — no Ramulator source is modified:
- Simulation core:
MemoryFactory::create+ theMemoryBaseinterface (tick/send/finish), the same calls gem5'sGem5Wrappermakes. - Configuration:
Config.add/contains/set_core_numonly. There is no value-overwrite setter;Config.from_fileparses the.cfgtext in Python and rebuilds a freshConfigwith the overrides applied. - Request completion: read completions are delivered via Ramulator's
callback (it already fires for reads). Ramulator has no write-completion
callback upstream, so — like gem5, which answers writes immediately upon
acceptance — write callbacks fire right when the request is accepted. Use
mem.flush()if you need to wait until writes are truly serviced by the DRAM.
A typical accelerator-simulation loop:
mem = MemorySystem(cfg)
done = [0]
def on_complete(info): # your completion handler
done[0] += 1
issued = 0
addr = 0
while done[0] < NUM_REQUESTS:
# 1. keep the request queue full (backpressure: send returns False
# when the queue is full — retry next cycle)
while issued - done[0] < 32 and issued < NUM_REQUESTS:
if mem.send_read(addr, callback=on_complete):
issued += 1
addr += 64
else:
break
# 2. advance the DRAM clock (batched inside C++ for speed)
mem.run(1000)
mem.flush() # drain remaining reads AND writesFor initial bursts, send_read_blocking(addr) blocks until accepted;
send_reads() / send_writes() / send_reads_range() / send_writes_range()
batch many requests in one C++ call.
For a self-contained drive loop — the role gem5's MemCtrl scheduler plays —
drive() / drive_range() run the whole backpressure + tick + drain loop
inside C++:
issued = mem.drive_range(0, NUM_REQUESTS, 64, queue_depth=32, batch=200,
callback=on_complete)batch trades scheduling granularity for cycle efficiency: ~queue_depth x 6
cycles wastes no DRAM time (measured at parity with the Python loop,
~150K req/s, with the loop overhead removed).
The channel/bank/row interleaving scheme is selectable via mapping
(Ramulator honors it for DDR3):
cfg = Config(standard="DDR3", speed="DDR3_1600K", org="DDR3_2Gb_x8",
mapping="cacheline_interleaving") # row_interleaving, *_randomized, defaultmappingmem.reset_stats() # per simulation phase
mem.run_until_idle()
stats = mem.get_stats() # 170+ raw Ramulator counters
print(stats["read_requests"], stats["read_latency_sum_0"])
from pyramulator import metrics
print(mem.metrics()) # derived summary dict:
# {'read_requests': 256, 'dram_cycles': 1460, 'avg_read_latency_cycles': 193.2,
# 'avg_read_latency_ns': 161.0, 'row_hit_rate': 0.98, 'bandwidth_gbs': 13.5}from pyramulator import benchmark_latency, benchmark_bandwidth, addresses
benchmark_latency(cfg, num_requests=256, mode="random", seed=42)
benchmark_bandwidth(cfg, num_requests=256)
addrs = addresses("sequential", 256, cacheline=64) # or "strided" / "random"
mem.send_reads(addrs)DDR3, DDR4, LPDDR3, LPDDR4, GDDR5, WideIO, WideIO2, HBM, SALP-1, SALP-2, SALP-MASA
The DRAM model itself is simulated at C++ speed (~3.6M DRAM cycles/s on a
single channel DDR4). Sustained request throughput with per-request Python
callbacks is ~150K req/s; callbacks are delivered in batches (no per-event
GIL round-trip). See bench/bench.py for measured latency and bandwidth
across standards.
examples/spmm_hbm.py— a naive single-PE SpMM accelerator streaming the dense matrix from an HBM timing model (event-driven loop with a bounded in-flight window, validated against numpy, reports cycles/bandwidth/row hits). Run withpython examples/spmm_hbm.py [channels].examples/accel_sim.py— generic accelerator-style read/write traffic.
Build, test and check inside a local virtualenv:
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]" # includes test, ruff, mypy, pytest-cov
pytest # 79 tests, ~95% coverage
ruff check pyramulator/ test/ bench/ examples/
mypy pyramulator/
python bench/bench.pySee CONTRIBUTING.md for the full workflow.
See CHANGELOG.md.
src/bindings.cpp— pybind11 bindings (the only C++ in this project)pyramulator/— pure-Python wrapper APIconfigs.py— standards/speed/org tables, capacity & bandwidth estimationmetrics.py— derived performance metricsworkload.py— address-stream generatorsbenchmark.py— one-call latency/bandwidth benchmarks
third_party/ramulator— Ramulator as a git submodule (pinned commit), used unmodified- Ramulator is not vendored; see
git submodule statusand https://github.com/CMU-SAFARI/ramulator
MIT — see LICENSE. Ramulator itself is MIT-licensed (Copyright 2015 SAFARI Research Group, Carnegie Mellon University).