From 58ebccc1f88d5e996dc7c934138ac4375c22b315 Mon Sep 17 00:00:00 2001 From: nab880 Date: Thu, 24 Sep 2026 23:23:25 -0700 Subject: [PATCH] balar: add cached Doorbell test driver and contracts Exercise cached packet writes, per-line flushes and MMIO doorbells with a dedicated driver and coherent test topology. Split cached return reads, reserve separate payload storage and validate D2H data through simulated-memory readback. Keep the existing MMIO protocol. --- src/sst/elements/balar/Makefile.am | 7 +- .../elements/balar/testcpu/doorbellTestCPU.cc | 900 ++++++++++++++++++ .../elements/balar/testcpu/doorbellTestCPU.h | 169 ++++ .../balar/tests/balar_test_topology.py | 144 ++- .../balar/tests/testBalar_testsuite_util.py | 63 ++ .../balar/tests/testDoorbellCPU-doorbell.py | 18 + .../tests/testDoorbellCPU-malloc-free.py | 19 + .../tests/testDoorbellCPU-wide-packet.py | 19 + .../balar/tests/testsuite_default_balar.py | 26 + 9 files changed, 1363 insertions(+), 2 deletions(-) create mode 100644 src/sst/elements/balar/testcpu/doorbellTestCPU.cc create mode 100644 src/sst/elements/balar/testcpu/doorbellTestCPU.h create mode 100644 src/sst/elements/balar/tests/testDoorbellCPU-doorbell.py create mode 100644 src/sst/elements/balar/tests/testDoorbellCPU-malloc-free.py create mode 100644 src/sst/elements/balar/tests/testDoorbellCPU-wide-packet.py diff --git a/src/sst/elements/balar/Makefile.am b/src/sst/elements/balar/Makefile.am index 0a8bf772dc..25bd979b09 100644 --- a/src/sst/elements/balar/Makefile.am +++ b/src/sst/elements/balar/Makefile.am @@ -22,7 +22,9 @@ libbalar_la_SOURCES = \ util.cc \ util.h \ testcpu/balarTestCPU.cc \ - testcpu/balarTestCPU.h + testcpu/balarTestCPU.h \ + testcpu/doorbellTestCPU.cc \ + testcpu/doorbellTestCPU.h EXTRA_DIST = \ tests/refFiles/test_gpgpu_rodinia-2.0-backprop-1024.out \ @@ -78,6 +80,9 @@ EXTRA_DIST = \ tests/testBalar-doorbell.py \ tests/testBalar-malloc-free.py \ tests/testBalar-wide-packet.py \ + tests/testDoorbellCPU-doorbell.py \ + tests/testDoorbellCPU-malloc-free.py \ + tests/testDoorbellCPU-wide-packet.py \ tests/balar_test_topology.py \ tests/traces/doorbell_malloc.trace \ tests/traces/malloc_free.trace \ diff --git a/src/sst/elements/balar/testcpu/doorbellTestCPU.cc b/src/sst/elements/balar/testcpu/doorbellTestCPU.cc new file mode 100644 index 0000000000..7623174b66 --- /dev/null +++ b/src/sst/elements/balar/testcpu/doorbellTestCPU.cc @@ -0,0 +1,900 @@ +// Copyright 2009-2026 NTESS. Under the terms +// of Contract DE-NA0003525 with NTESS, the U.S. +// Government retains certain rights in this software. +// +// Copyright (c) 2009-2026, NTESS +// All rights reserved. +// +// This file is part of the SST software package. For license +// information, see the LICENSE file in the top level directory of the +// distribution. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "testcpu/doorbellTestCPU.h" +#include "util.h" + +using namespace SST; +using namespace SST::Interfaces; +using namespace SST::BalarComponent; +using namespace SST::Statistics; + +namespace { + +std::string trim(const std::string& s) +{ + size_t start = s.find_first_not_of(" \t"); + if (start == std::string::npos) + return ""; + size_t end = s.find_last_not_of(" \t"); + return s.substr(start, end - start + 1); +} + +std::vector split(const std::string& s, const std::string& delim) +{ + std::vector out; + size_t pos = 0; + while (pos < s.size()) { + size_t next = s.find(delim, pos); + if (next == std::string::npos) { + out.push_back(s.substr(pos)); + break; + } + out.push_back(s.substr(pos, next - pos)); + pos = next + delim.size(); + } + return out; +} + +std::map map_from_vec(const std::vector& params, const std::string& delim) +{ + std::map m; + for (const auto& p : params) { + size_t pos = p.find(delim); + if (pos != std::string::npos) { + m[trim(p.substr(0, pos))] = trim(p.substr(pos + delim.size())); + } + } + return m; +} + +std::string lookup_param(const std::map& params, const std::string& key, SST::Output* out) +{ + auto it = params.find(key); + if (it != params.end()) { + return trim(it->second); + } + for (const auto& param : params) { + if (trim(param.first) == key) { + return trim(param.second); + } + } + std::ostringstream keys; + for (const auto& param : params) { + keys << " '" << param.first << "'"; + } + out->fatal(CALL_INFO, -1, "Trace parameter '%s' not found. Available keys:%s\n", key.c_str(), keys.str().c_str()); + return ""; +} + +// The numeric parses below fatal() on malformed input instead of letting +// std::stoull/stoll/stod throw an uncaught exception that would abort the run. +uint64_t parse_u64(const std::string& what, const std::string& raw, SST::Output* out) +{ + std::string value = trim(raw); + try { + size_t consumed = 0; + uint64_t v = std::stoull(value, &consumed); + if (consumed != value.size()) { + out->fatal(CALL_INFO, -1, "Trace value for '%s' is not a valid unsigned integer: '%s'\n", + what.c_str(), value.c_str()); + } + return v; + } catch (const std::exception&) { + out->fatal(CALL_INFO, -1, "Trace value for '%s' is not a valid unsigned integer: '%s'\n", + what.c_str(), value.c_str()); + } + return 0; +} + +int64_t parse_i64(const std::string& what, const std::string& raw, SST::Output* out) +{ + std::string value = trim(raw); + try { + size_t consumed = 0; + int64_t v = std::stoll(value, &consumed); + if (consumed != value.size()) { + out->fatal(CALL_INFO, -1, "Trace value for '%s' is not a valid integer: '%s'\n", + what.c_str(), value.c_str()); + } + return v; + } catch (const std::exception&) { + out->fatal(CALL_INFO, -1, "Trace value for '%s' is not a valid integer: '%s'\n", + what.c_str(), value.c_str()); + } + return 0; +} + +double parse_double(const std::string& what, const std::string& raw, SST::Output* out) +{ + std::string value = trim(raw); + try { + size_t consumed = 0; + double v = std::stod(value, &consumed); + if (consumed != value.size()) { + out->fatal(CALL_INFO, -1, "Trace value for '%s' is not a valid float: '%s'\n", + what.c_str(), value.c_str()); + } + return v; + } catch (const std::exception&) { + out->fatal(CALL_INFO, -1, "Trace value for '%s' is not a valid float: '%s'\n", + what.c_str(), value.c_str()); + } + return 0.0; +} + +uint64_t lookup_u64(const std::map& params, const std::string& key, SST::Output* out) +{ + return parse_u64(key, lookup_param(params, key, out), out); +} + +} // namespace + +class DoorbellTestCPU::CacheHandlers : public StandardMem::RequestHandler { +public: + friend class DoorbellTestCPU; + CacheHandlers(DoorbellTestCPU* cpu, SST::Output* out) : StandardMem::RequestHandler(out), cpu_(cpu) {} + virtual ~CacheHandlers() {} + void handle(StandardMem::ReadResp* resp) override { cpu_->onCacheReadResp(resp); } + void handle(StandardMem::WriteResp* resp) override { cpu_->onCacheWriteResp(resp); } + void handle(StandardMem::FlushResp* resp) override { cpu_->onCacheFlushResp(resp); } + void handle(StandardMem::Write* write) override { cpu_->onCacheWriteReq(write); } + +private: + DoorbellTestCPU* cpu_; +}; + +class DoorbellTestCPU::MmioHandlers : public StandardMem::RequestHandler { +public: + friend class DoorbellTestCPU; + MmioHandlers(DoorbellTestCPU* cpu, SST::Output* out) : StandardMem::RequestHandler(out), cpu_(cpu) {} + virtual ~MmioHandlers() {} + void handle(StandardMem::ReadResp* resp) override { cpu_->onMmioReadResp(resp); } + void handle(StandardMem::WriteResp* resp) override { cpu_->onMmioWriteResp(resp); } + void handle(StandardMem::Write* write) override { cpu_->onMmioWriteReq(write); } + +private: + DoorbellTestCPU* cpu_; +}; + +class DoorbellTestCPU::CudaAPITraceParser { +public: + CudaAPITraceParser(DoorbellTestCPU* cpu, SST::Output* out, const std::string& trace_file, const std::string& cuda_executable) + : cpu_(cpu), out_(out), cuda_executable_(cuda_executable), fat_cubin_handle_(0), has_peeked_packet_(false) + { + trace_stream_.open(trace_file, std::ifstream::in); + if (!trace_stream_.is_open()) { + out_->fatal(CALL_INFO, -1, "Error: trace file '%s' does not exist\n", trace_file.c_str()); + } + size_t sep = trace_file.rfind("/"); + trace_base_path_ = (sep == std::string::npos) ? "./" : trace_file.substr(0, sep + 1); + + BalarCudaCallPacket_t fatbin{}; + fatbin.cuda_call_id = CUDA_REG_FAT_BINARY; + fatbin.isSSTmem = false; + strncpy(fatbin.register_fatbin.file_name, cuda_executable_.c_str(), BALAR_CUDA_MAX_FILE_NAME - 1); + fatbin.register_fatbin.file_name[BALAR_CUDA_MAX_FILE_NAME - 1] = '\0'; + init_packets_.push(fatbin); + } + + ~CudaAPITraceParser() + { + for (void* p : heap_allocs_) { + free(p); + } + for (auto& kv : dptr_map_) { + free(kv.second); + } + } + + bool getNextPacket(BalarCudaCallPacket_t& pack) + { + if (has_peeked_packet_) { + pack = peeked_packet_; + has_peeked_packet_ = false; + return true; + } + if (!init_packets_.empty()) { + pack = init_packets_.front(); + init_packets_.pop(); + return true; + } + std::string line; + // Skip blank lines; stop only at a genuine end of stream so an interior + // blank line cannot silently truncate the trace. + do { + if (!std::getline(trace_stream_, line)) { + return false; + } + } while (trim(line).empty()); + out_->verbose(CALL_INFO, 2, 0, "Trace: %s\n", line.c_str()); + + pack = BalarCudaCallPacket_t{}; + pack.isSSTmem = false; + + size_t first_col = line.find(":"); + std::string cuda_call_type = line.substr(0, first_col); + { + std::string rest = line.substr(first_col + 1); + line = trim(rest); + } + auto params_map = map_from_vec(split(line, ","), ":"); + + if (cuda_call_type.find("memalloc") != std::string::npos) { + pack.cuda_call_id = CUDA_MALLOC; + std::string dptr_name = lookup_param(params_map, "dptr", out_); + size_t size = 0; + std::stringstream(lookup_param(params_map, "size", out_)) >> size; + auto it = dptr_map_.find(dptr_name); + if (it == dptr_map_.end()) { + auto* dptr = (CUdeviceptr*)malloc(sizeof(CUdeviceptr)); + dptr_map_[dptr_name] = dptr; + pack.cuda_malloc.devPtr = (void**)dptr; + } else { + pack.cuda_malloc.devPtr = (void**)it->second; + } + pack.cuda_malloc.size = size; + return true; + } + if (cuda_call_type.find("memcpyH2D") != std::string::npos || cuda_call_type.find("memcpyD2H") != std::string::npos) { + pack.cuda_call_id = CUDA_MEMCPY; + std::string dptr_name = lookup_param(params_map, "device_ptr", out_); + size_t size = 0; + std::stringstream(lookup_param(params_map, "size", out_)) >> size; + std::string data_path = trace_base_path_ + lookup_param(params_map, "data_file", out_); + std::ifstream data_stream(data_path, std::ios::binary); + if (!data_stream.is_open()) { + out_->fatal(CALL_INFO, -1, "Error: data file '%s' not found\n", data_path.c_str()); + } + std::vector file_data(size); + data_stream.read((char*)file_data.data(), size); + uint8_t* real_data = (uint8_t*)malloc(size); + heap_allocs_.push_back(real_data); + memcpy(real_data, file_data.data(), size); + auto it = dptr_map_.find(dptr_name); + if (it == dptr_map_.end()) { + out_->fatal(CALL_INFO, -1, "Unknown device pointer '%s'\n", dptr_name.c_str()); + } + if (cuda_call_type.find("memcpyH2D") != std::string::npos) { + pack.isSSTmem = true; + pack.cuda_memcpy.kind = cudaMemcpyHostToDevice; + pack.cuda_memcpy.dst = *it->second; + pack.cuda_memcpy.count = size; + pack.cuda_memcpy.payload = (uint64_t)real_data; + pack.cuda_memcpy.src = cpu_->scratchDataAddr(); + cpu_->scratch_append_payload_ = std::move(file_data); + } else { + uint8_t* buf = (uint8_t*)malloc(size); + heap_allocs_.push_back(buf); + pack.cuda_memcpy.kind = cudaMemcpyDeviceToHost; + pack.cuda_memcpy.dst = (uint64_t)buf; + pack.cuda_memcpy.src = (uint64_t)*it->second; + pack.cuda_memcpy.count = size; + pack.cuda_memcpy.payload = (uint64_t)real_data; + if (size >= cpu_->cacheLineSize_) { + pack.isSSTmem = true; + pack.cuda_memcpy.dst = cpu_->scratchDataAddr(); + pack.cuda_memcpy.dst_buf = buf; + } + } + return true; + } + if (cuda_call_type.find("kernel launch") != std::string::npos) { + std::string func_name = lookup_param(params_map, "name", out_); + std::string ptx_name = lookup_param(params_map, "ptx_name", out_); + BalarCudaCallPacket_t config{}, set_arg{}, launch{}, reg_fn{}; + config.cuda_call_id = CUDA_CONFIG_CALL; + set_arg.cuda_call_id = CUDA_SET_ARG; + launch.cuda_call_id = CUDA_LAUNCH; + config.configure_call.gdx = lookup_u64(params_map, "gdx", out_); + config.configure_call.gdy = lookup_u64(params_map, "gdy", out_); + config.configure_call.gdz = lookup_u64(params_map, "gdz", out_); + config.configure_call.bdx = lookup_u64(params_map, "bdx", out_); + config.configure_call.bdy = lookup_u64(params_map, "bdy", out_); + config.configure_call.bdz = lookup_u64(params_map, "bdz", out_); + config.configure_call.sharedMem = lookup_u64(params_map, "sharedBytes", out_); + config.configure_call.stream = nullptr; + init_packets_.push(config); + + if (func_map_.find(func_name) == func_map_.end()) { + uint64_t func_id = func_map_.size(); + func_map_[func_name] = func_id; + reg_fn.cuda_call_id = CUDA_REG_FUNCTION; + reg_fn.register_function.fatCubinHandle = fat_cubin_handle_; + reg_fn.register_function.hostFun = func_id; + strncpy(reg_fn.register_function.deviceFun, ptx_name.c_str(), BALAR_CUDA_MAX_KERNEL_NAME - 1); + pack = reg_fn; + } else { + pack = config; + init_packets_.pop(); + } + + std::string arguments = lookup_param(params_map, "args", out_); + size_t offset = 0; + while (!arguments.empty()) { + size_t pos = arguments.find("/"); + std::string arg_val = arguments.substr(0, pos); + arguments = arguments.substr(pos + 1); + pos = arguments.find("/"); + std::string arg_size_str = arguments.substr(0, pos); + arguments = arguments.substr(pos + 1); + size_t arg_size = (size_t)parse_u64("kernel arg size", arg_size_str, out_); + if (arg_size == 0) { + out_->fatal(CALL_INFO, -1, "Kernel arg '%s' has size 0; arg size must be > 0\n", + arg_val.c_str()); + } + if (arg_size > BALAR_CUDA_MAX_ARG_SIZE) { + out_->fatal(CALL_INFO, -1, "Kernel arg '%s' size %zu exceeds max %d\n", + arg_val.c_str(), arg_size, BALAR_CUDA_MAX_ARG_SIZE); + } + size_t align_amount = arg_size; + offset = (offset + align_amount - 1) / align_amount * align_amount; + set_arg.setup_argument.size = arg_size; + set_arg.setup_argument.offset = offset; + offset += arg_size; + if (arg_val.find("dptr") != std::string::npos) { + auto dit = dptr_map_.find(arg_val); + if (dit == dptr_map_.end()) { + out_->fatal(CALL_INFO, -1, "Unknown device pointer kernel arg '%s'\n", arg_val.c_str()); + } + set_arg.setup_argument.arg = (uint64_t)*dit->second; + } else if (arg_val.find(".") != std::string::npos) { + double val = parse_double("kernel arg", arg_val, out_); + set_arg.setup_argument.arg = 0; + if (arg_size == 8) { + memcpy(set_arg.setup_argument.value, &val, arg_size); + } else { + float val_f = (float)val; + memcpy(set_arg.setup_argument.value, &val_f, arg_size); + } + } else { + int64_t val = parse_i64("kernel arg", arg_val, out_); + set_arg.setup_argument.arg = 0; + if (arg_size > sizeof(val)) { + out_->fatal(CALL_INFO, -1, "Integer kernel arg size %zu exceeds %zu bytes\n", + arg_size, sizeof(val)); + } + memcpy(set_arg.setup_argument.value, &val, arg_size); + } + init_packets_.push(set_arg); + } + launch.cuda_launch.func = func_map_.at(func_name); + init_packets_.push(launch); + return true; + } + if (cuda_call_type.find("free") != std::string::npos) { + pack.cuda_call_id = CUDA_FREE; + std::string dptr_name = lookup_param(params_map, "dptr", out_); + auto it = dptr_map_.find(dptr_name); + if (it == dptr_map_.end()) { + out_->fatal(CALL_INFO, -1, "Unknown device pointer '%s'\n", dptr_name.c_str()); + } + pack.cuda_free.devPtr = (void*)*it->second; + return true; + } + out_->fatal(CALL_INFO, -1, "Unrecognized CUDA trace line: '%s'\n", line.c_str()); + return false; + } + + bool hasNextPacket() + { + if (has_peeked_packet_) { + return true; + } + has_peeked_packet_ = getNextPacket(peeked_packet_); + return has_peeked_packet_; + } + + void setFatbinHandle(uint64_t handle) { fat_cubin_handle_ = handle; } + +private: + DoorbellTestCPU* cpu_; + SST::Output* out_; + std::string cuda_executable_; + std::string trace_base_path_; + std::ifstream trace_stream_; + std::queue init_packets_; + std::map dptr_map_; + std::map func_map_; + std::vector heap_allocs_; + uint64_t fat_cubin_handle_; + bool has_peeked_packet_; + BalarCudaCallPacket_t peeked_packet_; +}; + +DoorbellTestCPU::DoorbellTestCPU(ComponentId_t id, Params& params) : Component(id) +{ + out.init("DoorbellTestCPU[@f:@l:@p] ", params.find("verbose", 1), 0, Output::STDOUT); + + bool found = false; + mmioAddr_ = params.find("mmio_addr", "0", found); + scratchMemAddr_ = params.find("scratch_mem_addr", "0", found); + cacheLineSize_ = params.find("cache_line_size", "64", found); + if (cacheLineSize_ == 0) { + out.fatal(CALL_INFO, -1, "cache_line_size must be > 0\n"); + } + + std::string mode_str = params.find("mode", "trace"); + if (mode_str == "doorbell_only") { + mode_ = DriverMode::DOORBELL_ONLY; + } else { + mode_ = DriverMode::TRACE; + } + + registerAsPrimaryComponent(); + primaryComponentDoNotEndSim(); + + std::string clock_freq = params.find("clock", "1GHz"); + clock_handler_ = new Clock::Handler(this); + clockTC_ = registerClock(clock_freq, clock_handler_); + + cache_link_ = loadUserSubComponent( + "cache_link", ComponentInfo::SHARE_NONE, clockTC_, + new StandardMem::Handler(this)); + mmio_link_ = loadUserSubComponent( + "mmio_link", ComponentInfo::SHARE_NONE, clockTC_, + new StandardMem::Handler(this)); + + if (!cache_link_ || !mmio_link_) { + out.fatal(CALL_INFO, -1, "DoorbellTestCPU requires cache_link and mmio_link subcomponents\n"); + } + + cache_handlers_ = new CacheHandlers(this, &out); + mmio_handlers_ = new MmioHandlers(this, &out); + + stat_cache_writes_ = registerStatistic("cache_writes"); + stat_mmio_writes_ = registerStatistic("mmio_writes"); + stat_flush_count_ = registerStatistic("flush_count"); + stat_cuda_calls_completed_ = registerStatistic("cuda_calls_completed"); + stat_total_memD2H_bytes_ = registerStatistic("total_memD2H_bytes"); + stat_correct_memD2H_bytes_ = registerStatistic("correct_memD2H_bytes"); + stat_correct_memD2H_ratio_ = registerStatistic("correct_memD2H_ratio"); + + enable_memcpy_dump_ = params.find("enable_memcpy_dump", false); + issued_initial_packet_ = false; + calls_completed_ = 0; + packet_issue_active_ = false; + scratch_append_payload_.clear(); + trace_parser_ = nullptr; + + cuda_executable_ = params.find("cuda_executable", found); + if (mode_ == DriverMode::TRACE) { + if (!found) { + out.fatal(CALL_INFO, -1, "cuda_executable required in trace mode\n"); + } + std::string trace_file = params.find("trace_file", "cuda_calls.trace"); + trace_parser_ = new CudaAPITraceParser(this, &out, trace_file, cuda_executable_); + } else if (!found) { + cuda_executable_ = "./balar_trace/vectorAdd"; + } +} + +DoorbellTestCPU::~DoorbellTestCPU() +{ + delete trace_parser_; + delete cache_handlers_; + delete mmio_handlers_; +} + +void DoorbellTestCPU::init(unsigned int phase) +{ + cache_link_->init(phase); + mmio_link_->init(phase); +} + +void DoorbellTestCPU::setup() +{ + cache_link_->setup(); + mmio_link_->setup(); +} + +void DoorbellTestCPU::finish() {} + +void DoorbellTestCPU::emergencyShutdown() +{ + if (out.getVerboseLevel() > 1) { + out.output("DoorbellTestCPU %s outstanding=%zu\n", getName().c_str(), requests_.size()); + } +} + +void DoorbellTestCPU::handleCacheEvent(StandardMem::Request* req) { req->handle(cache_handlers_); } +void DoorbellTestCPU::handleMmioEvent(StandardMem::Request* req) { req->handle(mmio_handlers_); } + +void DoorbellTestCPU::uint64ToData(uint64_t num, std::vector* data) +{ + data->clear(); + for (size_t i = 0; i < sizeof(uint64_t); i++) { + data->push_back(num & 0xFF); + num >>= 8; + } +} + +uint64_t DoorbellTestCPU::dataToUInt64(std::vector* data) +{ + uint64_t retval = 0; + for (int i = (int)data->size() - 1; i >= 0; i--) { + retval <<= 8; + retval |= (*data)[i]; + } + return retval; +} + +void DoorbellTestCPU::beginPacketIssue(const BalarCudaCallPacket_t& pack) +{ + active_packet_ = pack; + BalarCudaCallPacket_t pack_copy = pack; + vector* encoded = encode_balar_packet(&pack_copy); + packet_buffer_.assign(encoded->begin(), encoded->end()); + delete encoded; + if (!scratch_append_payload_.empty()) { + // The return packet is written back over the request. Keep memcpy data + // beyond both packet layouts, on a separate cache line. + packet_buffer_.resize(scratchDataAddr() - scratchMemAddr_, 0); + packet_buffer_.insert(packet_buffer_.end(), scratch_append_payload_.begin(), scratch_append_payload_.end()); + scratch_append_payload_.clear(); + } + + scratch_write_offset_ = 0; + scratch_bytes_remaining_ = packet_buffer_.size(); + packet_issue_active_ = true; + + out.verbose(_INFO_, "%s: issue packet %s (%zu bytes) to scratch 0x%lx\n", getName().c_str(), + CudaAPIEnumToString(pack.cuda_call_id), packet_buffer_.size(), scratchMemAddr_); + sendNextScratchChunk(); +} + +uint64_t DoorbellTestCPU::scratchDataAddr() const +{ + const size_t header_size = std::max(sizeof(BalarCudaCallPacket_t), sizeof(BalarCudaCallReturnPacket_t)); + if (scratchMemAddr_ > std::numeric_limits::max() - header_size) { + out.fatal(CALL_INFO, -1, "Scratch packet address overflows\n"); + } + const uint64_t header_end = scratchMemAddr_ + header_size; + const uint64_t padding = (cacheLineSize_ - header_end % cacheLineSize_) % cacheLineSize_; + if (header_end > std::numeric_limits::max() - padding) { + out.fatal(CALL_INFO, -1, "Scratch payload address overflows\n"); + } + return header_end + padding; +} + +size_t DoorbellTestCPU::cacheChunkSize(uint64_t addr, size_t remaining) const +{ + return std::min(remaining, cacheLineSize_ - addr % cacheLineSize_); +} + +void DoorbellTestCPU::sendNextScratchChunk() +{ + if (scratch_bytes_remaining_ == 0) { + return; + } + size_t chunk = cacheChunkSize(scratchMemAddr_ + scratch_write_offset_, scratch_bytes_remaining_); + std::vector payload(packet_buffer_.begin() + scratch_write_offset_, + packet_buffer_.begin() + scratch_write_offset_ + chunk); + auto* req = new StandardMem::Write(scratchMemAddr_ + scratch_write_offset_, chunk, payload, false); + requests_[req->getID()] = std::make_pair("ScratchWrite", IfacePath::CACHE); + stat_cache_writes_->addData(1); + cache_link_->send(req); + scratch_write_offset_ += chunk; + scratch_bytes_remaining_ -= chunk; +} + +void DoorbellTestCPU::sendNextFlush() +{ + if (flushes_remaining_ == 0) { + sendDoorbell(); + return; + } + StandardMem::Addr line_addr = scratchMemAddr_ - scratchMemAddr_ % cacheLineSize_ + + (flushes_outstanding_ * cacheLineSize_); + auto* req = new StandardMem::FlushAddr(line_addr, cacheLineSize_, true, 1); + requests_[req->getID()] = std::make_pair("ScratchFlush", IfacePath::CACHE); + stat_flush_count_->addData(1); + cache_link_->send(req); + flushes_outstanding_++; +} + +void DoorbellTestCPU::sendDoorbell() +{ + std::vector payload; + uint64ToData(scratchMemAddr_, &payload); + auto* req = new StandardMem::Write(mmioAddr_, payload.size(), payload, false); + requests_[req->getID()] = std::make_pair("Doorbell", IfacePath::MMIO); + stat_mmio_writes_->addData(1); + mmio_link_->send(req); +} + +void DoorbellTestCPU::sendStartCudaRetRead() +{ + auto* req = new StandardMem::Read(mmioAddr_, sizeof(uint64_t)); + requests_[req->getID()] = std::make_pair("Start_CUDA_ret", IfacePath::MMIO); + mmio_link_->send(req); +} + +void DoorbellTestCPU::sendReadRetPacket(uint64_t ret_addr) +{ + beginCacheRead(ret_addr, sizeof(BalarCudaCallReturnPacket_t), "Read_CUDA_ret_packet"); +} + +void DoorbellTestCPU::beginCacheRead(uint64_t addr, size_t size, const std::string& type) +{ + if (size == 0 || addr > std::numeric_limits::max() - (size - 1)) { + out.fatal(CALL_INFO, -1, "Invalid %s range at 0x%lx with size %zu\n", type.c_str(), addr, size); + } + cache_read_addr_ = addr; + cache_read_offset_ = 0; + cache_read_type_ = type; + cache_read_buffer_.assign(size, 0); + sendNextCacheRead(); +} + +void DoorbellTestCPU::sendNextCacheRead() +{ + const uint64_t addr = cache_read_addr_ + cache_read_offset_; + const size_t chunk = cacheChunkSize(addr, cache_read_buffer_.size() - cache_read_offset_); + auto* req = new StandardMem::Read(addr, chunk); + requests_[req->getID()] = std::make_pair(cache_read_type_, IfacePath::CACHE); + cache_link_->send(req); +} + +void DoorbellTestCPU::onCacheWriteReq(StandardMem::Write* write) +{ + if (!write->posted) { + cache_link_->send(write->makeResponse()); + } + delete write; +} + +void DoorbellTestCPU::onCacheWriteResp(StandardMem::WriteResp* resp) +{ + auto it = requests_.find(resp->getID()); + if (it == requests_.end()) { + out.fatal(CALL_INFO, -1, "Unknown cache WriteResp %ld\n", resp->getID()); + } + if (resp->getFail()) { + out.fatal(CALL_INFO, -1, "Scratch write failed\n"); + } + if (it->second.first == "ScratchWrite") { + if (scratch_bytes_remaining_ > 0) { + sendNextScratchChunk(); + } else { + size_t packet_size = packet_buffer_.size(); + flushes_remaining_ = (scratchMemAddr_ % cacheLineSize_ + packet_size + cacheLineSize_ - 1) / cacheLineSize_; + flushes_outstanding_ = 0; + sendNextFlush(); + } + } else { + out.fatal(CALL_INFO, -1, "Unexpected cache WriteResp type %s\n", it->second.first.c_str()); + } + requests_.erase(it); + delete resp; +} + +void DoorbellTestCPU::onCacheFlushResp(StandardMem::FlushResp* resp) +{ + auto it = requests_.find(resp->getID()); + if (it == requests_.end()) { + out.fatal(CALL_INFO, -1, "Unknown cache FlushResp %ld\n", resp->getID()); + } + if (resp->getFail()) { + out.fatal(CALL_INFO, -1, "Scratch flush failed\n"); + } + if (flushes_remaining_ > 0) { + flushes_remaining_--; + } + if (flushes_remaining_ > 0) { + sendNextFlush(); + } else { + sendDoorbell(); + } + requests_.erase(it); + delete resp; +} + +void DoorbellTestCPU::onCacheReadResp(StandardMem::ReadResp* resp) +{ + auto it = requests_.find(resp->getID()); + if (it == requests_.end()) { + out.fatal(CALL_INFO, -1, "Unknown cache ReadResp %ld\n", resp->getID()); + } + if (it->second.first != cache_read_type_) { + out.fatal(CALL_INFO, -1, "Unexpected cache ReadResp %s\n", it->second.first.c_str()); + } + const uint64_t addr = cache_read_addr_ + cache_read_offset_; + const size_t chunk = cacheChunkSize(addr, cache_read_buffer_.size() - cache_read_offset_); + if (resp->getFail() || resp->pAddr != addr || resp->data.size() != chunk) { + out.fatal(CALL_INFO, -1, "Invalid %s response at 0x%lx: expected %zu bytes, received %zu\n", + cache_read_type_.c_str(), addr, chunk, resp->data.size()); + } + std::copy(resp->data.begin(), resp->data.end(), cache_read_buffer_.begin() + cache_read_offset_); + cache_read_offset_ += chunk; + requests_.erase(it); + delete resp; + + if (cache_read_offset_ < cache_read_buffer_.size()) { + sendNextCacheRead(); + return; + } + if (cache_read_type_ == "Read_CUDA_ret_packet") { + memcpy(&return_packet_, cache_read_buffer_.data(), sizeof(return_packet_)); + if (return_packet_.cuda_call_id != active_packet_.cuda_call_id || return_packet_.cuda_error != cudaSuccess) { + out.fatal(CALL_INFO, -1, "CUDA call %s returned type %s with error %d\n", + CudaAPIEnumToString(active_packet_.cuda_call_id), CudaAPIEnumToString(return_packet_.cuda_call_id), + return_packet_.cuda_error); + } + if (active_packet_.cuda_call_id == CUDA_MEMCPY && active_packet_.isSSTmem && + active_packet_.cuda_memcpy.kind == cudaMemcpyDeviceToHost && active_packet_.cuda_memcpy.count > 0) { + // Validate what the CPU can actually read after DMA, not the + // simulator-side buffer used as the source of the DMA write. + beginCacheRead(active_packet_.cuda_memcpy.dst, active_packet_.cuda_memcpy.count, "Read_D2H_data"); + return; + } + } + finishCudaCall(); +} + +void DoorbellTestCPU::onMmioWriteReq(StandardMem::Write* write) +{ + if (!write->posted) { + mmio_link_->send(write->makeResponse()); + } + delete write; +} + +void DoorbellTestCPU::onMmioWriteResp(StandardMem::WriteResp* resp) +{ + auto it = requests_.find(resp->getID()); + if (it == requests_.end()) { + out.fatal(CALL_INFO, -1, "Unknown mmio WriteResp %ld\n", resp->getID()); + } + if (it->second.first == "Doorbell") { + sendStartCudaRetRead(); + } else { + out.fatal(CALL_INFO, -1, "Unexpected mmio WriteResp %s\n", it->second.first.c_str()); + } + requests_.erase(it); + delete resp; +} + +void DoorbellTestCPU::onMmioReadResp(StandardMem::ReadResp* resp) +{ + auto it = requests_.find(resp->getID()); + if (it == requests_.end()) { + out.fatal(CALL_INFO, -1, "Unknown mmio ReadResp %ld\n", resp->getID()); + } + if (it->second.first == "Start_CUDA_ret") { + uint64_t ret_addr = dataToUInt64(&(resp->data)); + sendReadRetPacket(ret_addr); + } else { + out.fatal(CALL_INFO, -1, "Unexpected mmio ReadResp %s\n", it->second.first.c_str()); + } + requests_.erase(it); + delete resp; +} + +void DoorbellTestCPU::completeCudaCall(const BalarCudaCallReturnPacket_t* ret_pack) +{ + stat_cuda_calls_completed_->addData(1); + if (ret_pack->cuda_call_id == CUDA_REG_FAT_BINARY && trace_parser_) { + trace_parser_->setFatbinHandle(ret_pack->fat_cubin_handle); + } else if (ret_pack->cuda_call_id == CUDA_MALLOC) { + out.verbose(_INFO_, "cudaMalloc -> 0x%lx\n", ret_pack->cudamalloc.malloc_addr); + if (ret_pack->cudamalloc.devptr_addr) { + *(CUdeviceptr*)ret_pack->cudamalloc.devptr_addr = ret_pack->cudamalloc.malloc_addr; + } + } else if (ret_pack->cuda_call_id == CUDA_MEMCPY && ret_pack->cudamemcpy.kind == cudaMemcpyDeviceToHost) { + size_t tot = active_packet_.cuda_memcpy.count; + size_t correct = 0; + const volatile uint8_t* sim_ptr = active_packet_.isSSTmem ? + cache_read_buffer_.data() : ret_pack->cudamemcpy.sim_data; + const volatile uint8_t* real_ptr = (const volatile uint8_t*)active_packet_.cuda_memcpy.payload; + if (sim_ptr && real_ptr) { + for (size_t i = 0; i < tot; i++) { + if (real_ptr[i] == sim_ptr[i]) { + correct++; + } + } + stat_total_memD2H_bytes_->addData(tot); + stat_correct_memD2H_bytes_->addData(correct); + stat_correct_memD2H_ratio_->addData(tot ? (double)correct / (double)tot : 1.0); + + // Optional: dump the GPU result (and reference) so a host tool can + // do a tolerance comparison for non-bit-exact (e.g. tone) inputs. + if (enable_memcpy_dump_) { + char buf[200]; + snprintf(buf, sizeof(buf), "cudamemcpyD2H-sim-%p-%p-size-%zu.data", + (void*)sim_ptr, (void*)real_ptr, tot); + std::ofstream simDump(buf, std::ios::out | std::ios::binary); + if (!simDump.is_open()) { + out.fatal(CALL_INFO, -1, "Cannot open '%s' for D2H dump\n", buf); + } + simDump.write((const char*)sim_ptr, tot); + + snprintf(buf, sizeof(buf), "cudamemcpyD2H-real-%p-%p-size-%zu.data", + (void*)sim_ptr, (void*)real_ptr, tot); + std::ofstream realDump(buf, std::ios::out | std::ios::binary); + if (!realDump.is_open()) { + out.fatal(CALL_INFO, -1, "Cannot open '%s' for D2H dump\n", buf); + } + realDump.write((const char*)real_ptr, tot); + } + } + } +} + +void DoorbellTestCPU::finishCudaCall() +{ + completeCudaCall(&return_packet_); + cache_read_buffer_.clear(); + packet_issue_active_ = false; + ++calls_completed_; + tryFinishSimulation(); +} + +void DoorbellTestCPU::tryFinishSimulation() +{ + if (mode_ == DriverMode::DOORBELL_ONLY) { + if (!packet_issue_active_ && requests_.empty() && calls_completed_ >= 1) { + out.verbose(CALL_INFO, 1, 0, "DoorbellTestCPU: Test Completed Successfuly\n"); + primaryComponentOKToEndSim(); + } + return; + } + if (!packet_issue_active_ && requests_.empty() && trace_parser_) { + if (!trace_parser_->hasNextPacket()) { + out.verbose(CALL_INFO, 1, 0, "DoorbellTestCPU: Test Completed Successfuly\n"); + primaryComponentOKToEndSim(); + } + } +} + +bool DoorbellTestCPU::clockTic(Cycle_t) +{ + if (mode_ == DriverMode::DOORBELL_ONLY) { + // Issue the initial fatbin exactly once, the first time the CPU is idle. + if (!issued_initial_packet_ && requests_.empty() && !packet_issue_active_) { + BalarCudaCallPacket_t fatbin{}; + fatbin.cuda_call_id = CUDA_REG_FAT_BINARY; + fatbin.isSSTmem = false; + strncpy(fatbin.register_fatbin.file_name, cuda_executable_.c_str(), BALAR_CUDA_MAX_FILE_NAME - 1); + fatbin.register_fatbin.file_name[BALAR_CUDA_MAX_FILE_NAME - 1] = '\0'; + issued_initial_packet_ = true; + beginPacketIssue(fatbin); + } + return false; + } + + if (!packet_issue_active_ && requests_.empty() && trace_parser_) { + BalarCudaCallPacket_t pack{}; + if (trace_parser_->getNextPacket(pack)) { + beginPacketIssue(pack); + } else { + out.verbose(CALL_INFO, 1, 0, "DoorbellTestCPU: Test Completed Successfuly\n"); + primaryComponentOKToEndSim(); + return true; + } + } + + tryFinishSimulation(); + return false; +} diff --git a/src/sst/elements/balar/testcpu/doorbellTestCPU.h b/src/sst/elements/balar/testcpu/doorbellTestCPU.h new file mode 100644 index 0000000000..a6c8d9dfaf --- /dev/null +++ b/src/sst/elements/balar/testcpu/doorbellTestCPU.h @@ -0,0 +1,169 @@ +// Copyright 2009-2026 NTESS. Under the terms +// of Contract DE-NA0003525 with NTESS, the U.S. +// Government retains certain rights in this software. +// +// Copyright (c) 2009-2026, NTESS +// All rights reserved. +// +// This file is part of the SST software package. For license +// information, see the LICENSE file in the top level directory of the +// distribution. + +#ifndef BALAR_DOORBELL_TEST_CPU_H +#define BALAR_DOORBELL_TEST_CPU_H + +#ifndef __STDC_FORMAT_MACROS +#define __STDC_FORMAT_MACROS +#endif +#include + +#include +#include +#include +#include "balar_packet.h" + +#include "builtin_types.h" +#include "cuda.h" + +#include +#include +#include +#include +#include + +namespace SST { +namespace BalarComponent { + +class DoorbellTestCPU : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT( + DoorbellTestCPU, + "balar", + "DoorbellTestCPU", + SST_ELI_ELEMENT_VERSION(1, 0, 0), + "Doorbell-style test CPU: cache_link for scratch+flush, mmio_link for GPU doorbell", + COMPONENT_CATEGORY_PROCESSOR) + + SST_ELI_DOCUMENT_PARAMS( + { "verbose", "(uint) Verbosity", "1" }, + { "clock", "(UnitAlgebra/string) Clock frequency", "1GHz" }, + { "mmio_addr", "(uint) BalarMMIO base address (doorbell)", "0" }, + { "scratch_mem_addr", "(uint) Scratch region for CUDA call packets", "0" }, + { "cache_line_size", "(uint) Cache line size for scratch writes and flush", "64" }, + { "mode", "(string) trace or doorbell_only", "trace" }, + { "trace_file", "(string) CUDA API trace file (trace mode)", "cuda_calls.trace" }, + { "cuda_executable", "(string) CUDA binary for fatbin registration", "" }, + { "enable_memcpy_dump", "(bool) Dump D2H memcpy verification files", "false" }) + + SST_ELI_DOCUMENT_STATISTICS( + { "cache_writes", "Scratch packet writes via cache_link", "count", 1 }, + { "mmio_writes", "Doorbell and MMIO writes via mmio_link", "count", 1 }, + { "flush_count", "FlushAddr(inv) issued before doorbell", "count", 1 }, + { "cuda_calls_completed", "Completed CUDA API round-trips", "count", 1 }, + { "total_memD2H_bytes", "Total D2H memcpy bytes checked", "count", 1 }, + { "correct_memD2H_bytes", "Correct D2H memcpy bytes", "count", 1 }, + { "correct_memD2H_ratio", "D2H byte correctness ratio", "count", 1 }) + + SST_ELI_DOCUMENT_SUBCOMPONENT_SLOTS( + { "cache_link", "Cache path for scratch writes and return packet reads", "SST::Interfaces::StandardMem" }, + { "mmio_link", "MMIO path for doorbell and status reads", "SST::Interfaces::StandardMem" }) + + DoorbellTestCPU(SST::ComponentId_t id, SST::Params& params); + ~DoorbellTestCPU() override; + void init(unsigned int phase) override; + void setup() override; + void finish() override; + void emergencyShutdown() override; + +private: + enum class DriverMode { TRACE, DOORBELL_ONLY }; + + enum class IfacePath { CACHE, MMIO }; + + void handleCacheEvent(Interfaces::StandardMem::Request* req); + void handleMmioEvent(Interfaces::StandardMem::Request* req); + bool clockTic(SST::Cycle_t cycle); + + void beginPacketIssue(const BalarCudaCallPacket_t& pack); + void sendNextScratchChunk(); + void sendNextFlush(); + void sendDoorbell(); + void sendStartCudaRetRead(); + void sendReadRetPacket(uint64_t ret_addr); + void beginCacheRead(uint64_t addr, size_t size, const std::string& type); + void sendNextCacheRead(); + uint64_t scratchDataAddr() const; + size_t cacheChunkSize(uint64_t addr, size_t remaining) const; + + void onCacheWriteResp(Interfaces::StandardMem::WriteResp* resp); + void onCacheWriteReq(Interfaces::StandardMem::Write* write); + void onCacheFlushResp(Interfaces::StandardMem::FlushResp* resp); + void onCacheReadResp(Interfaces::StandardMem::ReadResp* resp); + void onMmioWriteResp(Interfaces::StandardMem::WriteResp* resp); + void onMmioWriteReq(Interfaces::StandardMem::Write* write); + void onMmioReadResp(Interfaces::StandardMem::ReadResp* resp); + + void completeCudaCall(const BalarCudaCallReturnPacket_t* ret_pack); + void finishCudaCall(); + void tryFinishSimulation(); + + static void uint64ToData(uint64_t num, std::vector* data); + static uint64_t dataToUInt64(std::vector* data); + + Output out; + DriverMode mode_; + uint64_t scratchMemAddr_; + uint64_t mmioAddr_; + uint64_t cacheLineSize_; + bool enable_memcpy_dump_; + std::string cuda_executable_; + // Gates the one-shot initial fatbin issue in DOORBELL_ONLY mode so the + // trigger doesn't depend on a magic clock-tick number. + bool issued_initial_packet_; + // NOTE: redundant with stat_cuda_calls_completed_->getCollectionCount(); + // kept as a plain counter to avoid coupling control flow to the stats subsystem. + uint64_t calls_completed_; + + Interfaces::StandardMem* cache_link_; + Interfaces::StandardMem* mmio_link_; + + TimeConverter clockTC_; + Clock::HandlerBase* clock_handler_; + + std::map> requests_; + + std::vector packet_buffer_; + size_t scratch_bytes_remaining_; + size_t scratch_write_offset_; + size_t flushes_remaining_; + size_t flushes_outstanding_; + bool packet_issue_active_; + std::vector scratch_append_payload_; + BalarCudaCallPacket_t active_packet_{}; + BalarCudaCallReturnPacket_t return_packet_{}; + uint64_t cache_read_addr_; + size_t cache_read_offset_; + std::string cache_read_type_; + std::vector cache_read_buffer_; + + class CudaAPITraceParser; + CudaAPITraceParser* trace_parser_; + + class CacheHandlers; + class MmioHandlers; + CacheHandlers* cache_handlers_; + MmioHandlers* mmio_handlers_; + + Statistic* stat_cache_writes_; + Statistic* stat_mmio_writes_; + Statistic* stat_flush_count_; + Statistic* stat_cuda_calls_completed_; + Statistic* stat_total_memD2H_bytes_; + Statistic* stat_correct_memD2H_bytes_; + Statistic* stat_correct_memD2H_ratio_; +}; + +} // namespace BalarComponent +} // namespace SST + +#endif // BALAR_DOORBELL_TEST_CPU_H diff --git a/src/sst/elements/balar/tests/balar_test_topology.py b/src/sst/elements/balar/tests/balar_test_topology.py index c7a3b38ada..9e9c2c9cc7 100644 --- a/src/sst/elements/balar/tests/balar_test_topology.py +++ b/src/sst/elements/balar/tests/balar_test_topology.py @@ -1,5 +1,5 @@ """ -Shared SST topology builder for Balar contract tests. +Shared SST topology builders for Balar and Doorbell-pattern contract tests. """ import sst @@ -100,3 +100,145 @@ def build_testcpu_router(balar_builder, cfg_file, balar_verbosity=0, dma_verbosi sst.Link("link_mem").connect((mem_nic, "port", "1000ps"), (chiprtr, "port4", "1000ps")) return cpu, balar_mmio_addr + + +def build_doorbell_topology(balar_builder, cfg_file, mode="trace", balar_verbosity=0, dma_verbosity=0): + """ + Doorbell-pattern: DoorbellTestCPU cache_link -> L1 -> coherent memory fabric, + and mmio_link -> routed MMIO fabric for balarMMIO doorbells. + """ + # NOTE: hard-coded high MMIO aperture so wide scratch packets cannot overflow + # into the MMIO region. Could derive from max packet size + alignment instead. + balar_mmio_addr = 0x10000000 + dma_mmio_addr = balar_mmio_addr + 1024 + _, balar_mmio_iface = balar_builder.build( + cfg_file, balar_mmio_addr, dma_mmio_addr, verbosity=balar_verbosity + ) + + dma = sst.Component("dmaEngine", "balar.dmaEngine") + dma.addParams({ + "verbose": dma_verbosity, + "clock": CLOCK, + "mmio_addr": dma_mmio_addr, + "mmio_size": 512, + }) + dma_mmio_if = dma.setSubComponent("mmio_iface", "memHierarchy.standardInterface") + dma_mem_if = dma.setSubComponent("mem_iface", "memHierarchy.standardInterface") + + l1 = sst.Component("l1", "memHierarchy.Cache") + l1.addParams({ + "cache_frequency": CLOCK, + "cache_size": "32KB", + "associativity": 4, + "access_latency_cycles": 2, + "L1": 1, + "coherence_protocol": "mesi", + "replacement_policy": "lru", + "cache_line_size": 64, + "addr_range_start": 0, + "addr_range_end": balar_mmio_addr - 1, + }) + l1_cpu = l1.setSubComponent("highlink", "memHierarchy.MemLink") + l1_nic = l1.setSubComponent("lowlink", "memHierarchy.MemNIC") + l1_nic.addParams({ + "group": CORE_GROUP, + "destinations": CORE_DST, + "network_bw": NETWORK_BW, + }) + + cpu = sst.Component("cpu", "balar.DoorbellTestCPU") + cpu.addParams({ + "clock": CLOCK, + "verbose": balar_verbosity, + "scratch_mem_addr": 0, + "mmio_addr": balar_mmio_addr, + "cache_line_size": 64, + "mode": mode, + "enable_memcpy_dump": False, + }) + cache_if = cpu.setSubComponent("cache_link", "memHierarchy.standardInterface") + cache_if.addParams(DEBUG_PARAMS) + mmio_if = cpu.setSubComponent("mmio_link", "memHierarchy.standardInterface") + mmio_if.addParams(DEBUG_PARAMS) + cpu_mmio_nic = mmio_if.setSubComponent("lowlink", "memHierarchy.MemNIC") + cpu_mmio_nic.addParams({ + "group": CORE_GROUP, + "destinations": CORE_DST, + "network_bw": NETWORK_BW, + }) + + balar_mmio_nic = balar_mmio_iface.setSubComponent("lowlink", "memHierarchy.MemNIC") + balar_mmio_nic.addParams({ + "group": MMIO_GROUP, + "sources": MMIO_SRC, + "destinations": MMIO_DST, + "network_bw": NETWORK_BW, + }) + + dma_mem_nic = dma_mem_if.setSubComponent("lowlink", "memHierarchy.MemNIC") + dma_mem_nic.addParams({ + "group": CORE_GROUP, + "destinations": CORE_DST, + "network_bw": NETWORK_BW, + }) + + dma_mmio_nic = dma_mmio_if.setSubComponent("lowlink", "memHierarchy.MemNIC") + dma_mmio_nic.addParams({ + "group": MEMORY_GROUP, + "sources": MEMORY_SRC, + "network_bw": NETWORK_BW, + }) + + chiprtr = sst.Component("doorbell_chiprtr", "merlin.hr_router") + chiprtr.addParams({ + "xbar_bw": "1GB/s", + "id": "0", + "input_buf_size": "1KB", + "num_ports": "6", + "flit_size": "72B", + "output_buf_size": "1KB", + "link_bw": "1GB/s", + "topology": "merlin.singlerouter", + }) + chiprtr.setSubComponent("topology", "merlin.singlerouter") + + memctrl = sst.Component("memory", "memHierarchy.MemController") + memctrl.addParams({ + "clock": "1GHz", + "addr_range_start": 0, + "addr_range_end": balar_mmio_addr - 1, + }) + mem_be = memctrl.setSubComponent("backend", "memHierarchy.simpleMem") + mem_be.addParams({ + "access_time": "100 ns", + "mem_size": "512MiB", + }) + mem_hi = memctrl.setSubComponent("highlink", "memHierarchy.MemLink") + + directory = sst.Component("directory", "memHierarchy.DirectoryController") + directory.addParams({ + "clock": "1GHz", + "coherence_protocol": "MESI", + "cache_line_size": 64, + "entry_cache_size": 32768, + "mshr_num_entries": 16, + "addr_range_start": 0, + "addr_range_end": balar_mmio_addr - 1, + }) + dir_nic = directory.setSubComponent("highlink", "memHierarchy.MemNIC") + dir_nic.addParams({ + "group": MEMORY_GROUP, + "sources": MEMORY_SRC, + "network_bw": NETWORK_BW, + }) + + sst.Link("cpu_l1").connect((cache_if, "port", "1ns"), (l1_cpu, "port", "1ns")) + sst.Link("mem_bus").connect((mem_hi, "port", "1ns"), (directory, "lowlink", "1ns")) + sst.Link("doorbell_l1_rtr").connect((l1_nic, "port", "1ns"), (chiprtr, "port0", "1ns")) + sst.Link("doorbell_cpu_mmio_rtr").connect((cpu_mmio_nic, "port", "1ns"), (chiprtr, "port1", "1ns")) + sst.Link("doorbell_balar_mmio_rtr").connect((balar_mmio_nic, "port", "1ns"), (chiprtr, "port2", "1ns")) + sst.Link("doorbell_dma_mem_rtr").connect((dma_mem_nic, "port", "1ns"), (chiprtr, "port3", "1ns")) + sst.Link("doorbell_dma_mmio_rtr").connect((dma_mmio_nic, "port", "1ns"), (chiprtr, "port4", "1ns")) + sst.Link("doorbell_dir_rtr").connect((dir_nic, "port", "1ns"), (chiprtr, "port5", "1ns")) + + return cpu, balar_mmio_addr diff --git a/src/sst/elements/balar/tests/testBalar_testsuite_util.py b/src/sst/elements/balar/tests/testBalar_testsuite_util.py index f448016a1d..efbb091266 100644 --- a/src/sst/elements/balar/tests/testBalar_testsuite_util.py +++ b/src/sst/elements/balar/tests/testBalar_testsuite_util.py @@ -275,6 +275,66 @@ def _assert_min_d2h_ratio(self, statsfile, testcase, test_kind, min_d2h_ratio): "{0} {1}: {2} Min.f64 {3} < {4}".format( test_kind, testcase, stat_name, min_ratio, min_d2h_ratio)) + def doorbell_contract_testcpu_template( + self, testcase, sdl_name, trace_name, testtimeout=600, min_flush_count=0, + min_cuda_calls_completed=0, min_d2h_bytes=0, min_d2h_ratio=0.0): + """Doorbell-pattern contract test: DoorbellTestCPU with cache_link flush before mmio doorbell.""" + missing_nvcc_path = os.getenv("NVCC_PATH") == None + self.assertFalse(missing_nvcc_path, "doorbell contract test: Requires NVCC_PATH.") + + test_path = self.get_testsuite_dir() + outdir = self.get_test_output_run_dir() + testDataFileName = "test_gpgpu_{0}".format(testcase) + outfile = "{0}/{1}.out".format(outdir, testDataFileName) + errfile = "{0}/{1}.err".format(outdir, testDataFileName) + statsfile = "{0}/{1}.stats_out".format(outdir, testDataFileName) + sdlfile = "{0}/{1}".format(test_path, sdl_name) + vecAddBinary = "{0}/balar_trace/vectorAdd".format(self.testbalarDir) + gpuMemCfgfile = "{0}/gpu-v100-mem.cfg".format(self.testbalarDir) + if trace_name: + trace_path = "{0}/traces/{1}".format(self.testbalarDir, trace_name) + otherargs = '--model-options=\"-c {0} -s {1} -x {2} -t {3} -v 1"'.format( + gpuMemCfgfile, statsfile, vecAddBinary, trace_path) + else: + otherargs = '--model-options=\"-c {0} -s {1} -x {2} -v 1"'.format( + gpuMemCfgfile, statsfile, vecAddBinary) + + cmd = self.run_sst(sdlfile, outfile, errfile, set_cwd=self.testbalarDir, + other_args=otherargs, timeout_sec=testtimeout) + log_debug("cmd = {0}".format(cmd)) + + self.assertTrue(os_test_file(statsfile, "-e"), + "doorbell contract {0}: missing stats file {1}".format(testcase, statsfile)) + with open(outfile, "r", errors="replace") as fh: + out_text = fh.read() + self.assertIn("Test Completed Successfuly", out_text, + "doorbell contract {0}: DoorbellTestCPU did not report success".format(testcase)) + with open(errfile, "r", errors="replace") as fh: + err_text = fh.read() + self.assertNotIn("FATAL", err_text, + "doorbell contract {0}: simulation fatal in stderr".format(testcase)) + + if min_cuda_calls_completed > 0: + calls_completed = self._stat_sum_u64(statsfile, "cuda_calls_completed") + self.assertGreaterEqual( + calls_completed, min_cuda_calls_completed, + "doorbell contract {0}: cuda_calls_completed {1} < {2}".format( + testcase, calls_completed, min_cuda_calls_completed)) + if min_d2h_bytes > 0: + d2h_bytes = self._stat_sum_u64(statsfile, "total_memD2H_bytes") + self.assertGreaterEqual( + d2h_bytes, min_d2h_bytes, + "doorbell contract {0}: total_memD2H_bytes {1} < {2}".format( + testcase, d2h_bytes, min_d2h_bytes)) + if min_d2h_ratio > 0.0: + self._assert_min_d2h_ratio(statsfile, testcase, "doorbell contract", min_d2h_ratio) + if min_flush_count > 0: + flush_total = self._stat_sum_u64(statsfile, "flush_count") + self.assertGreaterEqual( + flush_total, min_flush_count, + "doorbell contract {0}: flush_count {1} < {2}".format( + testcase, flush_total, min_flush_count)) + def balar_vanadis_template(self, testcase, testtimeout=400): """Balar testcase template with vanadis with explicit CUDA api calls @@ -522,6 +582,9 @@ def _setupbalarTestFiles(self): "testBalar-doorbell.py", "testBalar-malloc-free.py", "testBalar-wide-packet.py", + "testDoorbellCPU-doorbell.py", + "testDoorbellCPU-malloc-free.py", + "testDoorbellCPU-wide-packet.py", ): os_symlink_file(test_path, self.testbalarDir, sdl) # Copy the shared packet definition files from balar src diff --git a/src/sst/elements/balar/tests/testDoorbellCPU-doorbell.py b/src/sst/elements/balar/tests/testDoorbellCPU-doorbell.py new file mode 100644 index 0000000000..0148b9b170 --- /dev/null +++ b/src/sst/elements/balar/tests/testDoorbellCPU-doorbell.py @@ -0,0 +1,18 @@ +# Doorbell-pattern doorbell: fatbin register via scratch+flush+mmio doorbell (doorbell_only mode). +import sst +from utils import * +import balarBlock +import balar_test_topology + +args = vars(balarTestParser.parse_args()) +balar_builder = balarBlock.Builder(args) +cpu, _ = balar_test_topology.build_doorbell_topology( + balar_builder, args["config"], mode="doorbell_only", + balar_verbosity=args["balar_verbosity"], dma_verbosity=args["dma_verbosity"]) +cpu.addParams({ + "cuda_executable": args["cuda_binary"], +}) + +sst.setStatisticLoadLevel(args["statlevel"]) +sst.enableAllStatisticsForAllComponents({"type": "sst.AccumulatorStatistic"}) +sst.setStatisticOutput("sst.statOutputTXT", {"filepath": args["statfile"]}) diff --git a/src/sst/elements/balar/tests/testDoorbellCPU-malloc-free.py b/src/sst/elements/balar/tests/testDoorbellCPU-malloc-free.py new file mode 100644 index 0000000000..c3ac798b6a --- /dev/null +++ b/src/sst/elements/balar/tests/testDoorbellCPU-malloc-free.py @@ -0,0 +1,19 @@ +# Doorbell-pattern malloc+free with L1, flush-before-doorbell, shared DRAM bus. +import sst +from utils import * +import balarBlock +import balar_test_topology + +args = vars(balarTestParser.parse_args()) +balar_builder = balarBlock.Builder(args) +cpu, _ = balar_test_topology.build_doorbell_topology( + balar_builder, args["config"], mode="trace", + balar_verbosity=args["balar_verbosity"], dma_verbosity=args["dma_verbosity"]) +cpu.addParams({ + "trace_file": args["trace"], + "cuda_executable": args["cuda_binary"], +}) + +sst.setStatisticLoadLevel(args["statlevel"]) +sst.enableAllStatisticsForAllComponents({"type": "sst.AccumulatorStatistic"}) +sst.setStatisticOutput("sst.statOutputTXT", {"filepath": args["statfile"]}) diff --git a/src/sst/elements/balar/tests/testDoorbellCPU-wide-packet.py b/src/sst/elements/balar/tests/testDoorbellCPU-wide-packet.py new file mode 100644 index 0000000000..0dd66f7732 --- /dev/null +++ b/src/sst/elements/balar/tests/testDoorbellCPU-wide-packet.py @@ -0,0 +1,19 @@ +# Doorbell-pattern wide scratch packet (4 KiB memcpy) with per-line flush before doorbell. +import sst +from utils import * +import balarBlock +import balar_test_topology + +args = vars(balarTestParser.parse_args()) +balar_builder = balarBlock.Builder(args) +cpu, _ = balar_test_topology.build_doorbell_topology( + balar_builder, args["config"], mode="trace", + balar_verbosity=args["balar_verbosity"], dma_verbosity=args["dma_verbosity"]) +cpu.addParams({ + "trace_file": args["trace"], + "cuda_executable": args["cuda_binary"], +}) + +sst.setStatisticLoadLevel(args["statlevel"]) +sst.enableAllStatisticsForAllComponents({"type": "sst.AccumulatorStatistic"}) +sst.setStatisticOutput("sst.statOutputTXT", {"filepath": args["statfile"]}) diff --git a/src/sst/elements/balar/tests/testsuite_default_balar.py b/src/sst/elements/balar/tests/testsuite_default_balar.py index 2dd3ada01d..efb18d31d4 100644 --- a/src/sst/elements/balar/tests/testsuite_default_balar.py +++ b/src/sst/elements/balar/tests/testsuite_default_balar.py @@ -41,6 +41,14 @@ def test_balar_sst_info_registers_components(self): self.skipTest("balar element not built (configure without --with-gpgpusim)") self.assertIn("balarMMIO", text, "sst-info balar should list balar.balarMMIO") + def test_balar_doorbell_testcpu_registered(self): + """sst-info balar lists DoorbellTestCPU when the element is built.""" + text, rc = self._sst_info_text() + if "UNABLE TO PROCESS LIBRARY" in text.upper() or rc != 0: + self.skipTest("balar element not built (configure without --with-gpgpusim)") + self.assertIn("DoorbellTestCPU", text, "sst-info balar should list balar.DoorbellTestCPU") + + class testcase_balar_simple(BalarTestCase): @BalarTestCase.balar_basic_unittest def test_balar_runvecadd_testcpu(self): @@ -62,6 +70,24 @@ def test_balar_contract_wide_packet(self): "wide_packet", "testBalar-wide-packet.py", "wide_memcpy_d2h.trace", testtimeout=60 * 20, min_d2h_ratio=1.0) + @BalarTestCase.balar_basic_unittest + def test_doorbellcpu_doorbell(self): + self.doorbell_contract_testcpu_template( + "doorbellcpu_doorbell", "testDoorbellCPU-doorbell.py", None, testtimeout=60 * 10) + + @BalarTestCase.balar_basic_unittest + def test_doorbellcpu_malloc_free(self): + self.doorbell_contract_testcpu_template( + "doorbellcpu_malloc_free", "testDoorbellCPU-malloc-free.py", "malloc_free.trace", + testtimeout=60 * 15, min_cuda_calls_completed=3) + + @BalarTestCase.balar_basic_unittest + def test_doorbellcpu_wide_packet(self): + self.doorbell_contract_testcpu_template( + "doorbellcpu_wide_packet", "testDoorbellCPU-wide-packet.py", "wide_memcpy_d2h.trace", + testtimeout=60 * 20, min_flush_count=64, min_cuda_calls_completed=5, + min_d2h_bytes=4096, min_d2h_ratio=1.0) + @unittest.skipIf( os.getenv("LLVM_INSTALL_PATH") is None or os.getenv("RISCV_TOOLCHAIN_INSTALL_PATH") is None, "Vanadis clang vecadd requires LLVM_INSTALL_PATH and RISCV_TOOLCHAIN_INSTALL_PATH")