Ramulator 2.1

repository·main·Indexed 20 days ago

https://github.com/cmu-safari/ramulator2

A modular, cycle-level DRAM simulator for high-performance memory system research. It features a C++ core with automated Python bindings and supports modern memory standards including DDR3/4/5, GDDR6/7, LPDDR5/6, and HBM1/2/3/4. It can operate as a standalone simulator using memory traces or be integrated as a library into other simulators like gem5.

Tokens
16.9K
Snippets
44
Records
60
Agent score
69%

What's inside ramulator2

  1. Overview of Ramulator 2.1

    main

    Ramulator 2.1 is a modular, extensible, cycle-level DRAM simulator designed for rapid implementation and evaluation of memory controller and DRAM design changes. It supports a wide range of standards including DDR3/4/5, GDDR6/7, LPDDR5/6, and HBM1/2/3/4.

    Key features:

    • Modular C++ Core: High-performance simulation logic.
    • Python Bindings: Automatically generated wrappers for easy, scriptable configuration and extension.
    • Dual Usage Modes: Can be used as a standalone simulator (taking memory traces) or integrated into other simulators (like gem5) as a library.
    • Visualizer: Includes a browser-based trace visualizer (Nuxt/WebGL).
  2. How the DRAM Device model works

    main

    The DRAMDevice converts a symbolic DRAM standard (defined in Python) into a live protocol model in C++. It maintains two simultaneous views of the channel:

    1. Hierarchical Node Tree: A hierarchy of DRAMNode objects (e.g., Channel -> Rank -> BankGroup -> Bank) used for scoped timing rules.
    2. Flat Bank View: A list of bank-oriented nodes (m_bank_nodes) used for direct command semantics.

    Key State in DRAMNodes

    Each DRAMNode tracks:

    • m_state: The protocol state (e.g., Closed, Opened).
    • m_cmd_ready_clk: The earliest cycle a specific command can next issue at this node.
    • m_cmd_history: A rolling window of recent issue times for commands (used for constraints like nFAW).
    • m_row_state: A map of currently open rows. Rows are tracked lazily; they only exist in this map if they are currently open, allowing the model to scale to large devices.

    Timing and State Machines

    • Timing (check_timing / update_timing): Timing is enforced via TimingConstraint objects. check_timing performs a recursive walk from the root to the addressed scope to ensure no constraints are violated. update_timing updates the m_cmd_ready_clk and history when a command is issued.
    • State (preq / action): The command sequence is driven by prerequisite checks. For example, a RD command's preq() might return ACT if the bank is closed, or PREpb if the bank is open to the wrong row. The controller repeatedly asks for the next legal command until the request is satisfied.
  3. Extend or create a DRAM Standard

    main

    DRAM standards are defined in Python scripts under python/ramulator/dram/. Ramulator uses these definitions to automatically generate the corresponding C++ implementation in src/ramulator/dram/impl/ during the build process.

    Creating a DRAM Variant

    To create a variant of an existing standard (e.g., adding a new command FOO to DDR3):

    1. Implement the new command in src/ramulator/dram/commands/.
    2. Inherit from the base standard in Python and specify the changes to commands, timing_params, and timing_constraints using TimingConstraint objects.

    Creating a New DRAM Standard

    A new standard requires a DRAMStandard subclass in python/ramulator/dram/ defining:

    • name, levels, commands, states, timing_params, supported_requests, org_presets, timing_presets.
    • resolve_secondary_timings() method.
    import math
    from ramulator.dram.ddr3 import DDR3
    from ramulator.dram.spec import TimingConstraint
    
    class DDR3Foo(DDR3):
        name = "DDR3Foo"
        commands = DDR3.commands + ["FOO"]
        timing_params = DDR3.timing_params + ["nFOO"]
        timing_constraints = DDR3.timing_constraints + [
            TimingConstraint(level="Bank", preceding=["FOO"], following=["ACT"], latency="nFOO"),
            TimingConstraint(level="Bank", preceding=["ACT"], following=["FOO"], latency="nRC"),
        ]
  4. Understand the Ramulator 2 configuration flow

    main

    The configuration process differs depending on whether you are using Python mode or C++ library mode:

    Python Mode

    1. Create Python component objects.
    2. Each object serializes itself using to_config().
    3. The Python binding converts the nested dictionary into a ConfigNode.
    4. The C++ factory creates the top-level frontend and memory-system objects.
    5. Child components are created recursively during their init() phase.
    6. The simulation loop advances the frontend and memory system based on their respective clock ratios.

    C++ Library Mode

    The process is identical to Python mode, except that instead of starting from Python objects, you load the ConfigNode tree directly from an exported YAML file.

  5. How the Frontend, DRAM, Controller, and Memory System components work together

    main

    Ramulator 2.1 uses a hierarchical configuration model:

    1. Frontend (ramulator.frontend): Generates memory requests. It can be a processor model (like SimpleO3) driven by traces or a synthetic generator (like LatencyThroughputTrace). It uses a translation object to handle address mapping.
    2. DRAM Device (ramulator.dram): Defines the physical characteristics of the memory (density, width, banks) using org_preset and timing constraints using timing_preset. You can override specific parameters (e.g., rank).
    3. Controller (ramulator.controller): Manages the DRAM device. It uses a scheduler (e.g., FRFCFS), a refresh_manager (e.g., AllBank), a row_policy (e.g., Open), and an addr_mapper (e.g., RoBaRaCoCh).
    4. Memory System (ramulator.memory_system): A wrapper around one or more controllers. It uses a channel_mapper (e.g., CacheLineInterleave) to distribute requests across controllers. It also defines a clock_ratio for the memory-side tick rate.
  6. Build Ramulator 2.1 from source

    main

    To build the simulator in its default mode (which includes the C++ library, Python extension modules, and runs the code generator), follow these steps from the repository root:

    Default Build (with Python bindings):

    mkdir -p build
    cd build
    cmake ..
    make -j
    cd ..

    Pure C++ Library Build (without Python bindings): If you only need the C++ library for integration into your own simulator and do not require Python support, use the RAMULATOR_PYTHON_BINDINGS=OFF flag.

    mkdir -p build
    cd build
    cmake .. -DRAMULATOR_PYTHON_BINDINGS=OFF
    make -j
    cd ..
  7. Extend Ramulator 2 with custom components

    main

    Ramulator 2 uses an interface and implementation pattern for its components. To create a new component, you must define an interface class (prefixed with I, e.g., IScheduler) and a concrete implementation class that inherits from both the interface and the common Implementation class.

    To make your custom components discoverable by the automatic self-registering component factory (which allows them to be instantiated via Python configuration), use the following macros in your C++ code:

    • RAMULATOR_REGISTER_INTERFACE(IfceClassName, "ifce_name"): Registers the interface.
    • RAMULATOR_REGISTER_IMPLEMENTATION(IfceClassName, ImplClassName, "ImplName"): Registers the implementation. Once registered, the component can be accessed in Python using the path ramulator.<ifce_name>.<ImplName>.

    To handle parameter parsing and child component creation within your implementation, use:

    • RAMULATOR_PARSE_PARAM(parsed_variable, type_t, "param_name")
    • RAMULATOR_CREATE_CHILD(IfceClassName, "ifce_name")
    RAMULATOR_REGISTER_INTERFACE(IScheduler, "scheduler")
    RAMULATOR_REGISTER_IMPLEMENTATION(IScheduler, MyCustomScheduler, "MyCustomScheduler")
    
    // Inside implementation
    RAMULATOR_PARSE_PARAM(m_priority, int, "priority")
    RAMULATOR_CREATE_CHILD(IFrontEnd, "frontend")
  8. Add a new C++ implementation (e.g., a Scheduler)

    main

    To add a new implementation of an existing interface (like a scheduler), create a .cpp file that inherits from the target interface (e.g., IScheduler) and the Implementation base class.

    Key steps:

    1. Use RAMULATOR_REGISTER_IMPLEMENTATION(Interface, ClassName, "Name") to register the class. This automatically generates Python wrappers.
    2. Use RAMULATOR_PARSE_PARAM(variable, type, "param_name") within init() to parse configuration parameters.
    3. Use cast_parent<ParentType>() to access the parent component.
    4. Use m_stats.add("stat_name", variable) to register variables for automatic statistics printing.
    5. Override init(), setup(), and the interface's virtual functions (e.g., get_best_request) to implement logic.
    6. Add the file to the relevant CMakeLists.txt and rebuild.

    Once built, the new component is available in Python via the generated wrapper.

    #include "controller/controller_base.h"
    #include "controller/scheduler/i_scheduler.h"
    
    namespace Ramulator {
    
    class FooBarScheduler : public IScheduler, public Implementation {
      RAMULATOR_REGISTER_IMPLEMENTATION(IScheduler, FooBarScheduler, "FooBar")
    
      ControllerBase* m_ctrl = nullptr;
      int m_weight = 0;
      size_t s_decisions = 0;
    
      void init() override {
        RAMULATOR_PARSE_PARAM(m_weight, int, "weight").default_val(4);
        m_ctrl = cast_parent<ControllerBase>();
        m_stats.add("foobar_decisions", s_decisions);
      }
    
      void setup(IFrontEnd* frontend, IMemorySystem* memory_system) override {
        // Resolve configurations that depend on other components
      }
    
      ReqBuffer::iterator get_best_request(ReqBuffer& buffer, RequestFilterRef filter) override {
        // Implement logic
      }
    };
    
    }
  9. Install the Ramulator Python Package

    main

    After building the project, install the Python package in editable mode. This allows you to run python -m ramulator or import ramulator from any directory.

    Recommended Installation (using a virtual environment): If you encounter installation issues, create and activate a virtual environment first.

    python3 -m venv ramulator2-venv
    source ramulator2-venv/bin/activate
    pip install -e .

    One-off alternative (without installation): If you prefer not to install the package, you can set the PYTHONPATH manually to point to the python directory.

    PYTHONPATH=python python3 examples/example_config.py
    pip install -e .
  10. Set up Ramulator 2.1 using Docker

    main

    To avoid dependency issues, it is highly recommended to use the provided Dev Container. You can open the repository directly in a GitHub Codespace or set up the container locally using Docker Compose.

    To set up the container locally:

    1. Build and start the container.
    2. Execute into the container bash session. The repository will be mounted at /workspace and the ramulator2-venv will be automatically activated.
    docker compose up -d --build --wait
    docker compose exec ramulator2 bash
  11. Run the Trace Visualizer with Docker

    main

    Ramulator 2.1 provides a browser-based visualizer for command timelines, request swimlanes, and throughput charts. You can run it using Docker Compose profiles.

    • Production mode: Builds a self-contained image and serves at http://localhost:3000.
    • Development mode: Enables hot-reload.
    • Custom Port: Use the RAMULATOR_VISUALIZER_PORT environment variable to change the default port.
    # Production build
    docker compose --profile visualizer up --build
    
    # Development with hot-reload
    docker compose --profile visualizer-dev up
    
    # Production with custom port
    RAMULATOR_VISUALIZER_PORT=4000 docker compose --profile visualizer up --build
  12. Configure different DRAM standards and controllers

    main

    To change the memory standard, replace both the DRAM object and the corresponding controller class.

    • DDR3, DDR4, DDR5, GDDR6: Use ramulator.dram.<Standard> and ramulator.controller.GenericDDR.
    • LPDDR5: Use ramulator.dram.LPDDR5 and ramulator.controller.LPDDR5.
    • HBM1, HBM2: Use ramulator.dram.HBM2 and ramulator.controller.HBM12.
    • HBM3, HBM4: Use ramulator.dram.HBM34 (implied by controller name) and ramulator.controller.HBM34.
    # Example: DDR5
    dram = ramulator.dram.DDR5(org_preset="DDR5_8Gb_x8", timing_preset="DDR5_4800AN")
    ctrl = ramulator.controller.GenericDDR(dram=dram, ...)
    
    # Example: LPDDR5
    dram = ramulator.dram.LPDDR5(org_preset="LPDDR5_8Gb_x16", timing_preset="LPDDR5_5500")
    ctrl = ramulator.controller.LPDDR5(dram=dram, ...)
    
    # Example: HBM2
    dram = ramulator.dram.HBM2(org_preset="HBM2_2Gb", timing_preset="HBM2_2000Mbps")
    ctrl = ramulator.controller.HBM12(dram=dram, ...)