From d4b42c3b7d3b2749278550c45adff0d95df27f3a Mon Sep 17 00:00:00 2001 From: nab880 Date: Fri, 10 Jul 2026 16:03:13 -0700 Subject: [PATCH 1/6] carcosa: add ECC framework and transport-neutral Hali --- src/sst/elements/carcosa/.gitignore | 9 + src/sst/elements/carcosa/Makefile.am | 114 +- .../carcosa/components/actionScorer.cc | 368 ++++ .../carcosa/components/actionScorer.h | 120 ++ .../carcosa/components/carcosaCPUBase.cc | 46 +- .../carcosa/components/carcosaCPUBase.h | 5 +- .../carcosa/components/carcosaMemCtrl.cc | 22 +- .../carcosa/components/carcosaMemCtrl.h | 6 +- .../components/criticalActionWatcher.cc | 375 ++++ .../components/criticalActionWatcher.h | 119 ++ .../elements/carcosa/components/eccGuard.cc | 1649 +++++++++++++++++ .../elements/carcosa/components/eccGuard.h | 405 ++++ .../carcosa/components/eccModelMath.h | 51 + .../elements/carcosa/components/eccPolicy.h | 212 +++ .../elements/carcosa/components/eccScheme.h | 195 ++ .../carcosa/components/faultInjEvent.h | 5 +- .../carcosa/components/faultInjManager.h | 2 +- .../carcosa/components/fourStateAgent.cc | 244 +++ .../carcosa/components/fourStateAgent.h | 107 ++ src/sst/elements/carcosa/components/hali.cc | 220 ++- src/sst/elements/carcosa/components/hali.h | 63 +- .../elements/carcosa/components/haliEvent.h | 16 +- .../carcosa/components/hyadesProtocol.h | 55 + .../carcosa/components/interceptionAgentAPI.h | 83 +- .../carcosa/components/pingPongAgent.cc | 11 +- .../carcosa/components/pingPongAgent.h | 13 +- .../components/pipelineStateRegistry.h | 199 ++ .../carcosa/components/pmDataRegistry.h | 10 +- .../elements/carcosa/components/vlaRegions.h | 115 ++ src/sst/elements/carcosa/configure.m4 | 18 +- .../carcosa/faultlogic/corruptMemFault.h | 14 +- .../carcosa/faultlogic/randomFlipFault.h | 5 +- .../carcosa/faultlogic/randomFlipMemHFault.h | 7 +- .../carcosa/faultlogic/stuckAtFault.h | 18 +- src/sst/elements/carcosa/hyades.h | 182 +- .../injectors/dropFlipFaultInjector.cc | 5 +- .../carcosa/injectors/faultInjectorBase.cc | 14 +- .../carcosa/injectors/faultInjectorBase.h | 20 +- .../carcosa/injectors/faultInjectorMemH.h | 6 +- .../injectors/randomDropFaultInjector.cc | 10 +- src/sst/elements/carcosa/tests/fourstate.c | 104 ++ src/sst/elements/carcosa/tests/pingpong.c | 11 +- ...cosaPingPong.py => testCarcosaPingPong.py} | 0 .../{testdynamicPM.py => testDynamicPM.py} | 0 .../carcosa/tests/testEccGuardJedecMix.py | 96 + .../carcosa/tests/testEccGuardRegionPolicy.py | 113 ++ .../carcosa/tests/testEccGuardResident.py | 98 + .../carcosa/tests/testEccGuardSmoke.py | 96 + .../carcosa/tests/testFourStateRegistry.py | 361 ++++ .../tests/testFourStateRegistryGated.py | 71 + ...{testhaliBacking.py => testHaliBacking.py} | 0 .../{testhaliMemH.py => testHaliMemH.py} | 0 .../tests/{testhaliPM.py => testHaliPM.py} | 0 ...estmanagerLogic.py => testManagerLogic.py} | 4 +- 54 files changed, 5795 insertions(+), 297 deletions(-) create mode 100644 src/sst/elements/carcosa/.gitignore create mode 100644 src/sst/elements/carcosa/components/actionScorer.cc create mode 100644 src/sst/elements/carcosa/components/actionScorer.h create mode 100644 src/sst/elements/carcosa/components/criticalActionWatcher.cc create mode 100644 src/sst/elements/carcosa/components/criticalActionWatcher.h create mode 100644 src/sst/elements/carcosa/components/eccGuard.cc create mode 100644 src/sst/elements/carcosa/components/eccGuard.h create mode 100644 src/sst/elements/carcosa/components/eccModelMath.h create mode 100644 src/sst/elements/carcosa/components/eccPolicy.h create mode 100644 src/sst/elements/carcosa/components/eccScheme.h create mode 100644 src/sst/elements/carcosa/components/fourStateAgent.cc create mode 100644 src/sst/elements/carcosa/components/fourStateAgent.h create mode 100644 src/sst/elements/carcosa/components/hyadesProtocol.h create mode 100644 src/sst/elements/carcosa/components/pipelineStateRegistry.h create mode 100644 src/sst/elements/carcosa/components/vlaRegions.h create mode 100644 src/sst/elements/carcosa/tests/fourstate.c rename src/sst/elements/carcosa/tests/{testcarcosaPingPong.py => testCarcosaPingPong.py} (100%) rename src/sst/elements/carcosa/tests/{testdynamicPM.py => testDynamicPM.py} (100%) create mode 100644 src/sst/elements/carcosa/tests/testEccGuardJedecMix.py create mode 100644 src/sst/elements/carcosa/tests/testEccGuardRegionPolicy.py create mode 100644 src/sst/elements/carcosa/tests/testEccGuardResident.py create mode 100644 src/sst/elements/carcosa/tests/testEccGuardSmoke.py create mode 100644 src/sst/elements/carcosa/tests/testFourStateRegistry.py create mode 100644 src/sst/elements/carcosa/tests/testFourStateRegistryGated.py rename src/sst/elements/carcosa/tests/{testhaliBacking.py => testHaliBacking.py} (100%) rename src/sst/elements/carcosa/tests/{testhaliMemH.py => testHaliMemH.py} (100%) rename src/sst/elements/carcosa/tests/{testhaliPM.py => testHaliPM.py} (100%) rename src/sst/elements/carcosa/tests/{testmanagerLogic.py => testManagerLogic.py} (99%) diff --git a/src/sst/elements/carcosa/.gitignore b/src/sst/elements/carcosa/.gitignore new file mode 100644 index 0000000000..5397488763 --- /dev/null +++ b/src/sst/elements/carcosa/.gitignore @@ -0,0 +1,9 @@ +tests/pingpong +tests/fourstate + +tests/*.log +tests/stdout-* +tests/stderr-* +tests/tmp/ + +*.bak diff --git a/src/sst/elements/carcosa/Makefile.am b/src/sst/elements/carcosa/Makefile.am index b96a6a3f4c..3dac048bf8 100644 --- a/src/sst/elements/carcosa/Makefile.am +++ b/src/sst/elements/carcosa/Makefile.am @@ -11,6 +11,15 @@ comp_LTLIBRARIES = libcarcosa.la libcarcosa_la_SOURCES = \ components/hali.cc \ components/hali.h \ + components/eccGuard.cc \ + components/eccGuard.h \ + components/eccModelMath.h \ + components/criticalActionWatcher.cc \ + components/criticalActionWatcher.h \ + components/eccScheme.h \ + components/eccPolicy.h \ + components/actionScorer.cc \ + components/actionScorer.h \ components/faultInjManager.cc \ components/faultInjManager.h \ injectors/faultInjectorBase.cc \ @@ -25,6 +34,8 @@ libcarcosa_la_SOURCES = \ injectors/randomFlipFaultInjector.h \ injectors/dropFlipFaultInjector.cc \ injectors/dropFlipFaultInjector.h \ + injectors/portModuleStateGate.cc \ + injectors/portModuleStateGate.h \ injectors/faultInjectorMemH.cc \ injectors/faultInjectorMemH.h \ faultlogic/faultBase.cc \ @@ -50,19 +61,55 @@ libcarcosa_la_SOURCES = \ components/faultInjEvent.h \ components/faultInjManagerAPI.h \ components/interceptionAgentAPI.h \ + components/hyadesProtocol.h \ + components/pipelineStateRegistry.h \ components/pingPongAgent.cc \ components/pingPongAgent.h \ + components/fourStateAgent.cc \ + components/fourStateAgent.h \ + components/framePipelineDriver.cc \ + components/framePipelineDriver.h \ + components/regressionTestComponents.cc \ + components/regressionTestComponents.h \ + components/vlaRegions.h \ components/carcosaMemCtrl.cc \ components/carcosaMemCtrl.h \ components/sensorComponent.cc \ - components/sensorComponent.h \ + components/sensorComponent.h \ components/sensorEvent.h \ - components/haliEvent.h + components/haliEvent.h \ + components/ringProtocol.h \ + components/carcosaHash.h \ + examples/SimplePipeline/simplePipelineExample.cc \ + examples/SimplePipeline/simplePipelineExample.h \ + examples/MmioControl/mmioControlExample.cc \ + examples/MmioControl/mmioControlExample.h + +# Optional balar ring bridge: compiled into libcarcosa only when GPGPU-Sim/balar +# is available (SST_CARCOSA_HAVE_BALAR, set by configure.m4 -> HAVE_BALAR_BRIDGE). +# Header-only dependency on balar's packet ABI, so only the CUDA include path is +# added here (no GPGPU-Sim libs). A checkout with no balar/ builds libcarcosa +# unchanged with the bridge compiled out. +if SST_CARCOSA_HAVE_BALAR +libcarcosa_la_SOURCES += \ + components/balarRingBridge.cc \ + components/balarRingBridge.h +# HAVE_BALAR_BRIDGE is passed on the command line (like balar's own -DHAVE_CUDA=1 +# in CUDA_CPPFLAGS) rather than via sst_element_config.h, which element sources do +# not include. CUDA_CPPFLAGS carries the CUDA include path for balar's packet headers. +AM_CPPFLAGS += $(CUDA_CPPFLAGS) -DHAVE_BALAR_BRIDGE=1 +endif EXTRA_DIST = \ hyades.h \ tests/pingpong.c \ - tests/testcarcosaPingPong.py \ + tests/fourstate.c \ + tests/testCarcosaPingPong.py \ + tests/testFourStateRegistry.py \ + tests/testFourStateRegistryGated.py \ + examples/SimplePipeline/tests/testSimplePipeline.py \ + examples/SimplePipeline/tests/testPortModuleStateGate.py \ + examples/MmioControl/tests/testMmioControl.py \ tests/testCorruptMemBasic.py \ tests/testCorruptMemDouble.py \ tests/testCorruptMemDoubleOverlap.py \ @@ -73,11 +120,39 @@ EXTRA_DIST = \ tests/testStuckAtOverlap.py \ tests/testStuckAtSameByte.py \ tests/mhlib.py \ - tests/testdynamicPM.py \ - tests/testhaliBacking.py \ - tests/testhaliMemH.py \ - tests/testhaliPM.py \ - tests/testmanagerLogic.py + tests/testDynamicPM.py \ + tests/testEccGuardJedecMix.py \ + tests/testEccGuardRegionPolicy.py \ + tests/testEccGuardResident.py \ + tests/testEccGuardSmoke.py \ + tests/testEccModelDeterministic.py \ + tests/eccRuntimeCommon.py \ + tests/testEccRuntimeCorrectable.py \ + tests/testEccRuntimeDueDrop.py \ + tests/testEccRuntimeEscape.py \ + tests/testEccCampaignReentry.py \ + tests/testEccPoissonDistribution.py \ + tests/testEccJedecDistribution.py \ + tests/testEccResidentDistribution.py \ + tests/framePipelineCommon.py \ + tests/testFramePipelineClean.py \ + tests/testFramePipelineCorrupt.py \ + tests/testFramePipelineFallback.py \ + tests/testFramePipelineMissingGolden.py \ + tests/testStateGateRegionNoOverlap.py \ + tests/testStateGateRegionOverlap.py \ + tests/testActionScorerTaxonomy.py \ + tests/testActionScorerTokenFallback.py \ + tests/testActionScorerDropDivergence.py \ + tests/haliEdgeCommon.py \ + tests/testHaliPayloadlessGetX.py \ + tests/testHaliDoubleDeferred.py \ + tests/testHaliDeferredComplete.py \ + tests/testHaliPayloadGetX.py \ + tests/testHaliBacking.py \ + tests/testHaliMemH.py \ + tests/testHaliPM.py \ + tests/testManagerLogic.py sstdir = $(includedir)/sst/elements/carcosa nobase_sst_HEADERS = \ @@ -87,6 +162,7 @@ nobase_sst_HEADERS = \ injectors/randomDropFaultInjector.h \ injectors/randomFlipFaultInjector.h \ injectors/dropFlipFaultInjector.h \ + injectors/portModuleStateGate.h \ injectors/faultInjectorMemH.h \ faultlogic/faultBase.h \ faultlogic/stuckAtFault.h \ @@ -95,12 +171,26 @@ nobase_sst_HEADERS = \ faultlogic/randomFlipFault.h \ faultlogic/randomFlipMemHFault.h \ components/pmDataRegistry.h \ - components/faultInjManagerAPI.h \ - components/interceptionAgentAPI.h \ - components/pingPongAgent.h + components/eccScheme.h \ + components/eccPolicy.h \ + components/eccGuard.h \ + components/eccModelMath.h \ + components/criticalActionWatcher.h \ + components/actionScorer.h \ + components/faultInjManagerAPI.h \ + components/interceptionAgentAPI.h \ + components/ringProtocol.h \ + components/hyadesProtocol.h \ + components/carcosaHash.h \ + components/pipelineStateRegistry.h \ + components/pingPongAgent.h \ + components/fourStateAgent.h \ + components/haliEvent.h \ + examples/SimplePipeline/simplePipelineExample.h \ + examples/MmioControl/mmioControlExample.h \ + components/vlaRegions.h libcarcosa_la_LDFLAGS = -module -avoid-version -libcarcosa_la_LIBADD = AM_CPPFLAGS += $(HMC_FLAG) install-exec-hook: diff --git a/src/sst/elements/carcosa/components/actionScorer.cc b/src/sst/elements/carcosa/components/actionScorer.cc new file mode 100644 index 0000000000..a95933d8b7 --- /dev/null +++ b/src/sst/elements/carcosa/components/actionScorer.cc @@ -0,0 +1,368 @@ +// 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 "sst_config.h" +#include "sst/elements/carcosa/components/actionScorer.h" +#include "sst/elements/carcosa/components/pipelineStateRegistry.h" +#include +#include +#include +#include +#include +#include + +using namespace SST; +using namespace SST::Carcosa; + +ActionScorer::ActionScorer(ComponentId_t id, Params& params) + : Component(id) { + out_ = new Output("", 1, 0, Output::STDOUT); + state_key_ = params.find("state_key", ""); + golden_path_ = params.find("golden_log", ""); + emit_golden_ = params.find("emit_golden", false); + golden_required_ = params.find("golden_required", true); + verbose_ = params.find("verbose", false); + expect_frames_total_ = params.find("expect_frames_total", -1); + expect_frames_dropped_ = params.find("expect_frames_dropped", -1); + expect_frames_argmax_diff_ = params.find("expect_frames_argmax_diff", -1); + expect_frames_action_diff_ = params.find("expect_frames_action_diff", -1); + expect_frames_unsafe_ = params.find("expect_frames_unsafe", -1); + expect_frames_o1_ = params.find("expect_frames_o1", -1); + expect_frames_o2_ = params.find("expect_frames_o2", -1); + expect_frames_o3_ = params.find("expect_frames_o3", -1); + expect_frames_o4_ = params.find("expect_frames_o4", -1); + if (state_key_.empty()) { + out_->fatal(CALL_INFO, -1, + "ActionScorer '%s': state_key is required.\n", getName().c_str()); + } + stat_frames_total_ = registerStatistic("frames_total"); + stat_frames_dropped_ = registerStatistic("frames_dropped"); + stat_frames_argmax_diff_ = registerStatistic("frames_argmax_diff"); + stat_frames_action_diff_ = registerStatistic("frames_action_diff"); + stat_frames_unsafe_ = registerStatistic("frames_safety_violated"); + stat_frames_o1_ = registerStatistic("frames_outcome_O1"); + stat_frames_o2_ = registerStatistic("frames_outcome_O2"); + stat_frames_o3_ = registerStatistic("frames_outcome_O3"); + stat_frames_o4_ = registerStatistic("frames_outcome_O4"); +} + +const char* ActionScorer::outcomeClassLabel(int oclass) { + switch (oclass) { + case 1: return "O1"; + case 2: return "O2"; + case 3: return "O3"; + case 4: return "O4"; + default: return "O?"; + } +} + +ActionScorer::~ActionScorer() { + delete out_; +} + +void ActionScorer::loadGoldenLog() { + if (golden_loaded_) return; + golden_loaded_ = true; + if (golden_path_.empty()) return; + + std::ifstream f(golden_path_); + if (!f.is_open()) { + if (golden_required_) { + out_->fatal(CALL_INFO, -1, + "ActionScorer '%s': golden_log='%s' could not be opened and " + "golden_required=true. Refusing to proceed with a degraded " + "score that would report unsafe_action_rate=0 for every " + "frame regardless of BER. Either run the matching BER=0 " + "Phase 1 pass to emit the golden file, or set " + "golden_required=false to opt in to the legacy passthrough.\n", + getName().c_str(), golden_path_.c_str()); + } + out_->output("ActionScorer '%s': WARNING golden_log '%s' could not be " + "opened; golden_required=false, so every frame will " + "report argmax_changed=0. unsafe_action_rate will reflect " + "drop_rate only.\n", + getName().c_str(), golden_path_.c_str()); + return; + } + std::string line; + int line_no = 0; + while (std::getline(f, line)) { + ++line_no; + if (line.empty()) continue; + if (line[0] == '#') continue; + // Allow header line "pipeline_cycle,kernel_at_close,action_checksum". + if (line.find("pipeline_cycle") != std::string::npos) continue; + + std::stringstream ss(line); + std::string cell; + std::vector parts; + while (std::getline(ss, cell, ',')) parts.push_back(cell); + if (parts.size() < 3) continue; + + GoldenEntry e{}; + try { + e.pipelineCycle = std::stoi(parts[0]); + e.kernelAtClose = std::stoi(parts[1]); + e.checksum = std::stoull(parts[2]); + // Optional 4th column: decoded-action token (goldens emitted + // before the token metric existed have 3 columns). + if (parts.size() >= 4 && !parts[3].empty()) { + e.token = std::stoull(parts[3]); + e.hasToken = true; + } + } catch (...) { + out_->output("ActionScorer '%s': skipping malformed golden_log line %d: '%s'\n", + getName().c_str(), line_no, line.c_str()); + continue; + } + golden_.push_back(e); + } + if (verbose_) { + out_->output("ActionScorer '%s': loaded %zu golden entries from '%s'\n", + getName().c_str(), golden_.size(), golden_path_.c_str()); + } + if (golden_.empty() && golden_required_) { + out_->fatal(CALL_INFO, -1, + "ActionScorer '%s': golden_log='%s' opened but contained zero " + "valid entries (header-only or all rows malformed) and " + "golden_required=true. Refusing to score with an empty golden " + "table; re-run the BER=0 emit-golden pass.\n", + getName().c_str(), golden_path_.c_str()); + } +} + +void ActionScorer::setup() { + loadGoldenLog(); +} + +void ActionScorer::finish() { + const PipelineStateBase* s = + PipelineStateRegistry::get(state_key_); + if (!s) { + out_->output("ActionScorer '%s': no snapshot for key '%s'; nothing to score.\n", + getName().c_str(), state_key_.c_str()); + return; + } + + // Index golden by (pipelineCycle, kernelAtClose) so the lookup tolerates + // frames being scored out of order across multi-cycle runs. + std::map, GoldenEntry> golden_idx; + for (const auto& g : golden_) { + golden_idx[{g.pipelineCycle, g.kernelAtClose}] = g; + } + // Cycle-only fallback: faults can change which kernel a frame closes in + // (e.g. DUE drop_frame); only cycles with a single golden entry are + // unambiguous fallback targets for divergence scoring. + std::map golden_by_cycle; + std::map golden_cycle_count; + for (const auto& g : golden_) { + if (++golden_cycle_count[g.pipelineCycle] == 1) + golden_by_cycle[g.pipelineCycle] = g; + } + + out_->output("\n=== Action Scorer %s Per-Frame Trace ===\n", getName().c_str()); + out_->output("pipeline_cycle,kernel_at_close,kernel_name," + "attributing_kernel_id,attributing_kernel_name,dropped," + "escapes_in_frame,flips_in_frame,action_checksum," + "golden_checksum,argmax_changed,action_token,golden_token," + "action_changed,safety_violated,outcome_class," + "sim_time_ps\n"); + + uint64_t prev_escapes = 0; + uint64_t prev_flips = 0; + uint64_t total = 0; + uint64_t dropped = 0; + uint64_t argmax_diff = 0; + uint64_t action_diff = 0; + uint64_t unsafe = 0; + uint64_t unmatched = 0; + uint64_t o1 = 0, o2 = 0, o3 = 0, o4 = 0; + + std::ostringstream golden_emit; + if (emit_golden_) { + golden_emit << "pipeline_cycle,kernel_at_close,action_checksum,action_token\n"; + } + + for (const auto& fr : s->frames) { + uint64_t escapes_in = fr.cumulativeEscapes >= prev_escapes + ? fr.cumulativeEscapes - prev_escapes : 0; + uint64_t flips_in = fr.cumulativeFlips >= prev_flips + ? fr.cumulativeFlips - prev_flips : 0; + prev_escapes = fr.cumulativeEscapes; + prev_flips = fr.cumulativeFlips; + + uint64_t golden_cs = 0; + uint64_t golden_tok = 0; + bool has_golden = false; + bool has_golden_token = false; + auto it = golden_idx.find({fr.pipelineCycle, fr.kernelAtClose}); + if (it != golden_idx.end()) { + golden_cs = it->second.checksum; + golden_tok = it->second.token; + has_golden = true; + has_golden_token = it->second.hasToken; + } else { + auto cit = golden_by_cycle.find(fr.pipelineCycle); + if (cit != golden_by_cycle.end() + && golden_cycle_count[fr.pipelineCycle] == 1) { + golden_cs = cit->second.checksum; + golden_tok = cit->second.token; + has_golden = true; + has_golden_token = cit->second.hasToken; + } + } + if (!has_golden && !golden_.empty()) { + ++unmatched; + out_->output("ActionScorer '%s': WARNING frame (cycle=%d, " + "kernel_at_close=%d) has no golden entry; divergence " + "flags forced to 0 for this frame.\n", + getName().c_str(), fr.pipelineCycle, fr.kernelAtClose); + } + + bool argmax_changed = has_golden && (golden_cs != fr.actionChecksum); + // Decoded-action divergence: only meaningful when both sides carry a + // token (frame token 0 = workload never published one). Falls back + // to the checksum oracle otherwise, preserving legacy scoring. + bool token_avail = has_golden_token && fr.actionToken != 0; + bool action_changed = token_avail && (golden_tok != fr.actionToken); + bool divergence = token_avail ? action_changed : argmax_changed; + bool had_escape = escapes_in > 0; + // unsafe_action_rate is for silent corruption, not DUE drops. With + // tokens, checksum-only escapes are O4 (not unsafe); without tokens, + // fall back to the conservative escape-or-checksum rule. + bool safety_violated = fr.dropped || divergence || had_escape; + bool unsafe_action = divergence || (!token_avail && had_escape); + + int outcome_class = 1; + if (safety_violated) { + if (fr.dropped && !divergence) outcome_class = 2; + else if (divergence) outcome_class = 3; + else if (had_escape) outcome_class = 4; + else outcome_class = 2; + } + + const char* kname = fr.kernelAtCloseName.empty() + ? "UNKNOWN" + : fr.kernelAtCloseName.c_str(); + + // attributing_kernel: kernel with the most escapes this frame; + // falls back to kernelAtClose when none were recorded. + int attr_id = fr.attributingKernel; + std::string attr_name = fr.attributingKernelName; + if (attr_name.empty()) { + attr_name = fr.kernelAtCloseName; + if (attr_id < 0) attr_id = fr.kernelAtClose; + } + const char* aname = attr_name.empty() ? "UNKNOWN" : attr_name.c_str(); + + out_->output("%d,%d,%s,%d,%s,%d,%" PRIu64 ",%" PRIu64 + ",%" PRIu64 ",%" PRIu64 ",%d,%" PRIu64 ",%" PRIu64 + ",%d,%d,%s,%" PRIu64 "\n", + fr.pipelineCycle, fr.kernelAtClose, kname, + attr_id, aname, + fr.dropped ? 1 : 0, + escapes_in, flips_in, + fr.actionChecksum, golden_cs, + argmax_changed ? 1 : 0, + fr.actionToken, golden_tok, + action_changed ? 1 : 0, + safety_violated ? 1 : 0, + outcomeClassLabel(outcome_class), + fr.simTimePs); + + if (emit_golden_) { + golden_emit << fr.pipelineCycle << "," + << fr.kernelAtClose << "," + << fr.actionChecksum << "," + << fr.actionToken << "\n"; + } + + ++total; + if (fr.dropped) ++dropped; + if (argmax_changed) ++argmax_diff; + if (action_changed) ++action_diff; + if (unsafe_action) ++unsafe; + switch (outcome_class) { + case 1: ++o1; break; + case 2: ++o2; break; + case 3: ++o3; break; + case 4: ++o4; break; + default: break; + } + } + out_->output("=== End Action Scorer %s Per-Frame Trace (%" PRIu64 " frames) ===\n\n", + getName().c_str(), total); + + if (stat_frames_total_) stat_frames_total_->addData(total); + if (stat_frames_dropped_) stat_frames_dropped_->addData(dropped); + if (stat_frames_argmax_diff_) stat_frames_argmax_diff_->addData(argmax_diff); + if (stat_frames_action_diff_) stat_frames_action_diff_->addData(action_diff); + if (stat_frames_unsafe_) stat_frames_unsafe_->addData(unsafe); + if (stat_frames_o1_) stat_frames_o1_->addData(o1); + if (stat_frames_o2_) stat_frames_o2_->addData(o2); + if (stat_frames_o3_) stat_frames_o3_->addData(o3); + if (stat_frames_o4_) stat_frames_o4_->addData(o4); + + out_->output("=== Action Scorer %s Summary ===\n", getName().c_str()); + out_->output("frames_total,frames_dropped,frames_argmax_diff,frames_action_diff," + "frames_unsafe," + "frames_outcome_O1,frames_outcome_O2,frames_outcome_O3,frames_outcome_O4," + "drop_rate,argmax_change_rate,action_change_rate,unsafe_action_rate\n"); + double dr = total ? static_cast(dropped) / total : 0.0; + double ar = total ? static_cast(argmax_diff) / total : 0.0; + double tr = total ? static_cast(action_diff) / total : 0.0; + double ur = total ? static_cast(unsafe) / total : 0.0; + out_->output("%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" + PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%.6e,%.6e,%.6e,%.6e\n", + total, dropped, argmax_diff, action_diff, unsafe, + o1, o2, o3, o4, dr, ar, tr, ur); + out_->output("=== End Action Scorer %s Summary ===\n\n", getName().c_str()); + + if (emit_golden_) { + out_->output("=== Action Scorer %s Golden Emit ===\n", getName().c_str()); + out_->output("%s", golden_emit.str().c_str()); + out_->output("=== End Action Scorer %s Golden Emit ===\n\n", getName().c_str()); + } + + if (unmatched > 0 && golden_required_) { + out_->fatal(CALL_INFO, -1, + "ActionScorer '%s': %" PRIu64 " of %" PRIu64 " frames had no " + "golden entry (even via the cycle-only fallback) and " + "golden_required=true. The golden log is incomplete for this " + "run; re-run the BER=0 emit-golden pass.\n", + getName().c_str(), unmatched, total); + } + + // Test hooks: turn the summary counters into pass/fail so in-tree + // configs (tests/testFramePipeline*.py) can assert scorer behavior via + // the sst exit code alone. + bool expect_ok = true; + auto check = [&](const char* name, int64_t want, uint64_t got) { + if (want < 0 || static_cast(want) == got) return; + out_->output("ActionScorer '%s': FAIL %s=%" PRIu64 " != expected %" PRId64 ".\n", + getName().c_str(), name, got, want); + expect_ok = false; + }; + check("frames_total", expect_frames_total_, total); + check("frames_dropped", expect_frames_dropped_, dropped); + check("frames_argmax_diff", expect_frames_argmax_diff_, argmax_diff); + check("frames_action_diff", expect_frames_action_diff_, action_diff); + check("frames_unsafe", expect_frames_unsafe_, unsafe); + check("frames_outcome_O1", expect_frames_o1_, o1); + check("frames_outcome_O2", expect_frames_o2_, o2); + check("frames_outcome_O3", expect_frames_o3_, o3); + check("frames_outcome_O4", expect_frames_o4_, o4); + if (!expect_ok) { + out_->fatal(CALL_INFO, -1, + "ActionScorer '%s': expect_* checks failed (see FAIL lines).\n", + getName().c_str()); + } +} diff --git a/src/sst/elements/carcosa/components/actionScorer.h b/src/sst/elements/carcosa/components/actionScorer.h new file mode 100644 index 0000000000..537a1d4a64 --- /dev/null +++ b/src/sst/elements/carcosa/components/actionScorer.h @@ -0,0 +1,120 @@ +// 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 SST_ELEMENTS_CARCOSA_ACTION_SCORER_H +#define SST_ELEMENTS_CARCOSA_ACTION_SCORER_H + +// ActionScorer: walks PipelineStateBase::frames on finish() into a CSV. +// Prefer actionToken over raw checksum for divergence (O3 vs O4); missing +// golden_log fatals unless golden_required=false (empty log => no divergence). + +#include +#include +#include +#include +#include + +namespace SST { +namespace Carcosa { + +class ActionScorer : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT( + ActionScorer, + "carcosa", + "ActionScorer", + SST_ELI_ELEMENT_VERSION(1, 0, 0), + "End-task behavioral scorer for VLA fault-injection runs. Reads the per-frame history " + "from PipelineStateBase::frames and emits a per-frame CSV plus a summary " + "(unsafe_action_rate, frame_drop_rate, mean_escapes_per_frame).", + COMPONENT_CATEGORY_UNCATEGORIZED) + + SST_ELI_DOCUMENT_PARAMS( + {"state_key", "Required. PipelineStateRegistry key whose ::frames vector this scorer ingests at finish().", ""}, + {"golden_log", "Optional path to a CSV (pipeline_cycle,kernel_at_close,action_checksum[,action_token]) whose checksums/tokens are treated as fault-free truth. Each line provides the golden values for the matching cycle. If empty, every frame's argmax_changed/action_changed is reported as 0 (used by the BER=0 emit-golden pass).", ""}, + {"golden_required", "If true (default) and golden_log is set but cannot be opened or contains no entries, fatal at setup() instead of silently scoring every frame as not-argmax-changed. Set to false only for self-replay sanity checks.", "true"}, + {"emit_golden", "If true and golden_log is empty, dump the observed (cycle,kernel,checksum) trace to STDOUT under a '=== Action Scorer Golden Emit ===' block so a baseline run can produce the file the comparison runs need.", "false"}, + {"verbose", "Enable verbose output.", "false"}, + {"expect_frames_total", "Test hook: expected frames_total at finish(); fatal on mismatch. -1 disables.", "-1"}, + {"expect_frames_dropped", "Test hook: expected frames_dropped at finish(); fatal on mismatch. -1 disables.", "-1"}, + {"expect_frames_argmax_diff", "Test hook: expected frames_argmax_diff at finish(); fatal on mismatch. -1 disables.", "-1"}, + {"expect_frames_action_diff", "Test hook: expected frames_action_diff at finish(); fatal on mismatch. -1 disables.", "-1"}, + {"expect_frames_unsafe", "Test hook: expected unsafe-action count at finish(); fatal on mismatch. -1 disables.", "-1"}, + {"expect_frames_o1", "Test hook: expected O1 count; -1 disables.", "-1"}, + {"expect_frames_o2", "Test hook: expected O2 count; -1 disables.", "-1"}, + {"expect_frames_o3", "Test hook: expected O3 count; -1 disables.", "-1"}, + {"expect_frames_o4", "Test hook: expected O4 count; -1 disables.", "-1"}) + + SST_ELI_DOCUMENT_STATISTICS( + {"frames_total", "Frames recorded in PipelineStateBase::frames.", "count", 1}, + {"frames_dropped", "Frames flagged dropped (DUE-on-frame).", "count", 1}, + {"frames_argmax_diff", "Frames whose action_checksum differs from golden (debug oracle).", "count", 1}, + {"frames_action_diff", "Frames whose decoded-action token differs from golden (headline divergence metric; falls back to checksum when tokens are unavailable).", "count", 1}, + {"frames_safety_violated", "Frames flagged unsafe (dropped OR argmax_diff).", "count", 1}, + {"frames_outcome_O1", "Frames classified O1 (clean).", "count", 1}, + {"frames_outcome_O2", "Frames classified O2 (late-but-correct).", "count", 1}, + {"frames_outcome_O3", "Frames classified O3 (SDC).", "count", 1}, + {"frames_outcome_O4", "Frames classified O4 (silent-benign).", "count", 1}) + + ActionScorer(SST::ComponentId_t id, SST::Params& params); + ~ActionScorer() override; + + void setup() override; + void finish() override; + +private: + struct GoldenEntry { + int pipelineCycle; + int kernelAtClose; + uint64_t checksum; + uint64_t token = 0; // decoded-action token; optional 4th column + bool hasToken = false; // false for legacy 3-column goldens + }; + + void loadGoldenLog(); + + std::string state_key_; + std::string golden_path_; + bool emit_golden_ = false; + bool golden_required_ = true; + bool verbose_ = false; + + std::vector golden_; + bool golden_loaded_ = false; + + // Test hooks (-1 = unchecked); see the expect_* params. + int64_t expect_frames_total_ = -1; + int64_t expect_frames_dropped_ = -1; + int64_t expect_frames_argmax_diff_ = -1; + int64_t expect_frames_action_diff_ = -1; + int64_t expect_frames_unsafe_ = -1; + int64_t expect_frames_o1_ = -1, expect_frames_o2_ = -1; + int64_t expect_frames_o3_ = -1, expect_frames_o4_ = -1; + + SST::Output* out_ = nullptr; + + Statistics::Statistic* stat_frames_total_ = nullptr; + Statistics::Statistic* stat_frames_dropped_ = nullptr; + Statistics::Statistic* stat_frames_argmax_diff_ = nullptr; + Statistics::Statistic* stat_frames_action_diff_ = nullptr; + Statistics::Statistic* stat_frames_unsafe_ = nullptr; + Statistics::Statistic* stat_frames_o1_ = nullptr; + Statistics::Statistic* stat_frames_o2_ = nullptr; + Statistics::Statistic* stat_frames_o3_ = nullptr; + Statistics::Statistic* stat_frames_o4_ = nullptr; + + static const char* outcomeClassLabel(int oclass); +}; + +} // namespace Carcosa +} // namespace SST + +#endif // SST_ELEMENTS_CARCOSA_ACTION_SCORER_H diff --git a/src/sst/elements/carcosa/components/carcosaCPUBase.cc b/src/sst/elements/carcosa/components/carcosaCPUBase.cc index 3a980edbf0..bca1a303eb 100644 --- a/src/sst/elements/carcosa/components/carcosaCPUBase.cc +++ b/src/sst/elements/carcosa/components/carcosaCPUBase.cc @@ -65,8 +65,8 @@ CarcosaCPUBase::CarcosaCPUBase(ComponentId_t id, Params& params) : ops = params.find("opCount", 0, found); sst_assert(found, CALL_INFO, -1, "%s, Error: parameter 'opCount' was not provided\n", getName().c_str()); - unsigned readf = params.find("read_freq", 25); - unsigned writef = params.find("write_freq", 75); + unsigned readf = params.find("read_freq", 75); + unsigned writef = params.find("write_freq", 25); unsigned flushf = params.find("flush_freq", 0); unsigned flushinvf = params.find("flushinv_freq", 0); unsigned customf = params.find("custom_freq", 0); @@ -177,7 +177,7 @@ bool CarcosaCPUBase::clockTic(Cycle_t) { if (clock_ticks % 1000 == 0) { #ifdef __SST_DEBUG_OUTPUT__ - out.output("test1 open from carcosa\n"); + out.output("test1 open from Carcosa\n"); #endif Carcosa::CpuEvent *ev = new Carcosa::CpuEvent("test1.txt", 0, 200); HaliLink->send(ev); @@ -188,7 +188,7 @@ bool CarcosaCPUBase::clockTic(Cycle_t) memory->send(req); } else if (clock_ticks % 500 == 0) { #ifdef __SST_DEBUG_OUTPUT__ - out.output("test2 open from carcosa\n"); + out.output("test2 open from Carcosa\n"); #endif Carcosa::CpuEvent *ev = new Carcosa::CpuEvent("test2.txt", 0, 200); HaliLink->send(ev); @@ -208,11 +208,41 @@ bool CarcosaCPUBase::clockTic(Cycle_t) if (reqsToSend > ops) reqsToSend = ops; for (int i = 0; i < (int)reqsToSend; i++) { - StandardMem::Addr addr = rng.generateNextUInt64() % 200; - rng.generateNextUInt32(); // consume RNG to maintain deterministic sequence - Interfaces::StandardMem::Request* req = createRead(addr); + StandardMem::Addr addr = rng.generateNextUInt64(); + uint32_t instNum = rng.generateNextUInt32() % high_mark; + std::string cmdString = "Read"; + Interfaces::StandardMem::Request* req; + if (ll_issued) { + req = createSC(); + cmdString = "StoreConditional"; + } else if (instNum < write_mark) { + req = createWrite(addr); + cmdString = "Write"; + } else if (instNum < flush_mark) { + req = createFlush(addr); + cmdString = "Flush"; + } else if (instNum < flushinv_mark) { + req = createFlushInv(addr); + cmdString = "FlushInv"; + } else if (instNum < custom_mark) { + // Custom requests are not implemented in this trimmed CPU. + req = createRead(addr); + } else if (instNum < llsc_mark) { + req = createLL(addr); + cmdString = "LoadLink"; + } else if (instNum < mmio_mark) { + if (rng.generateNextUInt32() % 2) { + req = createMMIORead(); + cmdString = "ReadMMIO"; + } else { + req = createMMIOWrite(); + cmdString = "WriteMMIO"; + } + } else { + req = createRead(addr); + } if (req->needsResponse()) { - requests[req->getID()] = std::make_pair(getCurrentSimTime(), std::string("Read")); + requests[req->getID()] = std::make_pair(getCurrentSimTime(), cmdString); } memory->send(req); ops--; diff --git a/src/sst/elements/carcosa/components/carcosaCPUBase.h b/src/sst/elements/carcosa/components/carcosaCPUBase.h index 6b16e129a5..b35ad51047 100644 --- a/src/sst/elements/carcosa/components/carcosaCPUBase.h +++ b/src/sst/elements/carcosa/components/carcosaCPUBase.h @@ -38,10 +38,7 @@ namespace SST { namespace MemHierarchy { using Req = SST::Interfaces::StandardMem::Request; -/** - * Shared base class for CarcosaCPU and FaultInjCPU. - * Contains all common state and logic; not ELI-registered. - */ +/** Shared base for CarcosaCPU / FaultInjCPU (not ELI-registered). */ class CarcosaCPUBase : public SST::Component { public: CarcosaCPUBase(SST::ComponentId_t id, SST::Params& params); diff --git a/src/sst/elements/carcosa/components/carcosaMemCtrl.cc b/src/sst/elements/carcosa/components/carcosaMemCtrl.cc index cb02679b9f..59fe17cfdd 100644 --- a/src/sst/elements/carcosa/components/carcosaMemCtrl.cc +++ b/src/sst/elements/carcosa/components/carcosaMemCtrl.cc @@ -45,13 +45,7 @@ using namespace SST::MemHierarchy; #define is_debug_event(ev) false #define Debug(level, fmt, ... ) #endif -/* - * Debug levels: - * 3 - event receive/response - * 4 - backing store - * 9 - init() - * 10 - address translation - */ +/* Debug levels: 3=event, 4=backing, 9=init, 10=addr translation. */ /*************************** Memory Controller ********************/ CarcosaMemCtrl::CarcosaMemCtrl(ComponentId_t id, Params ¶ms) : Component(id), backing_(NULL) { @@ -112,13 +106,7 @@ CarcosaMemCtrl::CarcosaMemCtrl(ComponentId_t id, Params ¶ms) : Component(id) string link_lat = params.find("direct_link_latency", "10 ns"); - /* CarcosaMemCtrl supports multiple ways of loading in backends: - * Legacy: - * Define backend and/or backendConvertor in the memcontroller's parameter set - * Better way: - * Fill backend slot with backend and memcontroller loads the compatible convertor - * - */ + /* Prefer backend subcomponent slot; fall back to legacy params. */ MemBackend * memory = loadUserSubComponent("backend"); if (!memory) { /* Try to load from our parameters (legacy mode 1) */ @@ -811,11 +799,7 @@ void CarcosaMemCtrl::processInitEvent( MemEventInit* me ) { void CarcosaMemCtrl::adjustRegionToMemSize() { - // Check memSize_ against region - // Set region_ to the smaller of the two - // It's sometimes useful to be able to adjust one of the params and not the other - // So, a mismatch is likely not an error, but alert the user in debug mode just in case - // TODO deprecate mem_size & just use region? + // Shrink region_ to memSize_ when both are set (either param may lag). uint64_t regSize = region_.end - region_.start; if (regSize != region_.REGION_MAX) { // The default is for region_.end = uint64_t -1, but then if we add one we wrap... regSize++; // Since region_.end and region_.start are inclusive diff --git a/src/sst/elements/carcosa/components/carcosaMemCtrl.h b/src/sst/elements/carcosa/components/carcosaMemCtrl.h index 351a35536d..de29c8040b 100644 --- a/src/sst/elements/carcosa/components/carcosaMemCtrl.h +++ b/src/sst/elements/carcosa/components/carcosaMemCtrl.h @@ -91,9 +91,9 @@ class CarcosaMemCtrl : public SST::Component { typedef uint64_t ReqId; CarcosaMemCtrl(ComponentId_t id, Params ¶ms); - void init(unsigned int phase) override; - void setup() override; - void complete(unsigned int phase) override; + virtual void init(unsigned int phase) override; + virtual void setup() override; + virtual void complete(unsigned int phase) override; void finish() override; virtual void handleMemResponse( SST::Event::id_type id, uint32_t flags ); diff --git a/src/sst/elements/carcosa/components/criticalActionWatcher.cc b/src/sst/elements/carcosa/components/criticalActionWatcher.cc new file mode 100644 index 0000000000..0820293d6d --- /dev/null +++ b/src/sst/elements/carcosa/components/criticalActionWatcher.cc @@ -0,0 +1,375 @@ +// 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. + +#include "sst_config.h" +#include "sst/elements/carcosa/components/criticalActionWatcher.h" +#include "sst/elements/carcosa/components/carcosaHash.h" +#include +#include +#include +#include + +using namespace SST; +using namespace SST::MemHierarchy; +using namespace SST::Carcosa; + +CriticalActionWatcher::CriticalActionWatcher(ComponentId_t id, Params& params) + : Component(id) { + out_ = new Output("", 1, 0, Output::STDOUT); + verbose_ = params.find("verbose", false); + state_key_ = params.find("state_key", ""); + critical_region_ = params.find("critical_region", "action_queue"); + critical_len_ = params.find("critical_len", 64); + applyOnResponsesOnly_ = + params.find("apply_on_responses_only", true); + actuation_kernel_name_ = params.find("actuation_kernel", ""); + golden_path_ = params.find("golden_log", ""); + golden_required_ = params.find("golden_required", true); + emit_golden_ = params.find("emit_golden", false); + + if (state_key_.empty()) { + out_->fatal(CALL_INFO, -1, + "CriticalActionWatcher '%s': state_key is required.\n", + getName().c_str()); + } + + stat_frames_critical_corrupted_ = + registerStatistic("frames_critical_region_corrupted"); + + if (isPortConnected("highlink")) { + highlink_ = configureLink("highlink", + new Event::Handler(this)); + } + if (isPortConnected("lowlink")) { + lowlink_ = configureLink("lowlink", + new Event::Handler(this)); + } + if (!highlink_ || !lowlink_) { + out_->fatal(CALL_INFO, -1, + "CriticalActionWatcher '%s': both highlink and lowlink must be connected.\n", + getName().c_str()); + } +} + +CriticalActionWatcher::~CriticalActionWatcher() { + delete out_; +} + +void CriticalActionWatcher::setup() { + state_ptr_ = PipelineStateRegistry::getMutable(state_key_); + if (!state_ptr_) { + out_->fatal(CALL_INFO, -1, + "CriticalActionWatcher '%s': no PipelineStateBase for key '%s'.\n", + getName().c_str(), state_key_.c_str()); + } + if (actuation_kernel_name_.empty()) { + actuation_kernel_name_ = state_ptr_->actuationKernelName; + } + loadGoldenLog(); +} + +void CriticalActionWatcher::init(unsigned phase) { + if (highlink_ && lowlink_) { + SST::Event* ev; + while ((ev = highlink_->recvUntimedData()) != nullptr) { + lowlink_->sendUntimedData(ev); + } + while ((ev = lowlink_->recvUntimedData()) != nullptr) { + highlink_->sendUntimedData(ev); + } + } + (void)phase; +} + +void CriticalActionWatcher::complete(unsigned phase) { + if (highlink_ && lowlink_) { + SST::Event* ev; + while ((ev = highlink_->recvUntimedData()) != nullptr) { + lowlink_->sendUntimedData(ev); + } + while ((ev = lowlink_->recvUntimedData()) != nullptr) { + highlink_->sendUntimedData(ev); + } + } + (void)phase; +} + +void CriticalActionWatcher::finish() { + if (saw_kernel_ && last_kernel_name_ == actuation_kernel_name_) + finalizeActuateFrame(); + if (emit_golden_) { + std::ostringstream os; + os << "=== Critical Action Watcher Golden Emit ===\n" + << "pipeline_cycle,kernel_at_close,action_checksum\n"; + for (const auto& row : emitted_golden_) { + os << row.first.first << "," << row.first.second << "," + << row.second << "\n"; + } + os << "=== End Critical Action Watcher Golden Emit ===\n"; + out_->output("%s", os.str().c_str()); + } + if (verbose_ && state_ptr_) { + out_->output("CriticalActionWatcher '%s': frames_critical_region_corrupted=%" PRIu64 "\n", + getName().c_str(), state_ptr_->framesCriticalRegionCorrupted); + } +} + +bool CriticalActionWatcher::isResponseCmd(MemEvent* mev) const { + if (!mev) return false; + Command c = mev->getCmd(); + return c == Command::GetSResp || c == Command::GetXResp || c == Command::WriteResp; +} + +bool CriticalActionWatcher::resolveCriticalBounds(uint64_t& base_out, uint64_t& len_out) const { + base_out = 0; + len_out = 0; + if (!state_ptr_) return false; + for (const auto& r : state_ptr_->regions) { + if (!r.valid || r.name != critical_region_) continue; + base_out = r.base; + len_out = r.size; + if (critical_len_ > 0 && critical_len_ < len_out) + len_out = critical_len_; + return len_out > 0; + } + return false; +} + +bool CriticalActionWatcher::eventOverlapsCritical(MemEvent* mev, + uint64_t& rel_off, + uint64_t& payload_off, + uint64_t& overlap_len) const { + rel_off = 0; + payload_off = 0; + overlap_len = 0; + if (!mev) return false; + uint64_t fbase = crit_base_, flen = crit_len_; + if (fbase == 0 && flen == 0 && !resolveCriticalBounds(fbase, flen)) + return false; + uint64_t vaddr = mev->getVirtualAddress(); + uint64_t addr = (vaddr != 0) ? vaddr : mev->getAddr(); + uint64_t size = mev->getPayload().empty() ? 64u : mev->getPayload().size(); + uint64_t end = addr + size; + uint64_t fend = fbase + flen; + if (!(addr < fend && end > fbase)) return false; + uint64_t ostart = std::max(addr, fbase); + uint64_t oend = std::min(end, fend); + rel_off = ostart - fbase; + payload_off = ostart - addr; + overlap_len = oend - ostart; + return overlap_len > 0; +} + +void CriticalActionWatcher::mergePayloadIntoSnapshot(uint64_t rel_off, + const std::vector& payload, + uint64_t payload_off, + uint64_t copy_len) { + if (snapshot_.empty()) return; + for (uint64_t i = 0; i < copy_len; ++i) { + uint64_t src = payload_off + i; + uint64_t pos = rel_off + i; + if (src >= payload.size() || pos >= snapshot_.size()) break; + snapshot_[pos] = payload[src]; + observed_this_frame_ = true; + } + // Publish after every merge, not on the later kernel-transition observation: + // the pipeline driver closes its FrameRecord on the ACTUATE status write, + // before this watcher necessarily sees traffic from the next kernel. + if (observed_this_frame_ && state_ptr_) { + state_ptr_->watcherActionChecksum = hashSnapshot(); + state_ptr_->watcherActionChecksumValid = true; + } +} + +uint64_t CriticalActionWatcher::hashSnapshot() const { + if (snapshot_.empty()) return 0; + return fnv1a64(snapshot_.data(), snapshot_.size()); +} + +void CriticalActionWatcher::loadGoldenLog() { + if (golden_path_.empty()) { + if (!emit_golden_) { + out_->output("CriticalActionWatcher '%s': WARNING no golden_log; " + "checksums will be published but critical-region " + "corruption will not be classified. Run a BER=0 pass " + "with emit_golden=true, then provide that CSV.\n", + getName().c_str()); + } + return; + } + + std::ifstream f(golden_path_); + if (!f.is_open()) { + if (golden_required_) { + out_->fatal(CALL_INFO, -1, + "CriticalActionWatcher '%s': golden_log='%s' could not be opened " + "and golden_required=true.\n", + getName().c_str(), golden_path_.c_str()); + } + out_->output("CriticalActionWatcher '%s': WARNING golden_log '%s' could " + "not be opened; corruption classification is disabled.\n", + getName().c_str(), golden_path_.c_str()); + return; + } + + std::string line; + int line_no = 0; + while (std::getline(f, line)) { + ++line_no; + if (line.empty() || line[0] == '#') continue; + std::istringstream ss(line); + std::string cycle_s, kernel_s, checksum_s; + if (!std::getline(ss, cycle_s, ',') || + !std::getline(ss, kernel_s, ',') || + !std::getline(ss, checksum_s, ',')) { + out_->output("CriticalActionWatcher '%s': skipping malformed " + "golden_log line %d: '%s'\n", + getName().c_str(), line_no, line.c_str()); + continue; + } + if (cycle_s.find("pipeline_cycle") != std::string::npos) continue; + + try { + int cycle = std::stoi(cycle_s); + int kernel = std::stoi(kernel_s); + uint64_t checksum = std::stoull(checksum_s, nullptr, 0); + auto key = std::make_pair(cycle, kernel); + if (!golden_by_frame_.emplace(key, checksum).second) { + out_->fatal(CALL_INFO, -1, + "CriticalActionWatcher '%s': duplicate golden entry for " + "pipeline_cycle=%d kernel_at_close=%d.\n", + getName().c_str(), cycle, kernel); + } + if (++golden_cycle_count_[cycle] == 1) + golden_by_cycle_[cycle] = checksum; + } catch (...) { + out_->output("CriticalActionWatcher '%s': skipping malformed " + "golden_log line %d: '%s'\n", + getName().c_str(), line_no, line.c_str()); + } + } + + if (golden_by_frame_.empty()) { + if (golden_required_) { + out_->fatal(CALL_INFO, -1, + "CriticalActionWatcher '%s': golden_log='%s' contained no valid " + "entries and golden_required=true.\n", + getName().c_str(), golden_path_.c_str()); + } + out_->output("CriticalActionWatcher '%s': WARNING golden_log '%s' " + "contained no valid entries; corruption classification " + "is disabled.\n", + getName().c_str(), golden_path_.c_str()); + } + if (verbose_) + out_->output("CriticalActionWatcher '%s': loaded %zu golden entries from '%s'.\n", + getName().c_str(), golden_by_frame_.size(), golden_path_.c_str()); +} + +bool CriticalActionWatcher::findGoldenChecksum(int pipeline_cycle, + int kernel_at_close, + uint64_t& checksum_out) const { + auto exact = golden_by_frame_.find({pipeline_cycle, kernel_at_close}); + if (exact != golden_by_frame_.end()) { + checksum_out = exact->second; + return true; + } + auto count = golden_cycle_count_.find(pipeline_cycle); + auto cycle = golden_by_cycle_.find(pipeline_cycle); + if (count != golden_cycle_count_.end() && count->second == 1 && + cycle != golden_by_cycle_.end()) { + checksum_out = cycle->second; + return true; + } + return false; +} + +void CriticalActionWatcher::finalizeActuateFrame() { + if (!state_ptr_) return; + uint64_t checksum = hashSnapshot(); + + int pipeline_cycle = state_ptr_->pipelineCycle; + if (emit_golden_ && observed_this_frame_) + emitted_golden_.push_back({{pipeline_cycle, last_kernel_id_}, checksum}); + + bool corrupted = false; + if (!golden_by_frame_.empty()) { + uint64_t golden = 0; + bool matched = observed_this_frame_ && + findGoldenChecksum(pipeline_cycle, last_kernel_id_, golden); + if (!matched) { + if (golden_required_) { + out_->fatal(CALL_INFO, -1, + "CriticalActionWatcher '%s': frame (pipeline_cycle=%d, " + "kernel_at_close=%d) %s; golden_required=true.\n", + getName().c_str(), pipeline_cycle, last_kernel_id_, + observed_this_frame_ ? "has no golden entry" + : "observed no critical-region bytes"); + } + if (!warned_unscored_frame_) { + warned_unscored_frame_ = true; + out_->output("CriticalActionWatcher '%s': WARNING at least one " + "frame could not be compared with golden_log; it " + "will not be classified (warning only prints once).\n", + getName().c_str()); + } + } else { + corrupted = checksum != golden; + } + } + state_ptr_->watcherCriticalCorrupted = corrupted; + if (corrupted) { + ++state_ptr_->framesCriticalRegionCorrupted; + if (stat_frames_critical_corrupted_) + stat_frames_critical_corrupted_->addData(1); + } + // Frame-close status write (no intervening critical traffic) already + // consumed this fold; retire the flag so the next frame cannot inherit a + // stale checksum if nothing downstream reads it. + state_ptr_->watcherActionChecksumValid = false; + snapshot_.assign(crit_len_, 0); + observed_this_frame_ = false; +} + +void CriticalActionWatcher::observeEvent(MemEvent* mev) { + if (!mev || !state_ptr_) return; + if (!resolveCriticalBounds(crit_base_, crit_len_)) { + crit_base_ = 0; + crit_len_ = 0; + } else if (snapshot_.size() != crit_len_) { + snapshot_.assign(crit_len_, 0); + } + + const std::string& k = state_ptr_->currentKernelName; + if (saw_kernel_ && last_kernel_name_ == actuation_kernel_name_ + && k != actuation_kernel_name_) { + finalizeActuateFrame(); + } + last_kernel_name_ = k; + last_kernel_id_ = state_ptr_->currentKernel; + saw_kernel_ = true; + + if ((!applyOnResponsesOnly_ || isResponseCmd(mev)) + && k == actuation_kernel_name_) { + uint64_t rel = 0, poff = 0, olen = 0; + if (eventOverlapsCritical(mev, rel, poff, olen)) { + const auto& payload = mev->getPayload(); + if (!payload.empty()) + mergePayloadIntoSnapshot(rel, payload, poff, olen); + } + } +} + +void CriticalActionWatcher::handleHighlink(Event* ev) { + observeEvent(dynamic_cast(ev)); + if (lowlink_) lowlink_->send(ev); +} + +void CriticalActionWatcher::handleLowlink(Event* ev) { + observeEvent(dynamic_cast(ev)); + if (highlink_) highlink_->send(ev); +} diff --git a/src/sst/elements/carcosa/components/criticalActionWatcher.h b/src/sst/elements/carcosa/components/criticalActionWatcher.h new file mode 100644 index 0000000000..2f802e10f1 --- /dev/null +++ b/src/sst/elements/carcosa/components/criticalActionWatcher.h @@ -0,0 +1,119 @@ +// 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. + +#ifndef SST_ELEMENTS_CARCOSA_CRITICAL_ACTION_WATCHER_H +#define SST_ELEMENTS_CARCOSA_CRITICAL_ACTION_WATCHER_H + +#include "sst/elements/carcosa/components/pipelineStateRegistry.h" +#include "sst/elements/memHierarchy/memEvent.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace SST { +namespace Carcosa { + +/** Snapshots labeled DRAM bytes; publishes evolving checksum during ACTUATE. */ +class CriticalActionWatcher : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT( + CriticalActionWatcher, + "carcosa", + "CriticalActionWatcher", + SST_ELI_ELEMENT_VERSION(1, 0, 0), + "Snapshots CPU-observed bytes in a critical memory region, publishes " + "watcherActionChecksum into PipelineStateBase during ACTUATE, " + "and compares each frame against an explicit fault-free golden log.", + COMPONENT_CATEGORY_MEMORY) + + SST_ELI_DOCUMENT_PARAMS( + {"verbose", "Enable verbose output.", "false"}, + {"state_key", "PipelineStateRegistry key (required).", ""}, + {"critical_region", "Published region name to watch (e.g. action_queue).", "action_queue"}, + {"critical_len", "Max bytes to hash (0 = entire region).", "64"}, + {"apply_on_responses_only", "Only snapshot read responses (these arrive on the lowlink side; requests are observed on highlink when false).", "true"}, + {"actuation_kernel", "Workload-supplied kernel name that marks frame commitment. The snapshot is taken during this kernel and frozen on the trailing edge (kernel != actuation_kernel). Falls back to PipelineStateBase::actuationKernelName when this param is empty.", ""}, + {"golden_log", "Optional ActionScorer-compatible CSV (pipeline_cycle,kernel_at_close,action_checksum[,action_token]) containing fault-free per-frame watcher checksums. Without it, checksums are still published but corruption is not classified. In the full pipeline the driver stamps the watcher checksum into FrameRecord::actionChecksum and ActionScorer owns golden comparison; configure this only in watcher-focused tests (tests/testFramePipeline*.py) or deployments without a scorer, so a regen does not have to keep two golden sets consistent.", ""}, + {"golden_required", "If true (default) and golden_log is configured, fatal when the file is missing, empty, ambiguous, or lacks a frame observed by this run.", "true"}, + {"emit_golden", "Emit observed watcher checksums in ActionScorer-compatible CSV form, normally during a BER=0 run with golden_log empty.", "false"}) + + SST_ELI_DOCUMENT_PORTS( + {"highlink", "Toward directory/cache", {"memHierarchy.MemEventBase"}}, + {"lowlink", "Toward EccGuard/memory", {"memHierarchy.MemEventBase"}}) + + SST_ELI_DOCUMENT_STATISTICS( + {"frames_critical_region_corrupted", "Frames whose critical-window checksum differs from the matching explicit golden-log entry.", "count", 1}) + + CriticalActionWatcher(SST::ComponentId_t id, SST::Params& params); + ~CriticalActionWatcher() override; + + void setup() override; + void init(unsigned phase) override; + void complete(unsigned phase) override; + void finish() override; + +private: + void handleHighlink(SST::Event* ev); + void handleLowlink(SST::Event* ev); + void observeEvent(SST::MemHierarchy::MemEvent* mev); + + bool isResponseCmd(SST::MemHierarchy::MemEvent* mev) const; + bool resolveCriticalBounds(uint64_t& base_out, uint64_t& len_out) const; + bool eventOverlapsCritical(SST::MemHierarchy::MemEvent* mev, + uint64_t& rel_off, uint64_t& payload_off, + uint64_t& overlap_len) const; + void mergePayloadIntoSnapshot(uint64_t rel_off, const std::vector& payload, + uint64_t payload_off, uint64_t copy_len); + uint64_t hashSnapshot() const; + void loadGoldenLog(); + bool findGoldenChecksum(int pipeline_cycle, int kernel_at_close, + uint64_t& checksum_out) const; + void finalizeActuateFrame(); + + SST::Output* out_ = nullptr; + bool verbose_ = false; + std::string state_key_; + std::string critical_region_; + uint64_t critical_len_ = 64; + + bool applyOnResponsesOnly_ = true; + + PipelineStateBase* state_ptr_ = nullptr; + std::string actuation_kernel_name_; + std::string last_kernel_name_; + int last_kernel_id_ = -1; + bool saw_kernel_ = false; + + std::string golden_path_; + bool golden_required_ = true; + bool emit_golden_ = false; + std::map, uint64_t> golden_by_frame_; + std::map golden_by_cycle_; + std::map golden_cycle_count_; + std::vector, uint64_t>> emitted_golden_; + bool warned_unscored_frame_ = false; + + uint64_t crit_base_ = 0; + uint64_t crit_len_ = 0; + std::vector snapshot_; + bool observed_this_frame_ = false; + + SST::Link* highlink_ = nullptr; + SST::Link* lowlink_ = nullptr; + + Statistics::Statistic* stat_frames_critical_corrupted_ = nullptr; +}; + +} // namespace Carcosa +} // namespace SST + +#endif /* SST_ELEMENTS_CARCOSA_CRITICAL_ACTION_WATCHER_H */ diff --git a/src/sst/elements/carcosa/components/eccGuard.cc b/src/sst/elements/carcosa/components/eccGuard.cc new file mode 100644 index 0000000000..06f1e52854 --- /dev/null +++ b/src/sst/elements/carcosa/components/eccGuard.cc @@ -0,0 +1,1649 @@ +// 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 "sst_config.h" +#include "sst/elements/carcosa/components/eccGuard.h" +#include "sst/elements/carcosa/components/eccModelMath.h" +#include +#include +#include +#include +#include +#include + +using namespace SST; +using namespace SST::MemHierarchy; +using namespace SST::Carcosa; + +namespace { + +constexpr int kModeCount = static_cast(EccGuard::FaultMode::Count); + +// Sridharan ASPLOS'15 Table 4 + Schroeder SIGMETRICS'09 dominant-mode mix. +// Order: cell, word, row, column, bank, device. +constexpr double kDefaultModeWeights[kModeCount] = { + 0.55, 0.15, 0.10, 0.08, 0.07, 0.05 +}; + +// Approx bit errors per fault mode across the line. Correlated modes deposit +// the whole count into one ECC word (spillover if needed); SingleCell scatters. +// Defaults are conservative under SECDED_64 / CHIPKILL_x4. +constexpr unsigned kFaultModeBitsLow[kModeCount] = { 1, 2, 4, 4, 8, 32 }; +constexpr unsigned kFaultModeBitsHigh[kModeCount] = { 1, 2, 8, 8, 16, 64 }; + +const char* faultModeName(EccGuard::FaultMode m) { + switch (m) { + case EccGuard::FaultMode::SingleCell: return "single_cell"; + case EccGuard::FaultMode::SingleWord: return "single_word"; + case EccGuard::FaultMode::SingleRow: return "single_row"; + case EccGuard::FaultMode::SingleColumn: return "single_column"; + case EccGuard::FaultMode::SingleBank: return "single_bank"; + case EccGuard::FaultMode::SingleDevice: return "single_device"; + default: return "unknown"; + } +} + +bool parseModeWeightsCsv(const std::string& csv, double out[kModeCount]) { + std::stringstream ss(csv); + std::string tok; + int idx = 0; + while (std::getline(ss, tok, ':')) { + if (idx >= kModeCount) return false; + try { + out[idx++] = std::stod(tok); + } catch (...) { + return false; + } + } + return idx == kModeCount; +} + +EccGuard::FaultModel parseFaultModel(const std::string& s) { + if (s == "jedec_mix" || s == "jedec" || s == "JEDEC_MIX") return EccGuard::FaultModel::JedecMix; + if (s == "campaign" || s == "CAMPAIGN") return EccGuard::FaultModel::Campaign; + if (s == "resident" || s == "RESIDENT") return EccGuard::FaultModel::Resident; + return EccGuard::FaultModel::Poisson; +} + +// Parse campaign mode name into FaultMode; unknown -> SingleRow (same +// defaulting as jedec_mix). Crash only on empty/garbage upstream. +EccGuard::FaultMode parseCampaignMode(const std::string& s) { + if (s == "cell" || s == "single_cell" || s == "SingleCell") return EccGuard::FaultMode::SingleCell; + if (s == "word" || s == "single_word" || s == "SingleWord") return EccGuard::FaultMode::SingleWord; + if (s == "row" || s == "single_row" || s == "SingleRow") return EccGuard::FaultMode::SingleRow; + if (s == "column" || s == "single_column" || s == "SingleColumn") return EccGuard::FaultMode::SingleColumn; + if (s == "bank" || s == "single_bank" || s == "SingleBank") return EccGuard::FaultMode::SingleBank; + if (s == "device" || s == "single_device" || s == "SingleDevice") return EccGuard::FaultMode::SingleDevice; + if (s == "multi_chip" || s == "MULTI_CHIP") return EccGuard::FaultMode::SingleWord; + return EccGuard::FaultMode::SingleRow; +} + +bool isMultiChipCampaignAlias(const std::string& s) { + return s == "multi_chip" || s == "MULTI_CHIP"; +} + +// Resolve campaign_target_kernel to the canonical currentKernelName key. +// Empty means every kernel (matches the empty name published between kernels). +std::string resolveCampaignKernel(const std::string& raw) { + if (raw.empty() || raw == "any" || raw == "ANY" || raw == "*" + || raw == "-1") { + return std::string(); + } + return raw; +} + +EccGuard::PayloadDtype parseDtype(const std::string& s) { + if (s == "bf16" || s == "BF16") return EccGuard::PayloadDtype::Bf16; + if (s == "fp8" || s == "FP8") return EccGuard::PayloadDtype::Fp8; + if (s == "int8" || s == "INT8") return EccGuard::PayloadDtype::Int8; + return EccGuard::PayloadDtype::Bytes; +} + +EccGuard::DueAction parseDueAction(const std::string& s) { + if (s == "drop_frame" || s == "drop" || s == "DROP_FRAME") return EccGuard::DueAction::DropFrame; + return EccGuard::DueAction::LatencyOnly; +} + +const char* dtypeName(EccGuard::PayloadDtype d) { + switch (d) { + case EccGuard::PayloadDtype::Bytes: return "bytes"; + case EccGuard::PayloadDtype::Bf16: return "bf16"; + case EccGuard::PayloadDtype::Fp8: return "fp8"; + case EccGuard::PayloadDtype::Int8: return "int8"; + } + return "unknown"; +} + +// Returns true if the given bit (0-indexed inside its element) is "high blast" +// for the dtype (sign bit or top exponent bit). Bit 0 is LSB of the element. +bool isHighBlastBit(EccGuard::PayloadDtype dtype, unsigned bit_in_element) { + switch (dtype) { + case EccGuard::PayloadDtype::Bf16: { + // bf16: [15] sign, [14:7] exponent, [6:0] mantissa. + if (bit_in_element == 15) return true; // sign + if (bit_in_element >= 13 && bit_in_element <= 14) return true; // top 2 exp bits + return false; + } + case EccGuard::PayloadDtype::Fp8: { + // E4M3-style fp8: [7] sign, [6:3] exponent, [2:0] mantissa. + if (bit_in_element == 7) return true; + if (bit_in_element == 6) return true; + return false; + } + case EccGuard::PayloadDtype::Int8: { + // Two's-complement int8: bit 7 sign. + return bit_in_element == 7; + } + case EccGuard::PayloadDtype::Bytes: + default: + return false; + } +} + +unsigned dtypeBytes(EccGuard::PayloadDtype d) { + switch (d) { + case EccGuard::PayloadDtype::Bf16: return 2; + case EccGuard::PayloadDtype::Fp8: + case EccGuard::PayloadDtype::Int8: return 1; + case EccGuard::PayloadDtype::Bytes: + default: return 1; + } +} + +} // namespace + +EccGuard::EccGuard(ComponentId_t id, Params& params) : Component(id) { + requireLibrary("memHierarchy"); + + out_ = new Output("", 1, 0, Output::STDOUT); + verbose_ = params.find("verbose", false); + test_total_min_ = params.find("test_total_min", -1); + test_total_max_ = params.find("test_total_max", -1); + test_clean_min_ = params.find("test_clean_min", -1); + test_clean_max_ = params.find("test_clean_max", -1); + test_correctable_min_ = params.find("test_correctable_min", -1); + test_correctable_max_ = params.find("test_correctable_max", -1); + test_due_min_ = params.find("test_due_min", -1); + test_due_max_ = params.find("test_due_max", -1); + test_escape_min_ = params.find("test_escape_min", -1); + test_escape_max_ = params.find("test_escape_max", -1); + test_resident_born_min_ = params.find("test_resident_born_min", -1); + test_resident_born_max_ = params.find("test_resident_born_max", -1); + + state_key_ = params.find("state_key", ""); + applyOnResponsesOnly_ = params.find("apply_on_responses_only", true); + + EccPolicyEntry uniform; + uniform.inherits_uniform = false; + std::string scheme_str = params.find("ecc_scheme", "none"); + if (!eccSchemeFromString(scheme_str, uniform.scheme)) { + out_->fatal(CALL_INFO, -1, + "EccGuard: unknown ecc_scheme '%s'. Use 'none', 'secded', or 'chipkill'.\n", + scheme_str.c_str()); + } + uniform.ber = params.find("ber", 0.0); + uniform.correctable_latency_ps = params.find("correctable_latency_ps", 0); + uniform.due_latency_ps = params.find("due_latency_ps", 0); + uniform.escape_latency_ps = params.find("escape_latency_ps", 0); + if (uniform.ber < 0.0 || uniform.ber > 1.0) { + out_->fatal(CALL_INFO, -1, + "EccGuard: ber=%g out of range [0.0, 1.0].\n", uniform.ber); + } + policy_.setUniform(uniform); + + std::string ks_csv = params.find("kernel_policy", ""); + if (!ks_csv.empty()) { + std::vector errors; + int parsed = policy_.parseCsv(ks_csv, errors); + for (auto& e : errors) out_->output("EccGuard: %s\n", e.c_str()); + if (verbose_) { + out_->output("EccGuard: parsed %d kernel/region policy override(s).\n", parsed); + } + } + + // Phase 2: fault model + dtype-aware flips + DUE action. + fault_model_ = parseFaultModel(params.find("fault_model", "poisson")); + payload_dtype_ = parseDtype (params.find("payload_dtype", "bytes")); + due_action_ = parseDueAction (params.find("due_action", "latency_only")); + + std::string mw_csv = params.find("fault_mode_weights", ""); + if (!mw_csv.empty()) { + if (!parseModeWeightsCsv(mw_csv, mode_weights_)) { + out_->fatal(CALL_INFO, -1, + "EccGuard: malformed fault_mode_weights '%s'; need 6 colon-separated doubles " + "(cell:word:row:column:bank:device).\n", mw_csv.c_str()); + } + } else { + for (int i = 0; i < kModeCount; ++i) mode_weights_[i] = kDefaultModeWeights[i]; + } + // Normalize. (parseCsv lets users pass arbitrary positive numbers.) + { + double sum = 0.0; + for (int i = 0; i < kModeCount; ++i) { + if (mode_weights_[i] < 0.0) mode_weights_[i] = 0.0; + sum += mode_weights_[i]; + } + if (sum <= 0.0) { + out_->fatal(CALL_INFO, -1, + "EccGuard: fault_mode_weights sum to zero; provide at least one positive weight.\n"); + } + for (int i = 0; i < kModeCount; ++i) mode_weights_[i] /= sum; + } + + fault_event_rate_ = params.find("fault_event_rate", 0.0); + + // Campaign-mode parameters. These are inert unless fault_model_ == Campaign. + { + std::string raw_target = params.find("campaign_target_kernel", "any"); + campaign_target_kernel_name_ = resolveCampaignKernel(raw_target); + if (campaign_target_kernel_name_.empty() + && raw_target != "any" && raw_target != "ANY" + && raw_target != "-1" && !raw_target.empty()) { + out_->output("EccGuard WARNING: campaign_target_kernel='%s' did not " + "resolve to a known kernel; defaulting to 'any'.\n", + raw_target.c_str()); + } + campaign_event_budget_ = params.find("campaign_event_budget", 0); + campaign_event_rate_ = params.find ("campaign_event_rate", 0.0); + campaign_max_per_kernel_entry_ = + params.find("campaign_max_events_per_kernel_entry", 0); + campaign_errors_fixed_ = params.find("campaign_errors_fixed", 0); + std::string raw_cmode = params.find("campaign_mode", "row"); + campaign_force_multi_chip_ = + params.find("campaign_force_multi_chip", false) + || isMultiChipCampaignAlias(raw_cmode); + campaign_mode_ = parseCampaignMode(raw_cmode); + addr_filter_region_ = params.find("addr_filter_region", ""); + addr_filter_len_ = params.find("addr_filter_len", 0); + inject_addr_start_ = params.find("inject_addr_start", 0); + inject_addr_len_ = params.find("inject_addr_len", 0); + if (fault_model_ == FaultModel::Campaign && verbose_) { + out_->output("EccGuard: campaign mode active: target=%s mode=%d budget=%" PRIu64 + " rate=%.3e\n", + campaign_target_kernel_name_.empty() + ? "any" + : campaign_target_kernel_name_.c_str(), + static_cast(campaign_mode_), + campaign_event_budget_, + campaign_event_rate_); + } + if (fault_model_ == FaultModel::Campaign && campaign_event_budget_ == 0) { + out_->output("EccGuard WARNING: fault_model='campaign' but " + "campaign_event_budget==0; no faults will be injected.\n"); + } + } + double fit_rate = params.find("fit_per_mbit_per_hour", 0.0); + double dram_mb = params.find("dram_capacity_mb", 1024.0); + double per_event_ns = params.find("sim_time_per_event_ns", 100.0); + if (fit_rate > 0.0 && fault_event_rate_ <= 0.0) { + // FIT = failures per 1e9 device-hours, per Mbit. Convert to per-event prob. + // dram_capacity_mb is megaBYTES; FIT is per megaBIT, hence the x8. + fault_event_rate_ = EccModelMath::fitEventRate( + fit_rate, dram_mb, per_event_ns); + if (verbose_) { + out_->output("EccGuard: FIT=%.3g per Mbit/h x dram_mb=%.0f, sim_ns/event=%.1f -> fault_event_rate=%.3e\n", + fit_rate, dram_mb, per_event_ns, fault_event_rate_); + } + } + + // Resident fault-map parameters (fault_model='resident'). Parsed + // unconditionally so sst-info documents them; inert unless the model is + // selected. + { + resident_addr_start_ = params.find("resident_addr_start", 0); + resident_addr_len_ = params.find("resident_addr_len", 0); + resident_faults_at_start_ = params.find("resident_faults_at_start", 0); + double rate_per_ms = params.find("resident_fault_rate_per_ms", 0.0); + resident_time_accel_ = params.find("resident_time_acceleration", 1.0); + double scrub_us = params.find("resident_scrub_interval_us", 0.0); + resident_scrub_interval_ns_ = static_cast(scrub_us * 1e3); + resident_permanent_fraction_ = + params.find("resident_permanent_fraction", 0.3); + std::string rmode = params.find("resident_mode", "mix"); + resident_mode_mix_ = (rmode.empty() || rmode == "mix" || rmode == "MIX"); + if (!resident_mode_mix_) resident_mode_fixed_ = parseCampaignMode(rmode); + resident_row_bytes_ = params.find("resident_row_bytes", 8192); + resident_bank_rows_ = params.find("resident_bank_rows", 8); + if (resident_row_bytes_ < 64) resident_row_bytes_ = 64; + if (resident_bank_rows_ == 0) resident_bank_rows_ = 1; + + if (rate_per_ms > 0.0) { + resident_rate_per_ns_ = rate_per_ms / 1e6; + } else if (fit_rate > 0.0) { + // Faults arrive per (capacity x time), not per access: FIT/Mbit/h + // x capacity -> failures/hour, then scale into sim time. + // dram_capacity_mb is megaBYTES; FIT is per megaBIT, hence x8. + double failures_per_hour = fit_rate * 1e-9 * dram_mb * 8.0; + resident_rate_per_ns_ = + failures_per_hour / 3.6e12 * resident_time_accel_; + } + + if (fault_model_ == FaultModel::Resident) { + uint64_t wbase = 0, wlen = 0; + if (!resolveResidentWindow(wbase, wlen) || wlen == 0) { + out_->fatal(CALL_INFO, -1, + "EccGuard: fault_model='resident' requires a bounded fault-map " + "window: set resident_addr_start/resident_addr_len (or the " + "inject_addr_start/inject_addr_len fallback).\n"); + } + if (resident_rate_per_ns_ <= 0.0 && resident_faults_at_start_ == 0) { + out_->output("EccGuard WARNING: fault_model='resident' with no " + "arrival rate (resident_fault_rate_per_ms / FIT) and " + "resident_faults_at_start==0; the fault map stays " + "empty and every access classifies clean.\n"); + } + } + } + + // Mersenne for the bit-pick (matches RandomFlipFault); std::mt19937 for Poisson. + uint64_t seed = params.find("seed", 0); + if (seed != 0) { + rng_.seed(seed); + stdRng_.seed(static_cast(seed)); + } else { + stdRng_.seed(0xC0FFEEu); + } + // Dedicated RNG for the resident fault map so birth times/footprints stay + // identical across paired A/B runs that differ only in scheme or traffic. + residentRng_.seed(seed != 0 ? seed * 0x9E3779B97F4A7C15ULL : 0xD1CEB00Cu); + + if (isPortConnected("highlink")) { + highlink_ = configureLink("highlink", + new Event::Handler(this)); + } + if (isPortConnected("lowlink")) { + lowlink_ = configureLink("lowlink", + new Event::Handler(this)); + } + if (!highlink_ || !lowlink_) { + out_->fatal(CALL_INFO, -1, + "EccGuard '%s': both highlink and lowlink must be connected.\n", + getName().c_str()); + } + + selfLink_ = configureSelfLink("ecc_self", "1ps", + new Event::Handler(this)); + + stat_total_ = registerStatistic("events_total"); + stat_clean_ = registerStatistic("events_clean"); + stat_correctable_ = registerStatistic("events_correctable"); + stat_due_ = registerStatistic("events_due"); + stat_escape_ = registerStatistic("events_escape"); + stat_latency_ = registerStatistic("latency_added_ps"); + stat_correlated_row_ = registerStatistic("events_correlated_row"); + stat_correlated_bank_ = registerStatistic("events_correlated_bank"); + stat_correlated_device_ = registerStatistic("events_correlated_device"); + stat_escape_high_blast_ = registerStatistic("escape_high_blast"); + stat_escape_low_blast_ = registerStatistic("escape_low_blast"); + stat_due_poisoned_ = registerStatistic("due_poisoned_bits"); + stat_frames_aborted_ = registerStatistic("frames_aborted"); + stat_resident_born_ = registerStatistic("resident_faults_born"); + stat_resident_scrubbed_ = registerStatistic("resident_faults_scrubbed"); + stat_resident_scrub_due_ = registerStatistic("resident_scrub_due"); +} + +EccGuard::~EccGuard() { + delete out_; +} + +void EccGuard::init(unsigned phase) { + if (highlink_ && lowlink_) { + SST::Event* ev; + while ((ev = highlink_->recvUntimedData()) != nullptr) { + lowlink_->sendUntimedData(ev); + } + while ((ev = lowlink_->recvUntimedData()) != nullptr) { + highlink_->sendUntimedData(ev); + } + } + (void)phase; +} + +void EccGuard::setup() { + resolveStateLazy(); + + // Warn in setup() for every policy BER above kEccBerTightUpperBound so + // reviewers see the bound before the sim produces numbers. + policy_.forEachEntry([&](const std::string& origin, const EccPolicyEntry& e) { + if (e.ber > 0.0) { + warnIfBerExceedsTightBound(e.ber, origin.c_str()); + } + }); + + if (fault_model_ == FaultModel::Resident) { + for (uint64_t i = 0; i < resident_faults_at_start_; ++i) { + materializeResidentFault(); + } + if (resident_rate_per_ns_ > 0.0) { + std::exponential_distribution exp_ns(resident_rate_per_ns_); + resident_next_birth_ns_ = + std::max(1, static_cast(exp_ns(residentRng_))); + } + if (resident_scrub_interval_ns_ > 0) { + resident_next_scrub_ns_ = resident_scrub_interval_ns_; + } + resident_started_ = true; + if (verbose_) { + uint64_t wbase = 0, wlen = 0; + resolveResidentWindow(wbase, wlen); + out_->output("EccGuard '%s': resident fault map over [0x%" PRIx64 + ", +%" PRIu64 "): initial_faults=%" PRIu64 + " rate=%.3e/ns scrub_ns=%" PRIu64 + " permanent_frac=%.2f lines_faulty=%zu\n", + getName().c_str(), wbase, wlen, + resident_faults_at_start_, resident_rate_per_ns_, + resident_scrub_interval_ns_, + resident_permanent_fraction_, resident_mask_.size()); + } + } + + if (verbose_) { + out_->output("EccGuard '%s': setup. uniform scheme=%s ber=%g state_key='%s' state_ptr=%p " + "fault_model=%s payload_dtype=%s due_action=%s event_rate=%.3e\n", + getName().c_str(), + eccSchemeName(policy_.uniform().scheme), + policy_.uniform().ber, + state_key_.c_str(), + (const void*)state_ptr_, + (fault_model_ == FaultModel::JedecMix + ? "jedec_mix" + : (fault_model_ == FaultModel::Campaign ? "campaign" + : "poisson")), + dtypeName(payload_dtype_), + due_action_ == DueAction::DropFrame ? "drop_frame" : "latency_only", + fault_event_rate_); + } +} + +void EccGuard::complete(unsigned phase) { + if (highlink_ && lowlink_) { + SST::Event* ev; + while ((ev = highlink_->recvUntimedData()) != nullptr) { + lowlink_->sendUntimedData(ev); + } + while ((ev = lowlink_->recvUntimedData()) != nullptr) { + highlink_->sendUntimedData(ev); + } + } + (void)phase; +} + +void EccGuard::finish() { + OutcomeCounters totals; + for (const auto& kv : per_kernel_) { + totals.clean += kv.second.clean; + totals.correctable += kv.second.correctable; + totals.due += kv.second.due; + totals.escape += kv.second.escape; + } + out_->output("\n=== EccGuard %s Per-Kernel Outcomes ===\n", getName().c_str()); + out_->output("kernel_name,clean,correctable,due,escape,latency_ps\n"); + for (const auto& kv : per_kernel_) { + const auto& c = kv.second; + if (c.clean + c.correctable + c.due + c.escape == 0) continue; + const std::string& kname = kv.first.empty() ? std::string("UNKNOWN") : kv.first; + out_->output("%s,%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 "\n", + kname.c_str(), + c.clean, c.correctable, c.due, c.escape, c.latency_ps); + } + out_->output("=== End EccGuard %s Per-Kernel Outcomes ===\n\n", getName().c_str()); + + if (!per_kernel_region_.empty()) { + out_->output("\n=== EccGuard %s Per-Kernel-Per-Region Outcomes ===\n", getName().c_str()); + out_->output("kernel_name,region,clean,correctable,due,escape,latency_ps\n"); + for (auto& kv : per_kernel_region_) { + const std::string& kname = kv.first.first.empty() ? std::string("UNKNOWN") : kv.first.first; + const std::string& region = kv.first.second.empty() ? std::string("unlabeled") : kv.first.second; + const auto& c = kv.second; + out_->output("%s,%s,%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 "\n", + kname.c_str(), region.c_str(), + c.clean, c.correctable, c.due, c.escape, c.latency_ps); + } + out_->output("=== End EccGuard %s Per-Kernel-Per-Region Outcomes ===\n\n", getName().c_str()); + } + + bool any_mode = false; + for (int i = 0; i < kModeCount; ++i) if (per_mode_draws_[i] > 0) { any_mode = true; break; } + if (any_mode) { + out_->output("\n=== EccGuard %s Fault-Mode Draws ===\n", getName().c_str()); + out_->output("mode,count\n"); + for (int i = 0; i < kModeCount; ++i) { + out_->output("%s,%" PRIu64 "\n", + faultModeName(static_cast(i)), per_mode_draws_[i]); + } + out_->output("=== End EccGuard %s Fault-Mode Draws ===\n\n", getName().c_str()); + } + + if (escape_high_blast_total_ + escape_low_blast_total_ > 0 + || frames_aborted_total_ > 0 || due_poison_flips_total_ > 0) { + out_->output("\n=== EccGuard %s Escape/Abort Summary ===\n", getName().c_str()); + out_->output("escape_high_blast,escape_low_blast,frames_aborted,payload_dtype,due_poisoned_bits\n"); + out_->output("%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%s,%" PRIu64 "\n", + escape_high_blast_total_, escape_low_blast_total_, + frames_aborted_total_, dtypeName(payload_dtype_), + due_poison_flips_total_); + out_->output("=== End EccGuard %s Escape/Abort Summary ===\n\n", getName().c_str()); + } + + if (fault_model_ == FaultModel::Resident) { + // Advance the fault-map clock to end-of-sim; it otherwise only moves + // on traffic, so sparse-then-idle patterns would under-report births + // and scrubs due after the last access. + advanceResidentClock(getCurrentSimTimeNano()); + uint64_t alive_permanent = 0; + for (const auto& f : resident_faults_) if (f.permanent) ++alive_permanent; + out_->output("\n=== EccGuard %s Resident Fault Map Summary ===\n", getName().c_str()); + out_->output("faults_born,faults_alive,faults_permanent,faults_scrubbed,scrub_due_words,lines_faulty\n"); + out_->output("%" PRIu64 ",%zu,%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%zu\n", + resident_faults_born_total_, resident_faults_.size(), + alive_permanent, resident_faults_scrubbed_total_, + resident_scrub_due_total_, resident_mask_.size()); + out_->output("=== End EccGuard %s Resident Fault Map Summary ===\n\n", getName().c_str()); + } + + const uint64_t total = totals.clean + totals.correctable + totals.due + totals.escape; + bool ok = true; + auto bound = [&](const char* name, uint64_t got, int64_t lo, int64_t hi) { + if ((lo >= 0 && got < static_cast(lo)) || + (hi >= 0 && got > static_cast(hi))) { + out_->output("EccGuard '%s': FAIL %s=%" PRIu64 + " outside [%" PRId64 ",%" PRId64 "].\n", + getName().c_str(), name, got, lo, hi); + ok = false; + } + }; + bound("total", total, test_total_min_, test_total_max_); + bound("clean", totals.clean, test_clean_min_, test_clean_max_); + bound("correctable", totals.correctable, + test_correctable_min_, test_correctable_max_); + bound("due", totals.due, test_due_min_, test_due_max_); + bound("escape", totals.escape, test_escape_min_, test_escape_max_); + bound("resident_born", resident_faults_born_total_, + test_resident_born_min_, test_resident_born_max_); + if (!ok) out_->fatal(CALL_INFO, -1, "EccGuard test expectations failed.\n"); +} + +void EccGuard::resolveStateLazy() { + if (state_ptr_ || state_key_.empty()) return; + state_ptr_ = PipelineStateRegistry::get(state_key_); +} + +int EccGuard::resolveRegionId(uint64_t addr) const { + if (!state_ptr_) return -1; + return state_ptr_->regionIdForAddress(addr); +} + +// Region attribution: EccGuard sees physical addrs below the dTLB, but agents +// publish virtual regions. Prefer MemEvent::vAddr_ (stamped by dTLB, preserved +// by clone/makeResponse); fall back to physical when vAddr=0 (e.g. writebacks). +int EccGuard::resolveRegionIdForEvent(MemEvent* mev) const { + if (!mev || !state_ptr_) return -1; + uint64_t vaddr = mev->getVirtualAddress(); + if (vaddr != 0) { + int rid = state_ptr_->regionIdForAddress(vaddr); + if (rid >= 0) return rid; + } + return state_ptr_->regionIdForAddress(mev->getAddr()); +} + +const std::string& EccGuard::regionNameForId(int region_id) const { + static const std::string empty; + if (!state_ptr_ || region_id < 0) return empty; + if (region_id >= static_cast(state_ptr_->regions.size())) return empty; + return state_ptr_->regions[region_id].name; +} + +bool EccGuard::resolveAddrFilterBounds(uint64_t& base_out, uint64_t& len_out) const { + base_out = 0; + len_out = 0; + if (addr_filter_region_.empty()) return false; + if (!state_ptr_) return false; + for (const auto& r : state_ptr_->regions) { + if (!r.valid || r.name != addr_filter_region_) continue; + base_out = r.base; + len_out = r.size; + if (addr_filter_len_ > 0 && addr_filter_len_ < len_out) + len_out = addr_filter_len_; + return len_out > 0; + } + return false; +} + +bool EccGuard::shouldApplyPolicy(MemEvent* mev) { + if (!mev) return false; + resolveStateLazy(); + if (!applyOnResponsesOnly_) return true; + if (mev->isResponse()) return true; + if (fault_model_ != FaultModel::Campaign || addr_filter_region_.empty()) + return false; + return eventOverlapsAddrFilter(mev) && !mev->getPayload().empty(); +} + +bool EccGuard::eventOverlapsAddrFilter(MemEvent* mev) const { + if (addr_filter_region_.empty() || !mev) return true; + if (state_ptr_) { + int rid = resolveRegionIdForEvent(mev); + if (rid >= 0 && regionNameForId(rid) == addr_filter_region_) return true; + } + uint64_t fbase = 0, flen = 0; + if (!resolveAddrFilterBounds(fbase, flen)) return false; + uint64_t vaddr = mev->getVirtualAddress(); + uint64_t addr = (vaddr != 0) ? vaddr : mev->getAddr(); + uint64_t size = mev->getPayload().empty() ? 64u : mev->getPayload().size(); + uint64_t end = addr + size; + uint64_t fend = fbase + flen; + return addr < fend && end > fbase; +} + +void EccGuard::noteCampaignKernelEntry(const std::string& kernel_name) { + if (campaign_max_per_kernel_entry_ == 0) return; + // Reset entry budget on any kernel change so re-entry starts fresh. + EccModelMath::resetCampaignEntry(kernel_name, + campaign_entry_kernel_name_, + campaign_events_this_entry_); +} + +void EccGuard::requestFrameAbort() { + if (state_key_.empty()) return; + PipelineStateBase* s = + PipelineStateRegistry::getMutable(state_key_); + if (!s) return; + s->frameAbortRequested = true; + ++frames_aborted_total_; + if (stat_frames_aborted_) stat_frames_aborted_->addData(1); +} + +namespace { +// Helper: bump the registry's cumulative ECC counters so the ActionScorer +// (and any other consumer) can compute per-frame deltas. Cheap pointer chase. +void publishCumulative(const std::string& state_key, uint64_t escapes_inc, + uint64_t flips_inc) { + if (state_key.empty()) return; + PipelineStateBase* s = + PipelineStateRegistry::getMutable(state_key); + if (!s) return; + s->eccCumulativeEscapes += escapes_inc; + s->eccCumulativeFlips += flips_inc; +} + +// Bump per-frame per-kernel escape counts (argmaxed at frame close). +void publishPerFrameEscape(const std::string& state_key, + const std::string& kernel_name) { + if (state_key.empty()) return; + PipelineStateBase* s = + PipelineStateRegistry::getMutable(state_key); + if (!s) return; + s->eccPerFrameEscapesByKernel[kernel_name] += 1; +} +} // namespace + +void EccGuard::handleHighlink(SST::Event* ev) { + auto* mev = dynamic_cast(ev); + if (!mev || !shouldApplyPolicy(mev)) { + if (lowlink_) lowlink_->send(ev); else delete ev; + return; + } + + uint64_t latency_ps = applyPolicy(mev); + if (latency_ps == 0) { + lowlink_->send(ev); + } else { + selfLink_->send(static_cast(latency_ps), + new EccGuardDelayEvent(ev, /*down=*/true)); + } +} + +void EccGuard::handleLowlink(SST::Event* ev) { + auto* mev = dynamic_cast(ev); + if (!mev) { + if (highlink_) highlink_->send(ev); else delete ev; + return; + } + if (!shouldApplyPolicy(mev)) { + highlink_->send(ev); + return; + } + + uint64_t latency_ps = applyPolicy(mev); + if (latency_ps == 0) { + highlink_->send(ev); + } else { + selfLink_->send(static_cast(latency_ps), + new EccGuardDelayEvent(ev, /*down=*/false)); + } +} + +void EccGuard::handleSelf(SST::Event* ev) { + auto* pe = dynamic_cast(ev); + if (!pe) { delete ev; return; } + SST::Event* original = pe->original(); + bool down = pe->isDown(); + pe->clearOriginal(); + delete pe; + + if (down) { + if (lowlink_) lowlink_->send(original); + else delete original; + } else { + if (highlink_) highlink_->send(original); + else delete original; + } +} + +namespace { + +// Number of ECC protection words a `payload_bytes` line contains under +// `scheme`. Falls back to 1 (treat the whole payload as one "word") when the +// scheme has no word concept (e.g. NONE). +inline uint32_t numWords(uint32_t payload_bytes, EccScheme scheme) { + return EccModelMath::wordCount(payload_bytes, scheme); +} + +// Bits per ECC word for the draw. For schemes with no word concept we use +// payload bits. +inline uint32_t bitsPerWord(uint32_t payload_bytes, EccScheme scheme) { + uint32_t wb = eccWordBytes(scheme); + if (wb == 0) return payload_bytes * 8; + return wb * 8; +} + +inline bool isCorrelatedMode(EccGuard::FaultMode m) { + // SingleWord and spatial modes deposit all errors into one ECC word — + // the clustering chipkill is designed against. SingleCell is 1-bit. + switch (m) { + case EccGuard::FaultMode::SingleCell: return false; + case EccGuard::FaultMode::SingleWord: return true; + case EccGuard::FaultMode::SingleRow: return true; + case EccGuard::FaultMode::SingleColumn: return true; + case EccGuard::FaultMode::SingleBank: return true; + case EccGuard::FaultMode::SingleDevice: return true; + default: return false; + } +} + +} // namespace + +// Distribute `errs` bit-errors uniformly across chips in one word. +// For SingleDevice mode all errors land in a single randomly-chosen chip. +void EccGuard::distributeErrorsToChips( + std::vector& chip_counts, + unsigned errs, EccScheme scheme, FaultMode mode) { + unsigned nchips = chipsPerEccWord(scheme); + if (nchips == 0 || errs == 0) return; + chip_counts.assign(nchips, 0); + if (campaign_force_multi_chip_ && nchips >= 3) { + unsigned need = std::min(3u, nchips); + std::vector picks; + picks.reserve(need); + std::uniform_int_distribution cpick(0, nchips - 1); + while (picks.size() < need) { + unsigned c = cpick(stdRng_); + if (std::find(picks.begin(), picks.end(), c) == picks.end()) + picks.push_back(c); + } + unsigned per = std::max(1u, errs / need); + unsigned rem = errs; + for (size_t i = 0; i < picks.size(); ++i) { + unsigned put = (i + 1 == picks.size()) ? rem : std::min(rem, per); + chip_counts[picks[i]] = static_cast(std::min(put, 255)); + rem -= put; + } + return; + } + if (mode == FaultMode::SingleDevice) { + std::uniform_int_distribution cpick(0, nchips - 1); + unsigned c = cpick(stdRng_); + chip_counts[c] = static_cast(std::min(errs, 4)); + } else { + std::uniform_int_distribution cpick(0, nchips - 1); + for (unsigned i = 0; i < errs; ++i) { + unsigned c = cpick(stdRng_); + if (chip_counts[c] < 255) ++chip_counts[c]; + } + } +} + +EccGuard::FaultDraw EccGuard::drawFaultPoisson(uint32_t payload_bytes, + double ber, + EccScheme scheme) { + FaultDraw d; + d.mode = FaultMode::SingleCell; + if (payload_bytes == 0) return d; + + uint32_t nwords = numWords(payload_bytes, scheme); + d.per_word_errors.assign(nwords, 0u); + if (ber <= 0.0) return d; + + // Per-word Bernoulli/Poisson draws; partial final words use actual bit count. + unsigned total = 0; + bool need_chips = (scheme == EccScheme::CHIPKILL_x4); + if (need_chips) d.per_word_chip_errors.resize(nwords); + for (uint32_t w = 0; w < nwords; ++w) { + unsigned word_bits = EccModelMath::wordBits(payload_bytes, scheme, w); + if (word_bits == 0) break; + std::poisson_distribution dist(static_cast(word_bits) * ber); + unsigned errs = dist(stdRng_); + if (errs > word_bits) errs = word_bits; + d.per_word_errors[w] = errs; + total += errs; + if (need_chips && errs > 0) + distributeErrorsToChips(d.per_word_chip_errors[w], errs, scheme, d.mode); + } + d.num_errors = total; + return d; +} + +EccGuard::FaultDraw EccGuard::drawFaultJedecMix(uint32_t payload_bytes, + double event_rate, + EccScheme scheme) { + FaultDraw d; + if (payload_bytes == 0) return d; + + uint32_t nwords = numWords(payload_bytes, scheme); + d.per_word_errors.assign(nwords, 0u); + if (event_rate <= 0.0) return d; + std::bernoulli_distribution gate(std::min(event_rate, 1.0)); + if (!gate(stdRng_)) return d; + + // Choose a mode by cumulative weight. + std::uniform_real_distribution u01(0.0, 1.0); + double r = u01(stdRng_); + double acc = 0.0; + int chosen = 0; + for (int i = 0; i < kModeCount; ++i) { + acc += mode_weights_[i]; + if (r <= acc) { chosen = i; break; } + } + d.mode = static_cast(chosen); + + // Sample a bit-error count uniformly inside the mode's range; cap at payload bits. + unsigned lo = kFaultModeBitsLow[chosen]; + unsigned hi = kFaultModeBitsHigh[chosen]; + if (hi < lo) hi = lo; + std::uniform_int_distribution nbits(lo, hi); + unsigned errs = nbits(stdRng_); + unsigned cap = payload_bytes * 8; + if (cap > 0 && errs > cap) errs = cap; + d.num_errors = errs; + + // Correlated/single-word modes deposit into one random word (physical + // clustering); SingleCell scatters across words bit-by-bit. + bool need_chips = (scheme == EccScheme::CHIPKILL_x4); + if (need_chips) d.per_word_chip_errors.resize(nwords); + if (nwords > 0) { + if (isCorrelatedMode(d.mode)) { + std::uniform_int_distribution wpick(0, nwords - 1); + uint32_t w = wpick(stdRng_); + unsigned word_cap = bitsPerWord(payload_bytes, scheme); + if (word_cap > 0 && errs > word_cap) { + d.per_word_errors[w] = word_cap; + if (need_chips) + distributeErrorsToChips(d.per_word_chip_errors[w], word_cap, scheme, d.mode); + unsigned remaining = errs - word_cap; + uint32_t offset = 1; + while (remaining > 0 && offset < nwords) { + uint32_t wn = (w + offset) % nwords; + unsigned put = std::min(word_cap, remaining); + d.per_word_errors[wn] = put; + if (need_chips) + distributeErrorsToChips(d.per_word_chip_errors[wn], put, scheme, d.mode); + remaining -= put; + ++offset; + } + } else { + d.per_word_errors[w] = errs; + if (need_chips) + distributeErrorsToChips(d.per_word_chip_errors[w], errs, scheme, d.mode); + } + } else { + // Uncorrelated (SingleCell, default): scatter bit-by-bit. + std::uniform_int_distribution wpick(0, nwords - 1); + for (unsigned i = 0; i < errs; ++i) { + uint32_t w = wpick(stdRng_); + d.per_word_errors[w] += 1; + } + if (need_chips) { + for (uint32_t w = 0; w < nwords; ++w) { + if (d.per_word_errors[w] > 0) + distributeErrorsToChips(d.per_word_chip_errors[w], + d.per_word_errors[w], scheme, d.mode); + } + } + } + } + + ++per_mode_draws_[chosen]; + if (chosen == static_cast(FaultMode::SingleRow) && stat_correlated_row_) stat_correlated_row_->addData(1); + if (chosen == static_cast(FaultMode::SingleBank) && stat_correlated_bank_) stat_correlated_bank_->addData(1); + if (chosen == static_cast(FaultMode::SingleDevice) && stat_correlated_device_) stat_correlated_device_->addData(1); + + return d; +} + +// Campaign injector: at most campaign_event_budget_ events of campaign_mode_ +// into one random ECC word. Gated on campaign_target_kernel_name_ when set. +EccGuard::FaultDraw EccGuard::drawFaultCampaign(uint32_t payload_bytes, + EccScheme scheme, + const std::string& kernel_name) { + FaultDraw d; + if (payload_bytes == 0) return d; + + uint32_t nwords = numWords(payload_bytes, scheme); + d.per_word_errors.assign(nwords, 0u); + + if (campaign_event_budget_ == 0) return d; + if (campaign_events_fired_ >= campaign_event_budget_) return d; + // Addr-filtered campaign: action_queue traffic is the temporal proxy. + // ReadResp often returns after publishKernel(IDLE), so do not gate on FSM. + const bool addr_filtered = !addr_filter_region_.empty(); + if (addr_filtered && campaign_max_per_kernel_entry_ > 0 && state_ptr_) { + const int pc = state_ptr_->pipelineCycle; + if (pc != campaign_entry_pipeline_cycle_) { + campaign_entry_pipeline_cycle_ = pc; + campaign_events_this_entry_ = 0; + } + } else { + // Must run before the target gate below: off-target events update the + // last-seen kernel so re-entering the target resets the entry budget. + noteCampaignKernelEntry(kernel_name); + } + if (!addr_filtered && !campaign_target_kernel_name_.empty() + && kernel_name != campaign_target_kernel_name_) { + return d; + } + if (campaign_max_per_kernel_entry_ > 0 + && campaign_events_this_entry_ >= campaign_max_per_kernel_entry_) { + return d; + } + if (campaign_event_rate_ <= 0.0) return d; + std::bernoulli_distribution gate(std::min(campaign_event_rate_, 1.0)); + if (!gate(stdRng_)) return d; + + d.mode = campaign_mode_; + int chosen = static_cast(campaign_mode_); + + unsigned lo = kFaultModeBitsLow [chosen]; + unsigned hi = kFaultModeBitsHigh[chosen]; + if (hi < lo) hi = lo; + unsigned errs = 0; + if (campaign_errors_fixed_ > 0) { + errs = campaign_errors_fixed_; + } else { + std::uniform_int_distribution nbits(lo, hi); + errs = nbits(stdRng_); + } + unsigned cap = payload_bytes * 8; + if (cap > 0 && errs > cap) errs = cap; + d.num_errors = errs; + + bool need_chips = (scheme == EccScheme::CHIPKILL_x4); + if (need_chips) d.per_word_chip_errors.resize(nwords); + if (nwords > 0) { + if (isCorrelatedMode(d.mode)) { + std::uniform_int_distribution wpick(0, nwords - 1); + uint32_t w = wpick(stdRng_); + unsigned word_cap = bitsPerWord(payload_bytes, scheme); + if (word_cap > 0 && errs > word_cap) { + d.per_word_errors[w] = word_cap; + if (need_chips) + distributeErrorsToChips(d.per_word_chip_errors[w], word_cap, scheme, d.mode); + unsigned remaining = errs - word_cap; + uint32_t offset = 1; + while (remaining > 0 && offset < nwords) { + uint32_t wn = (w + offset) % nwords; + unsigned put = std::min(word_cap, remaining); + d.per_word_errors[wn] = put; + if (need_chips) + distributeErrorsToChips(d.per_word_chip_errors[wn], put, scheme, d.mode); + remaining -= put; + ++offset; + } + } else { + d.per_word_errors[w] = errs; + if (need_chips) + distributeErrorsToChips(d.per_word_chip_errors[w], errs, scheme, d.mode); + } + } else { + std::uniform_int_distribution wpick(0, nwords - 1); + for (unsigned i = 0; i < errs; ++i) { + d.per_word_errors[wpick(stdRng_)] += 1; + } + if (need_chips) { + for (uint32_t w = 0; w < nwords; ++w) { + if (d.per_word_errors[w] > 0) + distributeErrorsToChips(d.per_word_chip_errors[w], + d.per_word_errors[w], scheme, d.mode); + } + } + } + } + + ++campaign_events_fired_; + ++campaign_events_this_entry_; + ++per_mode_draws_[chosen]; + if (chosen == static_cast(FaultMode::SingleRow) && stat_correlated_row_) stat_correlated_row_->addData(1); + if (chosen == static_cast(FaultMode::SingleBank) && stat_correlated_bank_) stat_correlated_bank_->addData(1); + if (chosen == static_cast(FaultMode::SingleDevice) && stat_correlated_device_) stat_correlated_device_->addData(1); + return d; +} + +// Resident fault map: Poisson births in sim time (capacity x residency), +// deterministic on every access, cleared only by patrol scrub. All randomness +// from residentRng_ so equal seeds give identical faults across schemes. + +bool EccGuard::resolveResidentWindow(uint64_t& base_out, uint64_t& len_out) const { + if (resident_addr_len_ > 0) { + base_out = resident_addr_start_; + len_out = resident_addr_len_; + return true; + } + if (inject_addr_len_ > 0) { + base_out = inject_addr_start_; + len_out = inject_addr_len_; + return true; + } + base_out = 0; + len_out = 0; + return false; +} + +void EccGuard::advanceResidentClock(uint64_t now_ns) { + if (!resident_started_) return; + while (true) { + const bool have_birth = resident_rate_per_ns_ > 0.0; + const bool have_scrub = resident_scrub_interval_ns_ > 0; + const uint64_t tb = have_birth ? resident_next_birth_ns_ : UINT64_MAX; + const uint64_t ts = have_scrub ? resident_next_scrub_ns_ : UINT64_MAX; + if (std::min(tb, ts) > now_ns) break; + if (tb <= ts) { + materializeResidentFault(); + std::exponential_distribution exp_ns(resident_rate_per_ns_); + resident_next_birth_ns_ = + tb + std::max(1, static_cast(exp_ns(residentRng_))); + } else { + applyResidentScrub(); + resident_next_scrub_ns_ = ts + resident_scrub_interval_ns_; + } + } +} + +// Interleaved x4 chip layout (write + read attribution): nibble N -> chip +// N % kResidentChipsPerLine. Keep residentChipForWordBit in sync or chipkill +// vs SECDED attribution silently rots. +static constexpr unsigned kResidentNibbleBits = 4; +static constexpr unsigned kResidentChipsPerLine = 32; + +static inline unsigned residentLineBitForChip(unsigned chip, unsigned nibble_idx, + unsigned bit_in_nibble) { + return kResidentNibbleBits * (chip + kResidentChipsPerLine * nibble_idx) + + bit_in_nibble; +} + +static inline unsigned residentChipForWordBit(uint32_t bit_in_word, size_t nchips) { + return static_cast((bit_in_word / kResidentNibbleBits) % nchips); +} + +void EccGuard::addUniformBitInLine(ResidentFault& f, uint64_t line_base, + unsigned bit_in_line) { + auto& mask = f.line_bits[line_base]; // value-initialized (zeroed) on first touch + mask[bit_in_line / 8] |= static_cast(1u << (bit_in_line % 8)); +} + +void EccGuard::addChipBitsInLine(ResidentFault& f, uint64_t line_base, + unsigned chip, unsigned nbits) { + unsigned positions[16]; + unsigned n = 0; + for (unsigned j = 0; j < 4; ++j) + for (unsigned i = 0; i < kResidentNibbleBits; ++i) + positions[n++] = residentLineBitForChip(chip, j, i); + if (nbits > 16) nbits = 16; + // Partial Fisher-Yates: nbits distinct positions. + for (unsigned k = 0; k < nbits; ++k) { + std::uniform_int_distribution pick(k, 15); + std::swap(positions[k], positions[pick(residentRng_)]); + addUniformBitInLine(f, line_base, positions[k]); + } +} + +void EccGuard::materializeResidentFault() { + uint64_t wbase = 0, wlen = 0; + if (!resolveResidentWindow(wbase, wlen) || wlen == 0) return; + const uint64_t first_line = wbase & ~63ULL; + const uint64_t nlines = (wbase + wlen - first_line + 63) / 64; + + ResidentFault f; + if (resident_mode_mix_) { + std::uniform_real_distribution u01(0.0, 1.0); + double r = u01(residentRng_), acc = 0.0; + int chosen = 0; + for (int i = 0; i < kModeCount; ++i) { + acc += mode_weights_[i]; + if (r <= acc) { chosen = i; break; } + } + f.mode = static_cast(chosen); + } else { + f.mode = resident_mode_fixed_; + } + f.permanent = + std::bernoulli_distribution(resident_permanent_fraction_)(residentRng_); + + auto lineAt = [&](uint64_t idx) { return first_line + idx * 64; }; + std::uniform_int_distribution lpick(0, nlines - 1); + std::uniform_int_distribution chip_pick(0, 31); + std::uniform_int_distribution k14(1, 4); + std::uniform_int_distribution k12(1, 2); + std::uniform_int_distribution bpick(0, 511); + + const uint64_t row_lines = std::max(1, resident_row_bytes_ / 64); + const uint64_t nrows = (nlines + row_lines - 1) / row_lines; + // Subsample gigantic windows so a device fault cannot materialize an + // unbounded footprint. + constexpr uint64_t kMaxFootprintLines = 65536; + const uint64_t stride = 1 + (nlines - 1) / kMaxFootprintLines; + + switch (f.mode) { + case FaultMode::SingleCell: + addUniformBitInLine(f, lineAt(lpick(residentRng_)), bpick(residentRng_)); + break; + case FaultMode::SingleWord: { + // Multi-bit fault at one address: 2 distinct bits inside one aligned + // 64-bit region of a single line. + uint64_t lb = lineAt(lpick(residentRng_)); + std::uniform_int_distribution rpick(0, 7); + std::uniform_int_distribution bit64(0, 63); + unsigned region = rpick(residentRng_); + unsigned b1 = bit64(residentRng_), b2 = b1; + while (b2 == b1) b2 = bit64(residentRng_); + addUniformBitInLine(f, lb, region * 64 + b1); + addUniformBitInLine(f, lb, region * 64 + b2); + break; + } + case FaultMode::SingleRow: { + // One DRAM row on one x4 chip: every line of the row carries 1-4 bad + // bits confined to that chip's nibbles. + unsigned chip = chip_pick(residentRng_); + std::uniform_int_distribution rowp(0, nrows - 1); + uint64_t r0 = rowp(residentRng_) * row_lines; + for (uint64_t i = r0; i < std::min(nlines, r0 + row_lines); i += stride) + addChipBitsInLine(f, lineAt(i), chip, k14(residentRng_)); + break; + } + case FaultMode::SingleColumn: { + // Same in-row line offset across every row, one chip. + unsigned chip = chip_pick(residentRng_); + std::uniform_int_distribution colp(0, row_lines - 1); + uint64_t col = colp(residentRng_); + for (uint64_t r = 0; r < nrows; r += stride) { + uint64_t idx = r * row_lines + col; + if (idx < nlines) + addChipBitsInLine(f, lineAt(idx), chip, k12(residentRng_)); + } + break; + } + case FaultMode::SingleBank: { + // A contiguous group of rows in one bank, one chip; the fault + // manifests at one scattered line per row. + unsigned chip = chip_pick(residentRng_); + uint64_t nbanks = std::max(1, nrows / resident_bank_rows_); + std::uniform_int_distribution bankp(0, nbanks - 1); + std::uniform_int_distribution colp(0, row_lines - 1); + uint64_t r0 = bankp(residentRng_) * resident_bank_rows_; + for (uint64_t r = r0; r < std::min(nrows, r0 + resident_bank_rows_); ++r) { + uint64_t idx = r * row_lines + colp(residentRng_); + if (idx < nlines) + addChipBitsInLine(f, lineAt(idx), chip, k14(residentRng_)); + } + break; + } + case FaultMode::SingleDevice: + // Whole x4 chip: every line in the window sees 1-4 bad bits in that + // chip's nibbles. The pattern chipkill is built to absorb. + for (uint64_t i = 0, chip = chip_pick(residentRng_); i < nlines; i += stride) + addChipBitsInLine(f, lineAt(i), static_cast(chip), + k14(residentRng_)); + break; + default: + addUniformBitInLine(f, lineAt(lpick(residentRng_)), bpick(residentRng_)); + break; + } + + for (const auto& kv : f.line_bits) { + auto& m = resident_mask_[kv.first]; + for (int i = 0; i < 64; ++i) m[i] |= kv.second[i]; + } + int chosen = static_cast(f.mode); + ++per_mode_draws_[chosen]; + if (chosen == static_cast(FaultMode::SingleRow) && stat_correlated_row_) stat_correlated_row_->addData(1); + if (chosen == static_cast(FaultMode::SingleBank) && stat_correlated_bank_) stat_correlated_bank_->addData(1); + if (chosen == static_cast(FaultMode::SingleDevice) && stat_correlated_device_) stat_correlated_device_->addData(1); + ++resident_faults_born_total_; + if (stat_resident_born_) stat_resident_born_->addData(1); + if (verbose_) { + out_->output("EccGuard '%s': resident fault born: mode=%s permanent=%d " + "lines=%zu\n", + getName().c_str(), faultModeName(f.mode), + f.permanent ? 1 : 0, f.line_bits.size()); + } + resident_faults_.push_back(std::move(f)); +} + +void EccGuard::rebuildResidentMask() { + resident_mask_.clear(); + for (const auto& f : resident_faults_) { + for (const auto& kv : f.line_bits) { + bool any = false; + for (uint8_t b : kv.second) if (b) { any = true; break; } + if (!any) continue; + auto& m = resident_mask_[kv.first]; + for (int i = 0; i < 64; ++i) m[i] |= kv.second[i]; + } + } +} + +void EccGuard::applyResidentScrub() { + if (resident_mask_.empty()) return; + // Patrol scrub: correct what the code can, rewrite clears transient cells; + // permanent cells fail again. Multi-fault words beyond correction stay — + // the accumulation-before-scrub effect. Uses the uniform scheme. + const EccScheme scheme = policy_.uniform().scheme; + const uint32_t wb = eccWordBytes(scheme); + for (auto& kv : resident_mask_) { + const uint64_t lb = kv.first; + const auto& mask = kv.second; + const uint32_t words = (wb == 0) ? 1 : (64 + wb - 1) / wb; + for (uint32_t w = 0; w < words; ++w) { + const uint32_t b0 = (wb == 0) ? 0 : w * wb; + const uint32_t b1 = (wb == 0) ? 64 : std::min(64, b0 + wb); + unsigned errs = 0; + std::vector chip_errs; + if (scheme == EccScheme::CHIPKILL_x4) + chip_errs.assign(chipsPerEccWord(scheme), 0); + for (uint32_t byte = b0; byte < b1; ++byte) { + unsigned m = mask[byte]; + while (m) { + unsigned bit = static_cast(__builtin_ctz(m)); + m &= m - 1; + ++errs; + if (!chip_errs.empty()) { + unsigned bit_in_word = (byte - b0) * 8 + bit; + unsigned chip = residentChipForWordBit(bit_in_word, + chip_errs.size()); + if (chip_errs[chip] < 255) ++chip_errs[chip]; + } + } + } + if (errs == 0) continue; + EccOutcome o = chip_errs.empty() + ? classifyEccWord(errs, scheme) + : classifyEccWordChipAware(chip_errs, scheme); + if (o == EccOutcome::Correctable) { + for (auto& f : resident_faults_) { + if (f.permanent) continue; + auto it = f.line_bits.find(lb); + if (it == f.line_bits.end()) continue; + for (uint32_t byte = b0; byte < b1; ++byte) + it->second[byte] = 0; + } + } else if (o != EccOutcome::Clean) { + ++resident_scrub_due_total_; + if (stat_resident_scrub_due_) stat_resident_scrub_due_->addData(1); + } + } + } + const size_t before = resident_faults_.size(); + resident_faults_.erase( + std::remove_if(resident_faults_.begin(), resident_faults_.end(), + [](const ResidentFault& f) { + if (f.permanent) return false; + for (const auto& kv : f.line_bits) + for (uint8_t b : kv.second) + if (b) return false; + return true; + }), + resident_faults_.end()); + const size_t cleared = before - resident_faults_.size(); + if (cleared > 0) { + resident_faults_scrubbed_total_ += cleared; + if (stat_resident_scrubbed_) stat_resident_scrubbed_->addData(cleared); + if (verbose_) { + out_->output("EccGuard '%s': patrol scrub cleared %zu transient " + "fault(s); %zu alive\n", + getName().c_str(), cleared, resident_faults_.size()); + } + } + rebuildResidentMask(); +} + +EccGuard::FaultDraw EccGuard::drawFaultResident(MemEvent* mev, + uint32_t payload_bytes, + EccScheme scheme) { + FaultDraw d; + if (payload_bytes == 0) return d; + uint32_t nwords = numWords(payload_bytes, scheme); + d.per_word_errors.assign(nwords, 0u); + if (resident_mask_.empty()) return d; + + // Same address-space preference as the window filters: the preserved + // virtual address when present, else the physical/SST address. + uint64_t a = mev->getVirtualAddress(); + if (a == 0) a = mev->getAddr(); + + const uint32_t wb = eccWordBytes(scheme); + const bool need_chips = (scheme == EccScheme::CHIPKILL_x4); + if (need_chips) d.per_word_chip_errors.resize(nwords); + + for (uint64_t lb = a & ~63ULL; lb < a + payload_bytes; lb += 64) { + auto it = resident_mask_.find(lb); + if (it == resident_mask_.end()) continue; + const auto& mask = it->second; + for (unsigned byte = 0; byte < 64; ++byte) { + unsigned m = mask[byte]; + if (!m) continue; + const uint64_t abs_byte = lb + byte; + if (abs_byte < a || abs_byte >= a + payload_bytes) continue; + const uint32_t rel_byte = static_cast(abs_byte - a); + const uint32_t w = (wb == 0) ? 0 + : std::min(rel_byte / wb, nwords - 1); + while (m) { + const unsigned bit = static_cast(__builtin_ctz(m)); + m &= m - 1; + d.exact_bits.push_back(rel_byte * 8 + bit); + d.per_word_errors[w] += 1; + d.num_errors += 1; + if (need_chips) { + auto& cc = d.per_word_chip_errors[w]; + if (cc.empty()) cc.assign(chipsPerEccWord(scheme), 0); + const uint32_t bit_in_word = (rel_byte - w * wb) * 8 + bit; + const unsigned chip = residentChipForWordBit(bit_in_word, + cc.size()); + if (cc[chip] < 255) ++cc[chip]; + } + } + } + } + return d; +} + +unsigned EccGuard::flipExactBitsInWord(MemEvent* mev, uint32_t word_index, + EccScheme scheme, + const std::vector& exact_bits, + unsigned& high_flips, unsigned& low_flips) { + auto& payload = mev->getPayload(); + if (payload.empty() || exact_bits.empty()) return 0; + const uint32_t total_bits = static_cast(payload.size()) * 8; + const uint32_t wb = eccWordBytes(scheme); + const uint32_t start_bit = (wb == 0) ? 0 : word_index * wb * 8; + const uint32_t end_bit = (wb == 0) ? total_bits + : std::min(total_bits, start_bit + wb * 8); + unsigned elem_bytes = dtypeBytes(payload_dtype_); + if (elem_bytes == 0) elem_bytes = 1; + + unsigned flipped = 0; + for (uint32_t b : exact_bits) { + if (b < start_bit || b >= end_bit) continue; + const uint32_t byte = b / 8u, bit = b % 8u; + payload[byte] ^= static_cast(1u << bit); + bool hi = false; + if (payload_dtype_ != PayloadDtype::Bytes) { + hi = isHighBlastBit(payload_dtype_, (byte % elem_bytes) * 8u + bit); + } + if (hi) ++high_flips; else ++low_flips; + ++flipped; + } + return flipped; +} + +void EccGuard::warnIfBerExceedsTightBound(double ber, const char* origin) { + if (ber <= kEccBerTightUpperBound) return; + // Memoize so repeated BER values don't spam the log. + uint64_t key = 0; + std::memcpy(&key, &ber, sizeof(key)); + if (!ber_warned_.insert(key).second) return; + if (out_) { + out_->output( + "EccGuard '%s': WARNING %s BER=%.3e exceeds the per-word " + "single-bit approximation's tight bound (%.1e). The per-word " + "draws still classify each event correctly, but the " + "Correctable/DUE/Escape proportions are no longer provably " + "tight to within ~1%% of an exact Binomial decode. See " + "eccScheme.h::kEccBerTightUpperBound for the derivation.\n", + getName().c_str(), origin, ber, kEccBerTightUpperBound); + } +} + +uint64_t EccGuard::applyPolicy(MemEvent* mev) { + if (!state_ptr_) resolveStateLazy(); + + std::string kernel_name; + if (state_ptr_) kernel_name = state_ptr_->currentKernelName; + + if (!addr_filter_region_.empty() && !eventOverlapsAddrFilter(mev)) { + if (stat_total_) stat_total_->addData(1); + if (stat_clean_) stat_clean_->addData(1); + return 0; + } + + // Raw inject window (no region registry): confine to [start, start+len). + // Prefer preserved vAddr; fall back to physical (e.g. balar H2D path). + if (inject_addr_len_ > 0) { + uint64_t a = mev->getVirtualAddress(); + if (a == 0) a = mev->getAddr(); + uint64_t sz = mev->getPayload().empty() ? 64u : mev->getPayload().size(); + if (a + sz <= inject_addr_start_ || + a >= inject_addr_start_ + inject_addr_len_) { + if (stat_total_) stat_total_->addData(1); + if (stat_clean_) stat_clean_->addData(1); + return 0; + } + } + + int region_id = resolveRegionIdForEvent(mev); + const std::string& region_name = regionNameForId(region_id); + + const EccPolicyEntry& entry = policy_.effectiveFor(kernel_name, region_name); + + auto& kernel_bucket = per_kernel_[kernel_name]; + auto& region_bucket = per_kernel_region_[std::make_pair(kernel_name, region_name)]; + + auto countClean = [&]() { + if (stat_total_) stat_total_->addData(1); + if (stat_clean_) stat_clean_->addData(1); + kernel_bucket.clean += 1; + region_bucket.clean += 1; + }; + + if (entry.ber <= 0.0 && fault_event_rate_ <= 0.0 + && entry.scheme == EccScheme::NONE + && fault_model_ != FaultModel::Campaign + && fault_model_ != FaultModel::Resident) { + countClean(); + return 0; + } + + auto& payload = mev->getPayload(); + if (payload.empty()) { + countClean(); + return 0; + } + + uint32_t payload_bytes = static_cast(payload.size()); + + FaultDraw draw; + if (fault_model_ == FaultModel::JedecMix) { + // Documented priority (see 'fault_event_rate' param docs): an explicit + // (or FIT-derived) event rate overrides BER; otherwise approximate the + // per-access event probability as BER * payload_bits (Poisson approx). + double rate = EccModelMath::jedecEventRate( + fault_event_rate_, entry.ber, payload_bytes); + draw = drawFaultJedecMix(payload_bytes, rate, entry.scheme); + } else if (fault_model_ == FaultModel::Campaign) { + draw = drawFaultCampaign(payload_bytes, entry.scheme, kernel_name); + } else if (fault_model_ == FaultModel::Resident) { + advanceResidentClock(getCurrentSimTimeNano()); + draw = drawFaultResident(mev, payload_bytes, entry.scheme); + } else { + draw = drawFaultPoisson(payload_bytes, entry.ber, entry.scheme); + } + + EccLineOutcome line = draw.per_word_chip_errors.empty() + ? aggregateLineOutcome(draw.per_word_errors, entry.scheme) + : aggregateLineOutcomeChipAware(draw.per_word_errors, + draw.per_word_chip_errors, entry.scheme); + EccOutcome outcome = line.outcome; + + // DUE response shared by DUE and Escape line outcomes (hardware fires per + // word). drop_frame aborts; latency_only forwards poison by flipping the + // DUE words' drawn error bits into the payload. + auto handleDueWords = [&]() { + if (line.due_words.empty()) return; + if (due_action_ == DueAction::DropFrame) { + requestFrameAbort(); + return; + } + unsigned hi = 0, lo = 0, flips = 0; + for (uint32_t w : line.due_words) { + flips += draw.exact_bits.empty() + ? flipBitsInWord(mev, w, entry.scheme, + draw.per_word_errors[w], hi, lo) + : flipExactBitsInWord(mev, w, entry.scheme, + draw.exact_bits, hi, lo); + } + due_poison_flips_total_ += flips; + if (stat_due_poisoned_) stat_due_poisoned_->addData(flips); + publishCumulative(state_key_, /*escapes*/0, /*flips*/flips); + }; + + uint64_t latency_ps = 0; + bool high_blast_flip = false; + switch (outcome) { + case EccOutcome::Clean: + latency_ps = 0; + break; + case EccOutcome::Correctable: + latency_ps = entry.correctable_latency_ps; + break; + case EccOutcome::DetectableUncorrectable: + latency_ps = entry.due_latency_ps; + handleDueWords(); + break; + case EccOutcome::SilentEscape: { + latency_ps = entry.escape_latency_ps; + // Corrupt only words whose ECC decode escaped (per_word_errors[w] bits). + // Correctable words leak nothing; DUE words get the DUE response below. + unsigned hi = 0, lo = 0, flips = 0; + for (uint32_t w : line.escape_words) { + flips += draw.exact_bits.empty() + ? flipBitsInWord(mev, w, entry.scheme, + draw.per_word_errors[w], hi, lo) + : flipExactBitsInWord(mev, w, entry.scheme, + draw.exact_bits, hi, lo); + } + escape_high_blast_total_ += hi; + escape_low_blast_total_ += lo; + if (hi && stat_escape_high_blast_) stat_escape_high_blast_->addData(hi); + if (lo && stat_escape_low_blast_) stat_escape_low_blast_->addData(lo); + high_blast_flip = (hi > 0); + publishCumulative(state_key_, /*escapes*/1, /*flips*/flips); + publishPerFrameEscape(state_key_, kernel_name); + handleDueWords(); + if (!line.due_words.empty() && entry.due_latency_ps > latency_ps) + latency_ps = entry.due_latency_ps; + break; + } + } + + if (stat_total_) stat_total_->addData(1); + switch (outcome) { + case EccOutcome::Clean: + if (stat_clean_) stat_clean_->addData(1); + kernel_bucket.clean += 1; + region_bucket.clean += 1; + break; + case EccOutcome::Correctable: + if (stat_correctable_) stat_correctable_->addData(1); + kernel_bucket.correctable += 1; + region_bucket.correctable += 1; + break; + case EccOutcome::DetectableUncorrectable: + if (stat_due_) stat_due_->addData(1); + kernel_bucket.due += 1; + region_bucket.due += 1; + break; + case EccOutcome::SilentEscape: + if (stat_escape_) stat_escape_->addData(1); + kernel_bucket.escape += 1; + region_bucket.escape += 1; + break; + } + if (latency_ps > 0) { + if (stat_latency_) stat_latency_->addData(latency_ps); + kernel_bucket.latency_ps += latency_ps; + region_bucket.latency_ps += latency_ps; + } + if (verbose_ && (outcome != EccOutcome::Clean || high_blast_flip)) { + out_->output("EccGuard '%s': addr=0x%llx vaddr=0x%llx kernel=%s region=%s mode=%s " + "errors=%u (escape_bits=%u over %zu words) outcome=%s " + "+%" PRIu64 " ps\n", + getName().c_str(), + (unsigned long long)mev->getAddr(), + (unsigned long long)mev->getVirtualAddress(), + kernel_name.empty() ? "UNKNOWN" : kernel_name.c_str(), + region_name.empty() ? "unlabeled" : region_name.c_str(), + faultModeName(draw.mode), + draw.num_errors, line.escape_bits, + draw.per_word_errors.size(), + eccOutcomeName(outcome), latency_ps); + } + + return latency_ps; +} + +unsigned EccGuard::flipBitsInWord(MemEvent* mev, uint32_t word_index, + EccScheme scheme, unsigned nbits, + unsigned& high_flips, unsigned& low_flips) { + auto& payload = mev->getPayload(); + if (payload.empty() || nbits == 0) return 0; + uint32_t total_bytes = static_cast(payload.size()); + uint32_t wb = eccWordBytes(scheme); + uint32_t start = (wb == 0) ? 0 : word_index * wb; + uint32_t end = (wb == 0) ? total_bytes + : std::min(total_bytes, start + wb); + if (start >= end) return 0; + uint32_t span_bits = (end - start) * 8; + if (nbits > span_bits) nbits = span_bits; + + unsigned elem_bytes = dtypeBytes(payload_dtype_); + if (elem_bytes == 0) elem_bytes = 1; + + // Sample distinct bit positions inside the word span so repeated flips + // cannot XOR-cancel. nbits << span_bits in practice, so rejection + // sampling terminates quickly; the cap above guarantees termination. + std::set used; + unsigned flipped = 0; + while (flipped < nbits) { + uint32_t bit = rng_.generateNextUInt32() % span_bits; + if (!used.insert(bit).second) continue; + uint32_t global_byte = start + bit / 8u; + uint32_t bit_in_byte = bit % 8u; + payload[global_byte] ^= static_cast(1u << bit_in_byte); + bool hi = false; + if (payload_dtype_ != PayloadDtype::Bytes) { + // Elements are little-endian and aligned to the payload start; + // bit 0 of an element is its LSB (byte 0 of bf16 = mantissa low, + // byte 1 = sign + exponent high). + uint32_t bit_in_elem = (global_byte % elem_bytes) * 8u + bit_in_byte; + hi = isHighBlastBit(payload_dtype_, bit_in_elem); + } + if (hi) ++high_flips; else ++low_flips; + ++flipped; + } + return flipped; +} diff --git a/src/sst/elements/carcosa/components/eccGuard.h b/src/sst/elements/carcosa/components/eccGuard.h new file mode 100644 index 0000000000..5c987f38cf --- /dev/null +++ b/src/sst/elements/carcosa/components/eccGuard.h @@ -0,0 +1,405 @@ +// 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 SST_ELEMENTS_CARCOSA_ECC_GUARD_H +#define SST_ELEMENTS_CARCOSA_ECC_GUARD_H + +// Inline memHierarchy ECC boundary: classify outcomes, apply scrub latency. +// jedec_mix weights default to Sridharan ASPLOS'15 Table 4 (cross-check: +// Schroeder SIGMETRICS'09). due_action=drop_frame sets frameAbortRequested. + +#include "sst/elements/carcosa/components/eccPolicy.h" +#include "sst/elements/carcosa/components/eccScheme.h" +#include "sst/elements/carcosa/components/pipelineStateRegistry.h" +#include "sst/elements/memHierarchy/memEvent.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace SST { +namespace Carcosa { + +// Self-link carrier: wraps the original MemEvent + direction so the handler +// can re-emit on the correct outgoing link after the scheduled latency. +class EccGuardDelayEvent : public SST::Event { +public: + EccGuardDelayEvent() : SST::Event(), original_(nullptr), down_(true) {} + EccGuardDelayEvent(SST::Event* original, bool down) + : SST::Event(), original_(original), down_(down) {} + ~EccGuardDelayEvent() override = default; + + SST::Event* original() const { return original_; } + bool isDown() const { return down_; } + void clearOriginal() { original_ = nullptr; } + + EccGuardDelayEvent* clone() override { + return new EccGuardDelayEvent(original_, down_); + } + +private: + SST::Event* original_; + bool down_; + + void serialize_order(SST::Core::Serialization::serializer& ser) override { + Event::serialize_order(ser); + SST_SER(original_); + SST_SER(down_); + } + ImplementSerializable(SST::Carcosa::EccGuardDelayEvent); +}; + +class EccGuard : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT( + EccGuard, + "carcosa", + "EccGuard", + SST_ELI_ELEMENT_VERSION(1, 0, 0), + "Inline ECC boundary: classifies access outcomes (clean/correctable/DUE/escape) " + "under a configurable scheme and applies kernel-aware scrub latencies via a " + "self-link. Reads currentKernel from PipelineStateRegistry.", + COMPONENT_CATEGORY_MEMORY) + + SST_ELI_DOCUMENT_PARAMS( + {"verbose", "Enable verbose output.", "false"}, + {"state_key", "PipelineStateRegistry key whose currentKernel field is consulted to pick a per-kernel policy. Empty disables kernel-aware lookup.", ""}, + {"ecc_scheme", "Uniform fallback ECC scheme: 'none', 'secded', or 'chipkill'.", "none"}, + {"ber", "Uniform fallback per-bit error rate per access.", "0.0"}, + {"correctable_latency_ps", "Uniform fallback scrub latency (ps) for correctable outcomes.", "0"}, + {"due_latency_ps", "Uniform fallback latency (ps) for detected-uncorrectable outcomes.", "0"}, + {"escape_latency_ps", "Uniform fallback latency (ps) for silent-escape outcomes (typically 0).", "0"}, + {"kernel_policy", "CSV of per-kernel/per-region overrides; entries 'KERNEL:scheme:ber:c_ps:d_ps:e_ps' or 'KERNEL@REGION:...' or '*@REGION:...'. Resolution precedence: (kernel,region) > region > kernel > uniform.", ""}, + {"apply_on_responses_only", "If true, only apply ECC modeling to MemEvent responses (read returns). Writes pass through.", "true"}, + {"fault_model", "Per-event fault sampler: 'poisson' (per-bit Bernoulli/Poisson on payload), 'jedec_mix' (mixture of single-cell/word/row/column/bank/device events with mixture weights derived from Sridharan ASPLOS'15 Table 4 plus Schroeder SIGMETRICS'09), or 'campaign' (deterministic fault budget keyed to a target VLA kernel; see campaign_* params).", "poisson"}, + {"campaign_target_kernel", "Campaign mode only: workload-supplied kernel name string (e.g. 'KV_CACHE_ATTN', 'ACTUATE') into which the entire fault budget is injected. Empty / 'any' / '*' targets every access (uniform campaign).", "any"}, + {"campaign_mode", "Campaign mode only: which fault mode to inject ('cell','word','row','column','bank','device').", "row"}, + {"campaign_event_budget", "Campaign mode only: total number of fault events to inject across the run; once exhausted the guard reverts to clean classification on every subsequent access. 0 disables campaign injection regardless of fault_model.", "0"}, + {"campaign_event_rate", "Campaign mode only: per-eligible-access probability of firing one campaign event. Eligible accesses are those whose currentKernel matches campaign_target_kernel.", "0.0"}, + {"campaign_max_events_per_kernel_entry", "Campaign mode only: cap fault events per contiguous visit to campaign_target_kernel (e.g. 1 per ACTUATE frame). 0 disables the per-entry cap.", "0"}, + {"campaign_errors_fixed", "Campaign mode only: if >0, inject exactly this many bit errors per event instead of sampling from the mode's [lo,hi] span.", "0"}, + {"campaign_force_multi_chip", "Campaign mode only: when true (or campaign_mode='multi_chip'), distribute chipkill errors across at least three x4 chips.", "false"}, + {"addr_filter_region", "If set (e.g. 'action_queue'), only inject faults on MemEvents whose virtual address overlaps that published region. Empty disables filtering.", ""}, + {"addr_filter_len", "When addr_filter_region is set, limit injection to the first N bytes of that region (0 = entire region).", "0"}, + {"inject_addr_start", "Raw injection-window base (physical/SST address). When inject_addr_len>0, inject ONLY on events overlapping [inject_addr_start, inject_addr_start+inject_addr_len). Needs no published region, unlike addr_filter_region.", "0"}, + {"inject_addr_len", "Length in bytes of the raw injection window (see inject_addr_start). 0 disables raw-window confinement.", "0"}, + {"fault_mode_weights", "JEDEC mixture weights as a CSV 'cell:word:row:column:bank:device'; need not sum to 1 (normalized internally). Defaults to '0.55:0.15:0.10:0.08:0.07:0.05'.", ""}, + {"fault_event_rate", "When fault_model='jedec_mix', per-access probability that a correlated fault event occurs (overrides BER for the mode draw). 0.0 falls back to BER * payload_bits as the event rate (Poisson approximation).", "0.0"}, + {"payload_dtype", "Data-type-aware flip target for the silent-escape path: 'bytes' (current behavior), 'bf16', 'fp8', 'int8'. High-blast bits (sign/high exponent) are tracked separately in escape_high_blast vs escape_low_blast.", "bytes"}, + {"due_action", "How to model a Detectable-Uncorrectable Error: 'latency_only' (add latency and forward the poisoned payload -- the DUE words' drawn error bits are flipped into the data, since an uncorrectable error cannot yield the correct data), or 'drop_frame' (set PipelineStateBase::frameAbortRequested so the VLA agents jump to ACTUATE and increment frames_dropped; payload is not consumed).", "latency_only"}, + {"resident_addr_start", "Resident model: base of the address window the fault map covers. Falls back to inject_addr_start/inject_addr_len when resident_addr_len==0. fault_model='resident' requires a non-empty window (bounded fault-map memory).", "0"}, + {"resident_addr_len", "Resident model: byte length of the fault-map window (see resident_addr_start).", "0"}, + {"resident_faults_at_start", "Resident model: number of faults materialized at t=0 (deterministic paired-comparison campaigns).", "0"}, + {"resident_fault_rate_per_ms", "Resident model: Poisson fault-arrival rate in faults per simulated millisecond. When 0, derived from fit_per_mbit_per_hour * dram_capacity_mb * resident_time_acceleration.", "0.0"}, + {"resident_time_acceleration", "Resident model: multiplier applied to the FIT-derived arrival rate so hour-scale field rates produce events in ms-scale simulations. Report alongside results.", "1.0"}, + {"resident_scrub_interval_us", "Resident model: patrol-scrub period in simulated microseconds. Each scrub clears transient faults whose words are correctable under the uniform scheme; accumulated (multi-fault) words and permanent faults survive. 0 disables scrub.", "0.0"}, + {"resident_permanent_fraction", "Resident model: probability a new fault is permanent (survives scrub). Field studies (Sridharan ASPLOS'15) find a large permanent share; sweep this axis.", "0.3"}, + {"resident_mode", "Resident model: fault mode for new faults: 'mix' (sample fault_mode_weights) or a fixed mode name ('cell','word','row','column','bank','device').", "mix"}, + {"resident_row_bytes", "Resident model: DRAM row size used for row/column/bank fault footprints.", "8192"}, + {"resident_bank_rows", "Resident model: number of rows a bank fault spans.", "8"}, + {"fit_per_mbit_per_hour", "Optional FIT calibration: when >0 and fault_event_rate==0, the guard derives event_rate = (FIT/Mbit/h) * dram_capacity_mb * (sim_time_per_event_ns/3.6e15). Reported in setup() so reviewers see a single FIT number. For fault_model='resident' the same FIT feeds the time-based arrival rate instead.", "0.0"}, + {"dram_capacity_mb", "Companion to fit_per_mbit_per_hour. DRAM capacity in MiB used for FIT->event_rate derivation.", "1024"}, + {"sim_time_per_event_ns", "Companion to fit_per_mbit_per_hour. Wall-clock interval in nanoseconds that one simulated MemEvent represents (e.g. average DRAM access latency).", "100"}, + {"seed", "RNG seed (0 = pick a default).", "0"}, + {"test_total_min", "Test branch hook: minimum total outcomes (-1 disables).", "-1"}, + {"test_total_max", "Test branch hook: maximum total outcomes (-1 disables).", "-1"}, + {"test_clean_min", "Test branch hook: minimum clean outcomes.", "-1"}, + {"test_clean_max", "Test branch hook: maximum clean outcomes.", "-1"}, + {"test_correctable_min", "Test branch hook: minimum correctable outcomes.", "-1"}, + {"test_correctable_max", "Test branch hook: maximum correctable outcomes.", "-1"}, + {"test_due_min", "Test branch hook: minimum DUE outcomes.", "-1"}, + {"test_due_max", "Test branch hook: maximum DUE outcomes.", "-1"}, + {"test_escape_min", "Test branch hook: minimum escape outcomes.", "-1"}, + {"test_escape_max", "Test branch hook: maximum escape outcomes.", "-1"}, + {"test_resident_born_min", "Test branch hook: minimum resident births.", "-1"}, + {"test_resident_born_max", "Test branch hook: maximum resident births.", "-1"}) + + SST_ELI_DOCUMENT_PORTS( + {"highlink", "Link toward the directory/cache side", {"memHierarchy.MemEventBase"}}, + {"lowlink", "Link toward the memory controller side", {"memHierarchy.MemEventBase"}}) + + SST_ELI_DOCUMENT_STATISTICS( + {"events_total", "Total events that traversed the guard.", "count", 1}, + {"events_clean", "Events classified clean.", "count", 1}, + {"events_correctable", "Events classified correctable.", "count", 1}, + {"events_due", "Events classified DUE.", "count", 1}, + {"events_escape", "Events classified silent escape.", "count", 1}, + {"latency_added_ps", "Total ps of ECC scrub/DUE latency added.", "ps", 1}, + {"events_correlated_row", "JEDEC mix: faults landing in a single DRAM row.", "count", 1}, + {"events_correlated_bank","JEDEC mix: faults landing in a single DRAM bank.", "count", 1}, + {"events_correlated_device","JEDEC mix: faults attributable to a single device.", "count", 1}, + {"escape_high_blast", "Silent escapes whose flipped bit hit a high-blast position (sign / high exponent).", "count", 1}, + {"escape_low_blast", "Silent escapes whose flipped bit hit a low-blast position (mantissa LSBs).", "count", 1}, + {"due_poisoned_bits", "Bits flipped into forwarded payloads by DUE words under due_action='latency_only' (poison forwarding).", "count", 1}, + {"frames_aborted", "Frames the guard requested be aborted via due_action='drop_frame'.", "count", 1}, + {"resident_faults_born", "Resident model: faults materialized into the fault map.", "count", 1}, + {"resident_faults_scrubbed", "Resident model: transient faults fully cleared by patrol scrub.", "count", 1}, + {"resident_scrub_due", "Resident model: words a scrub pass found uncorrectable (accumulation-before-scrub).", "count", 1}) + + EccGuard(SST::ComponentId_t id, SST::Params& params); + ~EccGuard() override; + + void setup() override; + void init(unsigned phase) override; + void complete(unsigned phase) override; + void finish() override; + + enum class FaultModel : uint8_t { Poisson, JedecMix, Campaign, Resident }; + enum class PayloadDtype : uint8_t { Bytes, Bf16, Fp8, Int8 }; + enum class DueAction : uint8_t { LatencyOnly, DropFrame }; + + enum class FaultMode : uint8_t { + SingleCell = 0, + SingleWord = 1, + SingleRow = 2, + SingleColumn = 3, + SingleBank = 4, + SingleDevice = 5, + Count + }; + + // One fault sample: per_word_errors[i] = bit errors in word i; num_errors + // is their sum. NONE schemes use a single entry. Caller sizes the vector + // before drawFault*, or relies on the draw to resize. + struct FaultDraw { + unsigned num_errors = 0; + FaultMode mode = FaultMode::SingleCell; + std::vector per_word_errors; + // Per-word per-chip error counts for chip-aware chipkill + // classification. Outer index = word, inner index = chip within word. + // Only populated when scheme == CHIPKILL_x4. + std::vector> per_word_chip_errors; + // Payload-relative faulty-cell bit positions (resident model). When + // non-empty, escape/DUE paths flip exactly these bits. + std::vector exact_bits; + }; + +private: + void handleHighlink(SST::Event* ev); + void handleLowlink(SST::Event* ev); + void handleSelf(SST::Event* ev); + + uint64_t applyPolicy(SST::MemHierarchy::MemEvent* mev); + // Flip nbits distinct random bits in ECC word word_index (no XOR-cancel). + // Classifies high/low blast for payload_dtype_. Returns bits actually flipped. + unsigned flipBitsInWord(SST::MemHierarchy::MemEvent* mev, + uint32_t word_index, EccScheme scheme, + unsigned nbits, + unsigned& high_flips, unsigned& low_flips); + FaultDraw drawFaultPoisson(uint32_t payload_bytes, double ber, EccScheme scheme); + FaultDraw drawFaultJedecMix(uint32_t payload_bytes, double event_rate, EccScheme scheme); + void distributeErrorsToChips(std::vector& chip_counts, + unsigned errs, EccScheme scheme, FaultMode mode); + // Campaign injection: deterministic budget gated on (current kernel == + // campaign_target_kernel_) and per-access probability + // campaign_event_rate_; see eccGuard.h docs. + FaultDraw drawFaultCampaign(uint32_t payload_bytes, EccScheme scheme, + const std::string& kernel_name); + + // Resident fault map: physical cells with birth time + permanent flag, + // Poisson in sim time, same corruption until scrub. Drawn from residentRng_ + // so paired A/B runs with the same seed see identical physical faults. + struct ResidentFault { + FaultMode mode = FaultMode::SingleCell; + bool permanent = false; + // line base address -> bitmask over the line's 512 bits. + std::map> line_bits; + }; + + bool resolveResidentWindow(uint64_t& base_out, uint64_t& len_out) const; + // Lazily advance the fault-birth / scrub processes to `now_ns`, + // processing births and scrub epochs in chronological order. + void advanceResidentClock(uint64_t now_ns); + void materializeResidentFault(); + void applyResidentScrub(); + void rebuildResidentMask(); + // Add `nbits` distinct faulty bits owned by x4 chip `chip` inside the + // line at `line_base` (chip c owns bit-nibbles {c, c+32, c+64, c+96}). + void addChipBitsInLine(ResidentFault& f, uint64_t line_base, + unsigned chip, unsigned nbits); + void addUniformBitInLine(ResidentFault& f, uint64_t line_base, + unsigned bit_in_line); + FaultDraw drawFaultResident(SST::MemHierarchy::MemEvent* mev, + uint32_t payload_bytes, EccScheme scheme); + // Flip the subset of draw.exact_bits that falls inside ECC word + // `word_index` (payload-relative), classifying blast per bit. + unsigned flipExactBitsInWord(SST::MemHierarchy::MemEvent* mev, + uint32_t word_index, EccScheme scheme, + const std::vector& exact_bits, + unsigned& high_flips, unsigned& low_flips); + + // Emit a one-shot warning whenever a policy entry's BER exceeds the + // documented tight-approximation bound (see kEccBerTightUpperBound in + // eccScheme.h). Tracks already-warned BER values to avoid log spam. + void warnIfBerExceedsTightBound(double ber, const char* origin); + + void resolveStateLazy(); + int resolveRegionId(uint64_t addr) const; + // Prefer the original virtual address carried on the MemEvent (stamped by + // the dTLB wrapper) so we match the agent-published virtual regions; fall + // back to the physical address. + int resolveRegionIdForEvent(SST::MemHierarchy::MemEvent* mev) const; + const std::string& regionNameForId(int region_id) const; + bool resolveAddrFilterBounds(uint64_t& base_out, uint64_t& len_out) const; + bool eventOverlapsAddrFilter(SST::MemHierarchy::MemEvent* mev) const; + /** Campaign + addr_filter: also inject on CPU writes (payload present). */ + bool shouldApplyPolicy(SST::MemHierarchy::MemEvent* mev); + void noteCampaignKernelEntry(const std::string& kernel_name); + + void requestFrameAbort(); + + SST::Output* out_ = nullptr; + bool verbose_ = false; + + SST::Link* highlink_ = nullptr; + SST::Link* lowlink_ = nullptr; + SST::Link* selfLink_ = nullptr; + + EccPolicyTable policy_; + bool applyOnResponsesOnly_ = true; + + FaultModel fault_model_ = FaultModel::Poisson; + PayloadDtype payload_dtype_ = PayloadDtype::Bytes; + DueAction due_action_ = DueAction::LatencyOnly; + + // JEDEC mixture weights, normalized in ctor; size == FaultMode::Count. + double mode_weights_[static_cast(FaultMode::Count)] = {}; + double fault_event_rate_ = 0.0; + + // Campaign params (fault_model_ == Campaign). Empty target kernel = any; + // campaign_event_budget_ counts down; depleted => Clean forever after. + std::string campaign_target_kernel_name_; + FaultMode campaign_mode_ = FaultMode::SingleRow; + uint64_t campaign_event_budget_ = 0; + double campaign_event_rate_ = 0.0; + uint64_t campaign_events_fired_ = 0; + uint64_t campaign_max_per_kernel_entry_ = 0; + uint64_t campaign_events_this_entry_ = 0; + // Sentinel kernel name for "no entry observed yet" so the first + // noteCampaignKernelEntry call always fires. + std::string campaign_entry_kernel_name_ = "\x01__none__"; + /** When addr_filter_region_ is set, cap per pipeline frame (async ReadResp). */ + int campaign_entry_pipeline_cycle_ = -1; + unsigned campaign_errors_fixed_ = 0; + bool campaign_force_multi_chip_ = false; + + std::string addr_filter_region_; + uint64_t addr_filter_len_ = 0; + // Raw inject window: when inject_addr_len_ > 0, only events overlapping + // [inject_addr_start_, +len). For transports with no region registry. + uint64_t inject_addr_start_ = 0; + uint64_t inject_addr_len_ = 0; + + // Resident fault-map state (fault_model='resident'). + uint64_t resident_addr_start_ = 0; + uint64_t resident_addr_len_ = 0; + uint64_t resident_faults_at_start_ = 0; + double resident_rate_per_ns_ = 0.0; + double resident_time_accel_ = 1.0; + uint64_t resident_scrub_interval_ns_ = 0; + double resident_permanent_fraction_ = 0.3; + bool resident_mode_mix_ = true; + FaultMode resident_mode_fixed_ = FaultMode::SingleCell; + uint64_t resident_row_bytes_ = 8192; + uint64_t resident_bank_rows_ = 8; + + std::vector resident_faults_; + // Merged (OR of live faults) fault mask, rebuilt on birth/scrub; the + // access path reads only this. + std::map> resident_mask_; + uint64_t resident_next_birth_ns_ = 0; + uint64_t resident_next_scrub_ns_ = 0; + bool resident_started_ = false; + std::mt19937_64 residentRng_; + + uint64_t resident_faults_born_total_ = 0; + uint64_t resident_faults_scrubbed_total_ = 0; + uint64_t resident_scrub_due_total_ = 0; + + Statistics::Statistic* stat_resident_born_ = nullptr; + Statistics::Statistic* stat_resident_scrubbed_ = nullptr; + Statistics::Statistic* stat_resident_scrub_due_ = nullptr; + + std::string state_key_; + const PipelineStateBase* state_ptr_ = nullptr; + + SST::RNG::MersenneRNG rng_; + std::mt19937 stdRng_; + + Statistics::Statistic* stat_total_ = nullptr; + Statistics::Statistic* stat_clean_ = nullptr; + Statistics::Statistic* stat_correctable_ = nullptr; + Statistics::Statistic* stat_due_ = nullptr; + Statistics::Statistic* stat_escape_ = nullptr; + Statistics::Statistic* stat_latency_ = nullptr; + Statistics::Statistic* stat_correlated_row_ = nullptr; + Statistics::Statistic* stat_correlated_bank_ = nullptr; + Statistics::Statistic* stat_correlated_device_ = nullptr; + Statistics::Statistic* stat_escape_high_blast_ = nullptr; + Statistics::Statistic* stat_escape_low_blast_ = nullptr; + Statistics::Statistic* stat_due_poisoned_ = nullptr; + Statistics::Statistic* stat_frames_aborted_ = nullptr; + + struct OutcomeCounters { + uint64_t clean = 0; + uint64_t correctable = 0; + uint64_t due = 0; + uint64_t escape = 0; + uint64_t latency_ps = 0; + }; + // Per-kernel counters keyed by the workload-supplied kernel name. The + // empty string is the catch-all for "no FSM publisher / unknown kernel". + std::map per_kernel_; + + // (kernel_name, region_name) -> counters. Region "" means "address + // didn't fall in any published region" (i.e. unlabeled DRAM); kernel + // "" means "no FSM publisher yet". + std::map, OutcomeCounters> per_kernel_region_; + + // Dedicated-branch test oracles. A negative value disables each bound. + int64_t test_total_min_ = -1, test_total_max_ = -1; + int64_t test_clean_min_ = -1, test_clean_max_ = -1; + int64_t test_correctable_min_ = -1, test_correctable_max_ = -1; + int64_t test_due_min_ = -1, test_due_max_ = -1; + int64_t test_escape_min_ = -1, test_escape_max_ = -1; + int64_t test_resident_born_min_ = -1, test_resident_born_max_ = -1; + + // Fault-mode draw counters; written every time fault_model_=JedecMix fires. + uint64_t per_mode_draws_[static_cast(FaultMode::Count)] = {}; + + // Tracked by data-type-aware flipper for the run-end summary. + uint64_t escape_high_blast_total_ = 0; + uint64_t escape_low_blast_total_ = 0; + + // Bits flipped into forwarded payloads by latency_only DUE poisoning. + uint64_t due_poison_flips_total_ = 0; + + uint64_t frames_aborted_total_ = 0; + + // Track which BER values have already triggered the tight-bound warning + // (key is the bit-pattern of the double so we don't worry about == on + // floats). Set in warnIfBerExceedsTightBound. + std::set ber_warned_; +}; + +} // namespace Carcosa +} // namespace SST + +#endif /* SST_ELEMENTS_CARCOSA_ECC_GUARD_H */ diff --git a/src/sst/elements/carcosa/components/eccModelMath.h b/src/sst/elements/carcosa/components/eccModelMath.h new file mode 100644 index 0000000000..7ce00d6db2 --- /dev/null +++ b/src/sst/elements/carcosa/components/eccModelMath.h @@ -0,0 +1,51 @@ +#ifndef SST_ELEMENTS_CARCOSA_ECC_MODEL_MATH_H +#define SST_ELEMENTS_CARCOSA_ECC_MODEL_MATH_H + +#include "sst/elements/carcosa/components/eccScheme.h" +#include +#include +#include + +namespace SST { namespace Carcosa { namespace EccModelMath { + +inline double fitEventRate(double fit_per_mbit_per_hour, double dram_mb, + double sim_time_per_event_ns) { + if (fit_per_mbit_per_hour <= 0.0 || dram_mb <= 0.0 || + sim_time_per_event_ns <= 0.0) return 0.0; + const double failures_per_hour = fit_per_mbit_per_hour * 1e-9 * dram_mb * 8.0; + return std::min(1.0, failures_per_hour * sim_time_per_event_ns / 3.6e12); +} + +inline double jedecEventRate(double explicit_rate, double ber, + uint32_t payload_bytes) { + return explicit_rate > 0.0 + ? explicit_rate + : ber * static_cast(payload_bytes) * 8.0; +} + +inline uint32_t wordCount(uint32_t payload_bytes, EccScheme scheme) { + const uint32_t wb = eccWordBytes(scheme); + if (wb == 0 || payload_bytes == 0) return payload_bytes ? 1u : 0u; + return (payload_bytes + wb - 1) / wb; +} + +inline uint32_t wordBits(uint32_t payload_bytes, EccScheme scheme, + uint32_t word_index) { + const uint32_t wb = eccWordBytes(scheme); + if (wb == 0) return word_index == 0 ? payload_bytes * 8 : 0; + const uint64_t used = static_cast(word_index) * wb; + if (used >= payload_bytes) return 0; + return static_cast(std::min(wb, payload_bytes - used) * 8); +} + +inline bool resetCampaignEntry(const std::string& observed, + std::string& previous, uint64_t& count) { + if (observed == previous) return false; + previous = observed; + count = 0; + return true; +} + +}}} // namespace SST::Carcosa::EccModelMath + +#endif diff --git a/src/sst/elements/carcosa/components/eccPolicy.h b/src/sst/elements/carcosa/components/eccPolicy.h new file mode 100644 index 0000000000..74889ffe83 --- /dev/null +++ b/src/sst/elements/carcosa/components/eccPolicy.h @@ -0,0 +1,212 @@ +// 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 SST_ELEMENTS_CARCOSA_ECC_POLICY_H +#define SST_ELEMENTS_CARCOSA_ECC_POLICY_H + +#include "sst/elements/carcosa/components/eccScheme.h" +#include +#include +#include +#include +#include +#include +#include + +namespace SST { +namespace Carcosa { + +// Latencies in picoseconds; ber is per-bit error probability per access. +struct EccPolicyEntry { + EccScheme scheme = EccScheme::NONE; + double ber = 0.0; + uint64_t correctable_latency_ps = 0; + uint64_t due_latency_ps = 0; + uint64_t escape_latency_ps = 0; + bool inherits_uniform = true; +}; + +// Precedence: (kernel,region) > region > kernel > uniform. +// kernel/region "*" or "" = any. CSV: KERNEL[:@REGION]:scheme:ber:c_ps:d_ps:e_ps +class EccPolicyTable { +public: + EccPolicyTable() = default; + + void setUniform(const EccPolicyEntry& e) { uniform_ = e; uniform_.inherits_uniform = false; } + const EccPolicyEntry& uniform() const { return uniform_; } + + // Walk every concrete table entry; callback gets a tag like "uniform", + // "kernel=PREFILL", "region=KV_CACHE". Used by EccGuard::setup() BER warnings. + template + void forEachEntry(Fn&& fn) const { + fn(std::string("uniform"), uniform_); + for (const auto& kv : per_kernel_) { + fn(std::string("kernel=") + kv.first, kv.second); + } + for (const auto& kv : per_region_) { + fn(std::string("region=") + kv.first, kv.second); + } + for (const auto& kv : per_kernel_region_) { + fn(std::string("kernel@region=") + kv.first, kv.second); + } + } + + // Backwards-compatible kernel-only resolver. Region-unaware callers stay on this path. + const EccPolicyEntry& effectiveFor(const std::string& kernel_name) const { + if (kernel_name.empty()) return uniform_; + auto it = per_kernel_.find(kernel_name); + if (it == per_kernel_.end()) return uniform_; + return it->second.inherits_uniform ? uniform_ : it->second; + } + + // (kernel,region) > region > kernel > uniform. + // region_name "" or unmatched falls through to kernel-only. + const EccPolicyEntry& effectiveFor(const std::string& kernel_name, + const std::string& region_name) const { + if (!region_name.empty()) { + if (!kernel_name.empty()) { + auto it = per_kernel_region_.find(makeComboKey(kernel_name, region_name)); + if (it != per_kernel_region_.end()) return it->second; + } + auto rit = per_region_.find(region_name); + if (rit != per_region_.end()) return rit->second; + } + return effectiveFor(kernel_name); + } + + void setPerKernel(const std::string& kernel_name, const EccPolicyEntry& e) { + if (kernel_name.empty()) return; + EccPolicyEntry copy = e; + copy.inherits_uniform = false; + per_kernel_[kernel_name] = copy; + } + + void setPerRegion(const std::string& region_name, const EccPolicyEntry& e) { + if (region_name.empty()) return; + EccPolicyEntry copy = e; + copy.inherits_uniform = false; + per_region_[region_name] = copy; + } + + void setPerKernelRegion(const std::string& kernel_name, const std::string& region_name, + const EccPolicyEntry& e) { + if (kernel_name.empty() || region_name.empty()) return; + EccPolicyEntry copy = e; + copy.inherits_uniform = false; + per_kernel_region_[makeComboKey(kernel_name, region_name)] = copy; + } + + // CSV per entry. See class doc for accepted forms. + int parseCsv(const std::string& csv, std::vector& errors) { + int parsed = 0; + if (csv.empty()) return 0; + + std::string buf; + std::istringstream ss(csv); + while (std::getline(ss, buf, ',')) { + trim(buf); + if (buf.empty()) continue; + + std::vector parts = splitColon(buf); + if (parts.size() < 2) { + errors.push_back("ecc_kernel_policy: malformed entry '" + buf + "'"); + continue; + } + for (auto& p : parts) trim(p); + + // Tag may be KERNEL, KERNEL@REGION, *@REGION, or KERNEL@*. + std::string kernel_tok; + std::string region_tok; + splitAt(parts[0], kernel_tok, region_tok); + + bool kernel_any = (kernel_tok == "*" || kernel_tok.empty()); + bool region_any = (region_tok == "*" || region_tok.empty()); + + EccPolicyEntry e; + e.inherits_uniform = false; + + if (parts.size() >= 2) { + if (!eccSchemeFromString(parts[1], e.scheme)) { + errors.push_back("ecc_kernel_policy: unknown scheme '" + parts[1] + "' for '" + parts[0] + "'"); + continue; + } + } + if (parts.size() >= 3) e.ber = parseDouble(parts[2]); + if (parts.size() >= 4) e.correctable_latency_ps = parseUInt64(parts[3]); + if (parts.size() >= 5) e.due_latency_ps = parseUInt64(parts[4]); + if (parts.size() >= 6) e.escape_latency_ps = parseUInt64(parts[5]); + + if (!kernel_any && !region_any) { + setPerKernelRegion(kernel_tok, region_tok, e); + } else if (!region_any) { + setPerRegion(region_tok, e); + } else if (!kernel_any) { + setPerKernel(kernel_tok, e); + } else { + errors.push_back("ecc_kernel_policy: '*@*' not allowed; use ecc_scheme/ber for the uniform fallback"); + continue; + } + ++parsed; + } + return parsed; + } + +private: + EccPolicyEntry uniform_{}; + std::unordered_map per_kernel_; + std::map per_region_; + std::map per_kernel_region_; + + static std::string makeComboKey(const std::string& kernel, + const std::string& region) { + return kernel + "@" + region; + } + + static void splitAt(const std::string& tag, std::string& kernel, + std::string& region) { + auto pos = tag.find('@'); + if (pos == std::string::npos) { + kernel = tag; + region.clear(); + } else { + kernel = tag.substr(0, pos); + region = tag.substr(pos + 1); + } + } + + static void trim(std::string& s) { + size_t b = 0; + while (b < s.size() && std::isspace(static_cast(s[b]))) ++b; + size_t e = s.size(); + while (e > b && std::isspace(static_cast(s[e - 1]))) --e; + s = s.substr(b, e - b); + } + + static std::vector splitColon(const std::string& s) { + std::vector out; + std::string buf; + std::istringstream ss(s); + while (std::getline(ss, buf, ':')) out.push_back(buf); + return out; + } + + static double parseDouble(const std::string& s) { + try { return std::stod(s); } catch (...) { return 0.0; } + } + static uint64_t parseUInt64(const std::string& s) { + try { return static_cast(std::stoull(s)); } catch (...) { return 0; } + } +}; + +} // namespace Carcosa +} // namespace SST + +#endif /* SST_ELEMENTS_CARCOSA_ECC_POLICY_H */ diff --git a/src/sst/elements/carcosa/components/eccScheme.h b/src/sst/elements/carcosa/components/eccScheme.h new file mode 100644 index 0000000000..1efb0fc699 --- /dev/null +++ b/src/sst/elements/carcosa/components/eccScheme.h @@ -0,0 +1,195 @@ +// 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 SST_ELEMENTS_CARCOSA_ECC_SCHEME_H +#define SST_ELEMENTS_CARCOSA_ECC_SCHEME_H + +#include +#include +#include + +namespace SST { +namespace Carcosa { + +enum class EccOutcome : uint8_t { + Clean = 0, + Correctable = 1, + DetectableUncorrectable = 2, + SilentEscape = 3, +}; + +inline const char* eccOutcomeName(EccOutcome o) { + switch (o) { + case EccOutcome::Clean: return "clean"; + case EccOutcome::Correctable: return "correctable"; + case EccOutcome::DetectableUncorrectable: return "due"; + case EccOutcome::SilentEscape: return "escape"; + } + return "unknown"; +} + +enum class EccScheme : uint8_t { + NONE = 0, + SECDED_64 = 1, + CHIPKILL_x4 = 2, +}; + +inline const char* eccSchemeName(EccScheme s) { + switch (s) { + case EccScheme::NONE: return "none"; + case EccScheme::SECDED_64: return "secded"; + case EccScheme::CHIPKILL_x4: return "chipkill"; + } + return "unknown"; +} + +inline bool eccSchemeFromString(const std::string& s, EccScheme& out) { + if (s == "none" || s == "NONE") { out = EccScheme::NONE; return true; } + if (s == "secded" || s == "SECDED" || + s == "secded_64" || s == "SECDED_64") { out = EccScheme::SECDED_64; return true; } + if (s == "chipkill" || s == "CHIPKILL" || + s == "chipkill_x4") { out = EccScheme::CHIPKILL_x4; return true; } + return false; +} + +// Per-word ECC: line outcome = worst word (Clean& chip_error_counts, EccScheme scheme) { + if (scheme != EccScheme::CHIPKILL_x4) + return classifyEccWord( + [&]() -> unsigned { + unsigned s = 0; + for (auto c : chip_error_counts) s += c; + return s; + }(), scheme); + unsigned affected = 0; + for (auto c : chip_error_counts) + if (c > 0) ++affected; + if (affected == 0) return EccOutcome::Clean; + if (affected == 1) return EccOutcome::Correctable; + if (affected == 2) return EccOutcome::DetectableUncorrectable; + return EccOutcome::SilentEscape; +} + +// Result of aggregating per-word outcomes into a single line outcome. +struct EccLineOutcome { + EccOutcome outcome = EccOutcome::Clean; + unsigned escape_bits = 0; // bits-on-the-wire from escaping words only + unsigned total_errors = 0; // sum across all words (book-keeping) + std::vector escape_words; // indices of words that escaped + std::vector due_words; // indices of words that went DUE +}; + +// Aggregate per-word outcomes: line = worst word; escape_bits sum only over +// SilentEscape words. DUE words are uncorrectable — DropFrame aborts, +// LatencyOnly forwards poisoned bits (EccGuard flips them into the payload). +inline EccLineOutcome aggregateLineOutcome(const std::vector& per_word_errors, + EccScheme scheme) { + EccLineOutcome r; + for (size_t w = 0; w < per_word_errors.size(); ++w) { + unsigned e = per_word_errors[w]; + r.total_errors += e; + EccOutcome o = classifyEccWord(e, scheme); + if (static_cast(o) > static_cast(r.outcome)) r.outcome = o; + if (o == EccOutcome::SilentEscape) { + r.escape_bits += e; + r.escape_words.push_back(static_cast(w)); + } else if (o == EccOutcome::DetectableUncorrectable) { + r.due_words.push_back(static_cast(w)); + } + } + return r; +} + +// Chip-aware variant for CHIPKILL_x4. Uses per-chip error distribution +// instead of per-bit thresholds for more accurate classification. +inline EccLineOutcome aggregateLineOutcomeChipAware( + const std::vector& per_word_errors, + const std::vector>& per_word_chip_errors, + EccScheme scheme) { + if (scheme != EccScheme::CHIPKILL_x4 + || per_word_chip_errors.size() != per_word_errors.size()) { + return aggregateLineOutcome(per_word_errors, scheme); + } + EccLineOutcome r; + for (size_t w = 0; w < per_word_errors.size(); ++w) { + r.total_errors += per_word_errors[w]; + EccOutcome o = classifyEccWordChipAware(per_word_chip_errors[w], scheme); + if (static_cast(o) > static_cast(r.outcome)) r.outcome = o; + if (o == EccOutcome::SilentEscape) { + r.escape_bits += per_word_errors[w]; + r.escape_words.push_back(static_cast(w)); + } else if (o == EccOutcome::DetectableUncorrectable) { + r.due_words.push_back(static_cast(w)); + } + } + return r; +} + +// Back-compat: single "total errors on the line" count treated as one word. +// Over-counts DUE/Escape when errors span multiple words — prefer per-word +// classifyEccWord + aggregateLineOutcome. +inline EccOutcome classifyEccOutcome(unsigned num_bit_errors, + uint32_t /*payload_bytes*/, + EccScheme scheme) { + return classifyEccWord(num_bit_errors, scheme); +} + +} // namespace Carcosa +} // namespace SST + +#endif /* SST_ELEMENTS_CARCOSA_ECC_SCHEME_H */ diff --git a/src/sst/elements/carcosa/components/faultInjEvent.h b/src/sst/elements/carcosa/components/faultInjEvent.h index 47cdc056a8..07067a157f 100644 --- a/src/sst/elements/carcosa/components/faultInjEvent.h +++ b/src/sst/elements/carcosa/components/faultInjEvent.h @@ -21,10 +21,7 @@ namespace SST { namespace Carcosa { -/** - * Fault Injection Event for passing fault injection parameters - * between CPU and Hali components. - */ +/** Fault-injection parameters passed between CPU and Hali. */ class FaultInjEvent : public SST::Event { public: FaultInjEvent() : SST::Event(), fname_(""), probability_(0.0f), rate_(0.0f), str_(""), num_(0) {} diff --git a/src/sst/elements/carcosa/components/faultInjManager.h b/src/sst/elements/carcosa/components/faultInjManager.h index 66b250f3fc..1cabc7a959 100644 --- a/src/sst/elements/carcosa/components/faultInjManager.h +++ b/src/sst/elements/carcosa/components/faultInjManager.h @@ -34,7 +34,7 @@ class FaultInjManager : public FaultInjManagerAPI "carcosa", "FaultInjManager", SST_ELI_ELEMENT_VERSION(1,0,0), - "Manages fault injection for carcosa components", + "Manages fault injection for Carcosa components", SST::Carcosa::FaultInjManagerAPI ) diff --git a/src/sst/elements/carcosa/components/fourStateAgent.cc b/src/sst/elements/carcosa/components/fourStateAgent.cc new file mode 100644 index 0000000000..41e8dde0ad --- /dev/null +++ b/src/sst/elements/carcosa/components/fourStateAgent.cc @@ -0,0 +1,244 @@ +// 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 "sst_config.h" +#include "sst/elements/carcosa/components/fourStateAgent.h" +#include "sst/elements/carcosa/components/haliEvent.h" +#include "sst/elements/carcosa/components/vlaRegions.h" +#include "sst/elements/memHierarchy/memEvent.h" +#include "sst/elements/memHierarchy/memTypes.h" + +#include +#include + +using namespace SST; +using namespace SST::MemHierarchy; +using namespace SST::Carcosa; + +FourStateAgent::FourStateAgent(ComponentId_t id, Params& params) + : InterceptionAgentAPI(id, params) +{ + out_ = new Output("", 1, 0, Output::STDOUT); + + stateKey_ = params.find("state_key", ""); + regionSize_ = params.find("region_size", 4096); + regionsCsv_ = params.find("regions", ""); + initialCommand_ = params.find("initial_command", 0); + numCommands_ = params.find("num_commands", 4); + maxIterations_ = params.find("max_iterations", 12); + verbose_ = params.find("verbose", false); + params.find_array("kernel_names", kernelNames_); + + if (numCommands_ < 1) { + out_->fatal(CALL_INFO, -1, + "FourStateAgent: 'num_commands' must be >= 1 (got %d).\n", numCommands_); + } + + if (stateKey_.empty()) { + out_->fatal(CALL_INFO, -1, + "FourStateAgent: 'state_key' is required (pick something unique per core, " + "e.g. 'core0').\n"); + } +} + +FourStateAgent::~FourStateAgent() +{ + delete out_; +} + +void FourStateAgent::agentSetup() +{ + nextCommand_ = initialCommand_; + + // Establish the registry entry for this core. After this point any + // PortModule (e.g. PortModuleStateGate) looking up stateKey_ will see + // a live snapshot instead of nullptr. + PipelineStateBase* s = PipelineStateRegistry::getOrCreate(stateKey_); + s->currentKernel = IDLE; + s->currentKernelName = kernelNameFor(IDLE); + s->pipelineCycle = 0; + publishedKernel_ = IDLE; + + // Publish the MMIO control region as a named region so that + // `region_names="mmio_control"` predicates can match. + s->ensureRegionSlot(0); + s->regions[0].base = controlAddrBase_; + s->regions[0].size = regionSize_; + s->regions[0].valid = regionSize_ > 0; + s->regions[0].id = 0; + s->regions[0].name = "mmio_control"; + + int n_user = publishUserRegions(stateKey_, regionsCsv_, out_, "FourStateAgent"); + if (verbose_ && n_user > 0) { + out_->output("FourStateAgent[%s]: published %d user region(s)\n", + stateKey_.c_str(), n_user); + } + + if (verbose_) { + out_->output("FourStateAgent[%s]: setup initial_command=%d num_commands=%d " + "max_iterations=%d mmio_base=0x%" PRIx64 " size=%" PRIu64 "\n", + stateKey_.c_str(), initialCommand_, numCommands_, maxIterations_, + controlAddrBase_, regionSize_); + } + + if (pendingCommandRead_) { + sendCommandResponse(pendingCommandRead_, nextCommand_); + pendingCommandRead_ = nullptr; + nextCommand_ = INT_MIN; + } +} + +void FourStateAgent::publishState(int kernel) +{ + PipelineStateBase* s = PipelineStateRegistry::getMutable(stateKey_); + if (!s) { + // agentSetup() should have created the entry; defensively re-create + // so a stray lookup from a PortModule never sees nullptr mid-run. + s = PipelineStateRegistry::getOrCreate(stateKey_); + } + s->currentKernel = kernel; + s->currentKernelName = kernelNameFor(kernel); + s->pipelineCycle = (numCommands_ > 0) ? (currentIteration_ / numCommands_) : 0; + publishedKernel_ = kernel; + + if (verbose_) { + out_->output("FourStateAgent[%s]: publish currentKernel=%d ('%s') pipelineCycle=%d\n", + stateKey_.c_str(), s->currentKernel, s->currentKernelName.c_str(), + s->pipelineCycle); + } +} + +std::string FourStateAgent::kernelNameFor(int kernel) const +{ + if (kernel == IDLE) return "IDLE"; + if (kernel >= 0 && kernel < static_cast(kernelNames_.size()) + && !kernelNames_[kernel].empty()) { + return kernelNames_[kernel]; + } + return "K" + std::to_string(kernel); +} + +bool FourStateAgent::handleInterceptedEvent(MemEvent* ev, Link* highlink) +{ + uint64_t offset = ev->getAddr() - controlAddrBase_; + + // Command read: the CPU is blocked waiting for the next kernel index. + // We do not publish here; publishState(nextCommand_) happens inside + // sendCommandResponse(), at the instant we commit to a specific kernel. + if (offset == 0x0000 && ev->getCmd() == Command::GetS) { + if (nextCommand_ >= -1 && nextCommand_ != INT_MIN) { + sendCommandResponse(ev, nextCommand_); + nextCommand_ = INT_MIN; + } else { + pendingCommandRead_ = ev; + } + return true; + } + + // Status write: the CPU has finished running the kernel it was + // dispatched. The CPU will not issue any more loads/stores from the + // handler past this point, so publish IDLE to close the gate window. + if (offset == 0x0004 && (ev->getCmd() == Command::Write || ev->getCmd() == Command::GetX)) { + sendWriteAck(ev); + publishState(IDLE); + localDone_ = true; + currentIteration_++; + if (leftHaliLink_) { + leftHaliLink_->send(new HaliEvent("done", static_cast(currentIteration_))); + } + if (verbose_) { + out_->output("FourStateAgent[%s]: sent done iteration %u\n", + stateKey_.c_str(), currentIteration_); + } + checkBothDone(); + return true; + } + + // Intercept range covered an address we don't route through the FSM; + // drop it and report handled so Hali doesn't forward it elsewhere. + delete ev; + return true; +} + +void FourStateAgent::notifyPartnerDone(unsigned iteration) +{ + (void)iteration; + partnerDone_ = true; + if (verbose_) { + out_->output("FourStateAgent[%s]: partner done (iteration=%u)\n", + stateKey_.c_str(), iteration); + } + checkBothDone(); +} + +void FourStateAgent::checkBothDone() +{ + if (!localDone_ || !partnerDone_) return; + + localDone_ = false; + partnerDone_ = false; + + // Advance the "next command" by cycling through [0, numCommands_). + if (currentIteration_ >= maxIterations_) { + nextCommand_ = -1; + } else { + nextCommand_ = currentIteration_ % numCommands_; + } + + if (pendingCommandRead_) { + sendCommandResponse(pendingCommandRead_, nextCommand_); + pendingCommandRead_ = nullptr; + nextCommand_ = INT_MIN; + } +} + +void FourStateAgent::setRingLink(Link* leftLink) { leftHaliLink_ = leftLink; } + +void FourStateAgent::setInterceptBase(uint64_t base) { + controlAddrBase_ = base; + // agentSetup() hasn't necessarily run yet, but if the registry entry + // already exists (e.g. a gate looked it up), keep the region info + // consistent with the base Hali handed us. + if (!stateKey_.empty()) { + PipelineStateBase* s = PipelineStateRegistry::getMutable(stateKey_); + if (s) { + s->ensureRegionSlot(0); + s->regions[0].base = controlAddrBase_; + s->regions[0].size = regionSize_; + s->regions[0].valid = regionSize_ > 0; + s->regions[0].id = 0; + s->regions[0].name = "mmio_control"; + } + } +} + +void FourStateAgent::setHighlink(Link* highlink) { highlink_ = highlink; } + +void FourStateAgent::sendCommandResponse(MemEvent* request, int value) +{ + // Publish BEFORE sending the response so PortModules on the lowlink see + // currentKernel on the first post-unblock load/store. value < 0 => IDLE. + publishState(value >= 0 ? value : IDLE); + + MemEvent* resp = request->makeResponse(); + std::vector data(4); + std::memcpy(data.data(), &value, sizeof(int)); + resp->setPayload(data); + if (highlink_) highlink_->send(resp); + delete request; +} + +void FourStateAgent::sendWriteAck(MemEvent* ev) +{ + MemEvent* resp = ev->makeResponse(); + if (highlink_) highlink_->send(resp); + delete ev; +} diff --git a/src/sst/elements/carcosa/components/fourStateAgent.h b/src/sst/elements/carcosa/components/fourStateAgent.h new file mode 100644 index 0000000000..f38c18b990 --- /dev/null +++ b/src/sst/elements/carcosa/components/fourStateAgent.h @@ -0,0 +1,107 @@ +// 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 CARCOSA_FOURSTATEAGENT_H +#define CARCOSA_FOURSTATEAGENT_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace SST { +namespace Carcosa { + +/** + * PingPong-compatible MMIO agent; publishes currentKernel into PipelineStateRegistry. + */ +class FourStateAgent : public InterceptionAgentAPI +{ +public: + SST_ELI_REGISTER_SUBCOMPONENT( + FourStateAgent, + "carcosa", + "FourStateAgent", + SST_ELI_ELEMENT_VERSION(0, 1, 0), + "N-kernel MMIO agent that publishes PipelineStateBase (currentKernel = CPU command index in flight).", + SST::Carcosa::InterceptionAgentAPI + ) + + SST_ELI_DOCUMENT_PARAMS( + {"state_key", "PipelineStateRegistry key this agent publishes into. Required.", ""}, + {"region_size", "Size in bytes of the published MMIO control region (regions[0]).", "4096"}, + {"regions", "Optional CSV of workload-labeled DRAM regions for region-aware EccGuard/PortModuleStateGate policies. 'name:base:size' triples; slot 0 reserved for mmio_control. Used by the Phase 6 region-routing smoke test.", ""}, + {"initial_command", "First command index returned to the CPU.", "0"}, + {"num_commands", "Number of command indices to cycle through (must match the jump_table length in the Vanadis binary; 2 for pingpong, 4 for fourstate).", "4"}, + {"max_iterations", "Max iterations before sending exit (-1).", "12"}, + {"kernel_names", "Optional list of workload kernel names for command indices 0..num_commands-1, published into PipelineStateBase::currentKernelName for name-keyed consumers (EccGuard kernel_policy, CriticalActionWatcher). Index k falls back to 'K' when unset; the idle state always publishes 'IDLE'.", ""}, + {"verbose", "Enable verbose per-event output.", "false"} + ) + + FourStateAgent(ComponentId_t id, Params& params); + FourStateAgent() : InterceptionAgentAPI() {} + ~FourStateAgent() override; + + bool handleInterceptedEvent(SST::MemHierarchy::MemEvent* ev, SST::Link* highlink) override; + void notifyPartnerDone(unsigned iteration) override; + void agentSetup() override; + void setRingLink(SST::Link* leftLink) override; + void setInterceptBase(uint64_t base) override; + void setHighlink(SST::Link* highlink) override; + + /** currentKernel sentinel while CPU is idle between kernels. */ + static constexpr int IDLE = -1; + +private: + void checkBothDone(); + void sendCommandResponse(SST::MemHierarchy::MemEvent* request, int value); + void sendWriteAck(SST::MemHierarchy::MemEvent* ev); + + /** Publish currentKernel / name / pipelineCycle into the registry. */ + void publishState(int kernel); + + /** Name published into currentKernelName for a given kernel index: + * kernel_names[k] when configured, else "K"; IDLE maps to "IDLE". */ + std::string kernelNameFor(int kernel) const; + + SST::Output* out_ = nullptr; + SST::Link* leftHaliLink_ = nullptr; + SST::Link* highlink_ = nullptr; + + // Ping/pong-compatible protocol state (kept identical so hyades.h works) + uint64_t controlAddrBase_ = 0; + uint64_t regionSize_ = 4096; + int initialCommand_ = 0; + int numCommands_ = 4; + int maxIterations_ = 12; + int currentIteration_ = 0; + int nextCommand_ = INT_MIN; + bool partnerDone_ = false; + bool localDone_ = false; + bool verbose_ = false; + SST::MemHierarchy::MemEvent* pendingCommandRead_ = nullptr; + + // Registry publishing state + std::string stateKey_; + std::string regionsCsv_; + std::vector kernelNames_; + int publishedKernel_ = IDLE; +}; + +} // namespace Carcosa +} // namespace SST + +#endif /* CARCOSA_FOURSTATEAGENT_H */ diff --git a/src/sst/elements/carcosa/components/hali.cc b/src/sst/elements/carcosa/components/hali.cc index c27bf0242f..2eb9a8c886 100644 --- a/src/sst/elements/carcosa/components/hali.cc +++ b/src/sst/elements/carcosa/components/hali.cc @@ -30,6 +30,7 @@ #include #include #include +#include using namespace SST; using namespace SST::MemHierarchy; @@ -139,6 +140,17 @@ Hali::Hali(ComponentId_t id, Params& params) : Component(id) { primaryComponentDoNotEndSim(); } + // Optional MMIO transport: control region arrives as StandardMem on a + // dedicated link; both transports dispatch to handleControlAccess. + mmioBase_ = params.find("mmio_base", 0xBEEF0000); + mmioHandler_ = new MmioHandler(this, out_); + TimeConverter mmioTC = getTimeConverter(params.find("clock", "1GHz")); + mmioIface_ = loadUserSubComponent( + "mmio_iface", ComponentInfo::SHARE_NONE, mmioTC, + new StandardMem::Handler(this)); + if (interceptionAgent_) + interceptionAgent_->setControlChannel(this); + eventsReceived_ = 0; eventsForwarded_ = 0; eventsSent_ = 0; @@ -149,6 +161,11 @@ Hali::Hali(ComponentId_t id, Params& params) : Component(id) { * Lifecycle Phase #2: Init *****************************************************************************/ void Hali::init(unsigned phase) { + if (mmioIface_) mmioIface_->init(phase); + // Forward the init phase to the interception agent so an agent that owns a + // memHierarchy StandardMem interface can complete its untimed init handshake + // (default agentInit is a no-op for ring-only agents). + if (interceptionAgent_) interceptionAgent_->agentInit(phase); if (verbose_) { out_->output("Phase: Init(%u), %s\n", phase, getName().c_str()); out_->output(" %s: highlink_=%p, lowlink_=%p\n", getName().c_str(), @@ -204,6 +221,8 @@ void Hali::init(unsigned phase) { void Hali::setup() { out_->output("Phase: Setup, %s\n", getName().c_str()); + if (mmioIface_) mmioIface_->setup(); + if (faultInjManager_) { faultInjManager_->processMessagesFromPMs(); } @@ -321,8 +340,20 @@ void Hali::handleCpuEvent(SST::Event* ev) { void Hali::highlinkMemEvent(SST::Event* ev) { MemEvent* mevent = dynamic_cast(ev); - if (mevent && interceptionAgent_ && isInterceptedAddress(mevent->getAddr())) { - if (interceptionAgent_->handleInterceptedEvent(mevent, highlink_)) { + uint64_t base = 0; + if (mevent && interceptionAgent_ && interceptedRangeBase(mevent->getAddr(), base)) { + // Transport-neutral path first: normalize into a ControlAccess and let + // the agent's handleControlAccess consume it. Identical decode to the + // MMIO transport, so a driver runs unchanged under both. + ControlMemDispatch dispatch = dispatchControlMemEvent(mevent, base); + if (dispatch == ControlMemDispatch::Handled) { + return; + } + // Fall back only when dispatch explicitly permits it. In particular, a + // payload-less GetX must bypass both APIs and continue to memory as an + // ownership request, not be decoded by a legacy agent as a write of 0. + if (dispatch == ControlMemDispatch::LegacyFallback && + interceptionAgent_->handleInterceptedEvent(mevent, highlink_)) { return; } } @@ -375,12 +406,14 @@ void Hali::handleHaliEvent(SST::Event* ev) { HaliEvent* event = dynamic_cast(ev); if (event) { - if (interceptionAgent_ && event->getStr() == "done") { + // With an interception agent, ring HaliEvents are sideband for that + // agent (self-named init tokens are drained in init()). + if (interceptionAgent_) { if (verbose_) { - out_->output(" %" PRIu64 " %s received done (iteration %u)\n", - getCurrentSimCycle(), getName().c_str(), event->getNum()); + out_->output(" %" PRIu64 " %s received %s\n", + getCurrentSimCycle(), getName().c_str(), event->toString().c_str()); } - interceptionAgent_->notifyPartnerDone(event->getNum()); + interceptionAgent_->handleRingEvent(event); delete event; return; } @@ -426,6 +459,16 @@ bool Hali::isInterceptedAddress(uint64_t addr) const { return false; } +bool Hali::interceptedRangeBase(uint64_t addr, uint64_t& base) const { + for (const auto& range : interceptRanges_) { + if (addr >= range.first && addr < range.second) { + base = range.first; + return true; + } + } + return false; +} + /***************************************************************************** * Lifecycle Phase #5: Complete *****************************************************************************/ @@ -496,6 +539,7 @@ void Hali::finish() { *****************************************************************************/ Hali::~Hali() { out_->output("Phase: Destruction\n"); + delete mmioHandler_; delete out_; } @@ -510,3 +554,167 @@ void Hali::printStatus(Output& sim_out) { sim_out.output("%s: sent %u, received %u, forwarded %u.\n", getName().c_str(), eventsSent_, eventsReceived_, eventsForwarded_); } + +/***************************************************************************** + * MMIO-peripheral transport (optional; mirror of data-plane interception) + *****************************************************************************/ +void Hali::handleMmioRequest(StandardMem::Request* req) { + req->handle(mmioHandler_); +} + +void Hali::MmioHandler::handle(StandardMem::Read* req) { + uint64_t offset = req->pAddr - hali_->mmioBase_; + if (hali_->interceptionAgent_) { + ControlAccess acc; + acc.isWrite = false; + acc.offset = offset; + ControlResult r = hali_->interceptionAgent_->handleControlAccess(acc); + if (r == ControlResult::Handled) { + hali_->sendMmioReadResponse(req, acc.readValue); + return; + } + if (r == ControlResult::Deferred) { + // Park the read until the agent arms a value (e.g. command not ready). + hali_->parkGuard("mmio"); + hali_->pendingTransport_ = PendingTransport::Mmio; + hali_->pendingMmioRead_ = req; + return; + } + } + // Ignored / no agent: complete the load with 0 so the CPU does not + // stall, but say so once -- a valid-looking 0 here usually means a + // missing interceptionAgent or a mistyped offset. + if (!hali_->warnedSilentControlRead_) { + hali_->warnedSilentControlRead_ = true; + hali_->out_->output("%s: WARNING un-handled control read at offset " + "0x%" PRIx64 " answered with 0 (%s). Check the " + "interceptionAgent wiring if this is unexpected; " + "warning only prints once.\n", + hali_->getName().c_str(), offset, + hali_->interceptionAgent_ ? "agent returned Ignored" + : "no agent attached"); + } + hali_->sendMmioReadResponse(req, 0); +} + +void Hali::MmioHandler::handle(StandardMem::Write* req) { + uint64_t offset = req->pAddr - hali_->mmioBase_; + uint32_t value = 0; + if (req->data.size() >= sizeof(uint32_t)) + std::memcpy(&value, req->data.data(), sizeof(uint32_t)); + if (hali_->interceptionAgent_) { + ControlAccess acc; + acc.isWrite = true; + acc.offset = offset; + acc.value = value; + acc.posted = req->posted; + hali_->interceptionAgent_->handleControlAccess(acc); + } + if (!req->posted) { + StandardMem::WriteResp* resp = + static_cast(req->makeResponse()); + hali_->mmioIface_->send(resp); + } + delete req; +} + +void Hali::sendMmioReadResponse(StandardMem::Read* req, uint32_t value) { + StandardMem::ReadResp* resp = static_cast(req->makeResponse()); + std::memcpy(resp->data.data(), &value, std::min(resp->data.size(), sizeof(uint32_t))); + mmioIface_->send(resp); + delete req; +} + +void Hali::parkGuard(const char* transport) { + if (pendingMmioRead_ || pendingDataPlaneRead_) { + out_->fatal(CALL_INFO, -1, + "Error in %s: a second Deferred control read (%s transport) " + "arrived while one is already parked. The hub keeps exactly one " + "parked read; overwriting it would leak the first requester and " + "hang its core. Serialize control reads (single requester) or " + "extend Hali with a pending-read queue.\n", + getName().c_str(), transport); + } +} + +void Hali::completePendingRead(uint32_t value) { + if (pendingTransport_ == PendingTransport::Mmio && pendingMmioRead_) { + StandardMem::Read* req = pendingMmioRead_; + pendingMmioRead_ = nullptr; + pendingTransport_ = PendingTransport::None; + sendMmioReadResponse(req, value); + } else if (pendingTransport_ == PendingTransport::DataPlane && pendingDataPlaneRead_) { + MemEvent* req = pendingDataPlaneRead_; + pendingDataPlaneRead_ = nullptr; + pendingTransport_ = PendingTransport::None; + sendDataPlaneReadResponse(req, value); + } +} + +/***************************************************************************** + * Data-plane transport (CPU load/store interception; mirror of MMIO above) + *****************************************************************************/ +Hali::ControlMemDispatch Hali::dispatchControlMemEvent(MemEvent* mevent, uint64_t base) { + Command cmd = mevent->getCmd(); + bool isWrite = (cmd == Command::Write || cmd == Command::GetX); + bool isRead = (cmd == Command::GetS); + if (!isWrite && !isRead) + return ControlMemDispatch::LegacyFallback; + // Payload-less GetX is read-for-ownership, not a store of 0 — bypass both + // agent APIs and forward normally. + if (cmd == Command::GetX && mevent->getPayloadSize() == 0) { + if (!warnedPayloadlessControlGetX_) { + warnedPayloadlessControlGetX_ = true; + out_->output("%s: WARNING payload-less GetX at intercepted control " + "offset 0x%" PRIx64 " bypassed control agents and was " + "forwarded normally. Keep control ranges uncacheable " + "so writes carry their payload; warning only prints once.\n", + getName().c_str(), mevent->getAddr() - base); + } + return ControlMemDispatch::BypassLegacy; + } + + ControlAccess acc; + acc.isWrite = isWrite; + acc.offset = mevent->getAddr() - base; + if (isWrite) { + const std::vector& payload = mevent->getPayload(); + if (payload.size() >= sizeof(uint32_t)) + std::memcpy(&acc.value, payload.data(), sizeof(uint32_t)); + } + + ControlResult r = interceptionAgent_->handleControlAccess(acc); + if (r == ControlResult::Ignored) + return ControlMemDispatch::LegacyFallback; + + if (isWrite) { + // Writes are always Handled here; ack on the data plane like the CPU expects. + sendDataPlaneWriteAck(mevent); + return ControlMemDispatch::Handled; + } + // Read. + if (r == ControlResult::Handled) { + sendDataPlaneReadResponse(mevent, acc.readValue); + return ControlMemDispatch::Handled; + } + // Deferred: park the load until the agent arms a value. + parkGuard("data-plane"); + pendingTransport_ = PendingTransport::DataPlane; + pendingDataPlaneRead_ = mevent; + return ControlMemDispatch::Handled; +} + +void Hali::sendDataPlaneReadResponse(MemEvent* req, uint32_t value) { + MemEvent* resp = req->makeResponse(); + std::vector data(sizeof(uint32_t)); + std::memcpy(data.data(), &value, sizeof(uint32_t)); + resp->setPayload(data); + if (highlink_) highlink_->send(resp); + delete req; +} + +void Hali::sendDataPlaneWriteAck(MemEvent* req) { + MemEvent* resp = req->makeResponse(); + if (highlink_) highlink_->send(resp); + delete req; +} diff --git a/src/sst/elements/carcosa/components/hali.h b/src/sst/elements/carcosa/components/hali.h index d23cc6108e..d58adf15f6 100644 --- a/src/sst/elements/carcosa/components/hali.h +++ b/src/sst/elements/carcosa/components/hali.h @@ -16,14 +16,12 @@ #ifndef CARCOSA_HALI_COMPONENT_H #define CARCOSA_HALI_COMPONENT_H -/** - * Hali - interface layer between sensors/CPUs, the memory hierarchy (highlink/lowlink), - * and other Hali instances on a ring. Provides fault-injection hooks via FaultInjManager. - */ +/** Hali: CPU/sensors <-> memHierarchy and ring peers; FaultInjManager hooks. */ #include #include #include +#include #include "sst/elements/carcosa/components/carcosaMemCtrl.h" #include "sst/elements/carcosa/components/faultInjManagerAPI.h" #include "sst/elements/carcosa/components/interceptionAgentAPI.h" @@ -37,7 +35,7 @@ class MemEvent; namespace Carcosa { -class Hali : public SST::Component { +class Hali : public SST::Component, public ControlChannel { public: SST_ELI_REGISTER_COMPONENT( Hali, @@ -52,11 +50,14 @@ class Hali : public SST::Component { {"Sensors", "Number of SensorComponents this interface receives from.", NULL}, {"CPUs", "Number of Compute components the Hali sends to.", NULL}, {"verbose", "Enable verbose output for debugging.", "false"}, - {"intercept_ranges", "Semicolon-separated base,size pairs (e.g. '0xBEEF0000,4096') for addresses to hand to InterceptionAgent.", ""} + {"intercept_ranges", "Semicolon-separated base,size pairs (e.g. '0xBEEF0000,4096') for addresses to hand to InterceptionAgent.", ""}, + {"clock", "Clock for the optional MMIO StandardMem interface.", "1GHz"}, + {"mmio_base", "Base address of the MMIO control region (MMIO-peripheral transport).", "0xBEEF0000"} ) SST_ELI_DOCUMENT_SUBCOMPONENT_SLOTS( - {"interceptionAgent", "Optional agent for intercepted memory accesses (e.g. carcosa.PingPongAgent). If unset, no interception.", "SST::Carcosa::InterceptionAgentAPI"} + {"interceptionAgent", "Optional agent for intercepted memory accesses (e.g. Carcosa.PingPongAgent). If unset, no interception.", "SST::Carcosa::InterceptionAgentAPI"}, + {"mmio_iface", "Optional StandardMem interface delivering the control region as MMIO requests (MMIO-peripheral transport, e.g. from an MMIO region handler).", "SST::Interfaces::StandardMem"} ) SST_ELI_DOCUMENT_PORTS( @@ -89,6 +90,9 @@ class Hali : public SST::Component { /** Returns true if addr falls within any registered intercept range. */ bool isInterceptedAddress(uint64_t addr) const; + // ControlChannel: complete a control read the agent previously Deferred. + void completePendingRead(uint32_t value) override; + private: unsigned eventsToSend_; bool verbose_; @@ -120,6 +124,51 @@ class Hali : public SST::Component { // Address ranges (base, end_exclusive) forwarded to the interception agent. std::vector> interceptRanges_; + + // Base of the intercept range containing addr (data-plane transport), or + // false if addr is not intercepted. Used to compute the control offset. + bool interceptedRangeBase(uint64_t addr, uint64_t& base) const; + + // Data-plane: MemEvent -> ControlAccess -> handleControlAccess. + // BypassLegacy forwards requests (e.g. payload-less GetX) without agent APIs. + enum class ControlMemDispatch { Handled, LegacyFallback, BypassLegacy }; + ControlMemDispatch dispatchControlMemEvent(SST::MemHierarchy::MemEvent* mevent, uint64_t base); + void sendDataPlaneReadResponse(SST::MemHierarchy::MemEvent* req, uint32_t value); + void sendDataPlaneWriteAck(SST::MemHierarchy::MemEvent* req); + + // --- Optional MMIO-peripheral transport (active when mmio_iface is connected) --- + void handleMmioRequest(SST::Interfaces::StandardMem::Request* req); + void sendMmioReadResponse(SST::Interfaces::StandardMem::Read* req, uint32_t value); + + class MmioHandler : public SST::Interfaces::StandardMem::RequestHandler { + public: + MmioHandler(Hali* hali, SST::Output* out) + : SST::Interfaces::StandardMem::RequestHandler(out), hali_(hali) {} + void handle(SST::Interfaces::StandardMem::Read* req) override; + void handle(SST::Interfaces::StandardMem::Write* req) override; + private: + Hali* hali_; + }; + + SST::Interfaces::StandardMem* mmioIface_ = nullptr; + MmioHandler* mmioHandler_ = nullptr; + uint64_t mmioBase_ = 0xBEEF0000; + + // Single parked Deferred control read. A second while one is parked is + // fatal (must not overwrite/leak the first requester); see parkGuard(). + enum class PendingTransport { None, Mmio, DataPlane }; + PendingTransport pendingTransport_ = PendingTransport::None; + SST::Interfaces::StandardMem::Read* pendingMmioRead_ = nullptr; + SST::MemHierarchy::MemEvent* pendingDataPlaneRead_ = nullptr; + + // Fatal if a Deferred control read is already parked. + void parkGuard(const char* transport); + + // Warn-once flag: an un-handled / agent-less control read answered with 0 + // usually means a misconfiguration (missing interceptionAgent, mistyped + // offset), so say so instead of silently returning valid-looking data. + bool warnedSilentControlRead_ = false; + bool warnedPayloadlessControlGetX_ = false; }; } // namespace Carcosa diff --git a/src/sst/elements/carcosa/components/haliEvent.h b/src/sst/elements/carcosa/components/haliEvent.h index e4ceb39629..af78a0bd9c 100644 --- a/src/sst/elements/carcosa/components/haliEvent.h +++ b/src/sst/elements/carcosa/components/haliEvent.h @@ -17,18 +17,23 @@ #define CARCOSA_HALIEVENT_H #include +#include +#include +#include namespace SST { namespace Carcosa { /** - * Hali Event for communication between Hali components in a ring. + * Ring event: tag + num (+ optional opaque Cmd payload; partners agree on layout). */ class HaliEvent : public SST::Event { public: HaliEvent() : SST::Event(), str_(""), num_(0) {} HaliEvent(const std::string& val) : SST::Event(), str_(val), num_(0) {} HaliEvent(const std::string& sval, unsigned uval) : SST::Event(), str_(sval), num_(uval) {} + HaliEvent(const std::string& sval, unsigned uval, std::vector payload) + : SST::Event(), str_(sval), num_(uval), payload_(std::move(payload)) {} HaliEvent(unsigned val) : SST::Event(), str_(""), num_(val) {} ~HaliEvent() {} @@ -36,9 +41,14 @@ class HaliEvent : public SST::Event { std::string getStr() const { return str_; } unsigned getNum() const { return num_; } + bool hasPayload() const { return !payload_.empty(); } + const std::vector& getPayload() const { return payload_; } + void setPayload(std::vector payload) { payload_ = std::move(payload); } + std::string toString() const override { std::stringstream s; - s << "HaliEvent. String='" << str_ << "' Number='" << num_ << "'"; + s << "HaliEvent. String='" << str_ << "' Number='" << num_ + << "' PayloadBytes='" << payload_.size() << "'"; return s.str(); } @@ -49,11 +59,13 @@ class HaliEvent : public SST::Event { private: std::string str_; unsigned num_; + std::vector payload_; void serialize_order(SST::Core::Serialization::serializer& ser) override { Event::serialize_order(ser); SST_SER(str_); SST_SER(num_); + SST_SER(payload_); } ImplementSerializable(SST::Carcosa::HaliEvent); diff --git a/src/sst/elements/carcosa/components/hyadesProtocol.h b/src/sst/elements/carcosa/components/hyadesProtocol.h new file mode 100644 index 0000000000..5530acc054 --- /dev/null +++ b/src/sst/elements/carcosa/components/hyadesProtocol.h @@ -0,0 +1,55 @@ +// 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. +// +// Portions are copyright of other developers: +// See the file CONTRIBUTORS.TXT in the top level directory +// of the distribution for more information. +// +// 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 CARCOSA_HYADES_PROTOCOL_H +#define CARCOSA_HYADES_PROTOCOL_H + +#include + +namespace SST { +namespace Carcosa { + +/** + * C++ mirror of hyades.h MMIO ABI — keep offsets in sync across transports. + */ +namespace HyadesAbi { + static constexpr uint64_t kCommand = 0x00; // R: next kernel/action index; <0 = exit + static constexpr uint64_t kStatus = 0x04; // W: completed index (kernel-end / FSM advance) + static constexpr uint64_t kSeqLen = 0x08; // R: current sequence length + static constexpr uint64_t kRole = 0x10; // R: core role (0 = default) + static constexpr uint64_t kRegionBaseLo = 0x40; // W: staged region base, low 32 bits + static constexpr uint64_t kRegionBaseHi = 0x80; // W: staged region base, high 32 bits + static constexpr uint64_t kRegionSize = 0xC0; // W: staged region size + static constexpr uint64_t kRegionCommit = 0x100; // W: commit staged region into slot N + static constexpr uint64_t kActionChecksum = 0x140; // W: per-frame action checksum, low 32 bits (commits the value) + static constexpr uint64_t kActionToken = 0x180; // W: per-frame decoded-action token + // Action-checksum HI. Workload writes HI, fences, then LO (commit); a + // second fence keeps status-write from overtaking. Consumers clear HI after LO. + static constexpr uint64_t kActionChecksumHi = 0x1C0; + + constexpr uint64_t combineActionChecksum(uint32_t hi, uint32_t lo) { + return (static_cast(hi) << 32) | static_cast(lo); + } + static_assert(combineActionChecksum(0x01234567u, 0x89ABCDEFu) == + 0x0123456789ABCDEFull, + "Hyades checksum HI/LO combine must preserve all 64 bits"); + + static constexpr int kExitSentinel = -1; // command value meaning "end run" +} + +} // namespace Carcosa +} // namespace SST + +#endif // CARCOSA_HYADES_PROTOCOL_H diff --git a/src/sst/elements/carcosa/components/interceptionAgentAPI.h b/src/sst/elements/carcosa/components/interceptionAgentAPI.h index c6848f5f2b..7662c9de58 100644 --- a/src/sst/elements/carcosa/components/interceptionAgentAPI.h +++ b/src/sst/elements/carcosa/components/interceptionAgentAPI.h @@ -19,10 +19,42 @@ #include #include #include +#include +#include "sst/elements/carcosa/components/haliEvent.h" +#include "sst/elements/carcosa/components/hyadesProtocol.h" +#include "sst/elements/carcosa/components/ringProtocol.h" +#include +#include +#include namespace SST { namespace Carcosa { +// Transport-neutral control access: data-plane MemEvents or MMIO StandardMem +// both normalize to ControlAccess -> handleControlAccess. + +enum class ControlResult { + Ignored, // agent does not recognize this offset; hub applies default handling + Handled, // agent consumed it (read: readValue set; write: state updated) + Deferred // read parked; agent will complete it later via ControlChannel +}; + +struct ControlAccess { + bool isWrite = false; // in: true for a store, false for a load + uint64_t offset = 0; // in: byte offset from the control-region base + uint32_t value = 0; // in: store payload (when isWrite) + bool posted = false; // in: store needs no acknowledgement + uint32_t readValue = 0; // out: load result (when returning Handled) +}; + +// Hub handle to complete a previously Deferred read. At most one parked; +// hub answers on whichever transport delivered it. +class ControlChannel { +public: + virtual ~ControlChannel() {} + virtual void completePendingRead(uint32_t value) = 0; +}; + class InterceptionAgentAPI : public SST::SubComponent { public: @@ -31,28 +63,61 @@ class InterceptionAgentAPI : public SST::SubComponent InterceptionAgentAPI(ComponentId_t id, Params& params) : SubComponent(id) {} virtual ~InterceptionAgentAPI() {} - /** Called when Hali intercepts a MemEvent whose address falls in a registered range. - * The agent must produce and send the response (via the provided highlink). - * Returns true if the agent handled the event (Hali will not forward it). */ + /** Intercepted MemEvent: respond on highlink; return true if handled (not forwarded). */ virtual bool handleInterceptedEvent(SST::MemHierarchy::MemEvent* ev, SST::Link* highlink) = 0; - /** Called when a partner Hali signals "done" via the ring. Not all agents need this. */ - virtual void notifyPartnerDone(unsigned iteration) {} + virtual void notifyPartnerDone(unsigned iteration) { (void)iteration; } + + // Default: RingTag::Done -> notifyPartnerDone. Accelerator agents override + // for Cmd/SeqLen/Exit/Done (ringProtocol.h). + virtual void handleRingEvent(SST::Carcosa::HaliEvent* ev) { + if (ev && ev->getStr() == RingTag::Done) { + notifyPartnerDone(ev->getNum()); + } + } - /** Called during setup phase to allow the agent to initialize. */ virtual void agentSetup() {} - /** Optional: set the Hali ring link for sending "done" to partner. Default no-op. */ + // Init-phase hook for agents that own a StandardMem iface (untimed handshake). + // Default no-op for ring-only agents. + virtual void agentInit(unsigned phase) { (void)phase; } + virtual void setRingLink(SST::Link* leftLink) { (void)leftLink; } - /** Optional: set the base address of the intercepted region (e.g. first range). Default no-op. */ virtual void setInterceptBase(uint64_t base) { (void)base; } - /** Optional: set the highlink for sending responses (e.g. command read response). Default no-op. */ virtual void setHighlink(SST::Link* highlink) { (void)highlink; } + // Receive the hub's control channel (for completing Deferred reads). Mirrors + // the setHighlink/setRingLink injection pattern; default ignores it. + virtual void setControlChannel(ControlChannel* ch) { (void)ch; } + + // Transport-neutral control hook. Default: not recognized (hub falls back + // to legacy handling). Override for Hyades ABI on either transport. + virtual ControlResult handleControlAccess(ControlAccess& acc) { + (void)acc; + return ControlResult::Ignored; + } + InterceptionAgentAPI() {} ImplementVirtualSerializable(SST::Carcosa::InterceptionAgentAPI); + +protected: + /** Log, delete ev, return true; no response (reads hang). For unreachable offsets only. */ + bool warnAndDropUnknownIntercept(SST::MemHierarchy::MemEvent* ev, + uint64_t base) { + uint64_t addr = static_cast(ev->getAddr()); + uint64_t off = addr - base; + int cmdIdx = static_cast(ev->getCmd()); + const char* cmdName = SST::MemHierarchy::CommandString[cmdIdx]; + fprintf(stderr, + "[CARCOSA WARN] %s: unhandled intercepted access " + "cmd=%s addr=0x%" PRIx64 " offset=+0x%" PRIx64 + " (event dropped, no response sent)\n", + getName().c_str(), cmdName, addr, off); + delete ev; + return true; + } }; } // namespace Carcosa diff --git a/src/sst/elements/carcosa/components/pingPongAgent.cc b/src/sst/elements/carcosa/components/pingPongAgent.cc index a51b58e68f..2239d99185 100644 --- a/src/sst/elements/carcosa/components/pingPongAgent.cc +++ b/src/sst/elements/carcosa/components/pingPongAgent.cc @@ -6,12 +6,10 @@ // All rights reserved. // // Portions are copyright of other developers: -// See the file CONTRIBUTORS.TXT in the top level directory -// of the distribution for more information. +// See the file CONTRIBUTORS.TXT in the top level directory of the distribution. // -// This file is part of the SST software package. For license -// information, see the LICENSE file in the top level directory of the -// distribution. +// 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 "sst_config.h" #include "sst/elements/carcosa/components/pingPongAgent.h" @@ -65,8 +63,7 @@ bool PingPongAgent::handleInterceptedEvent(MemEvent* ev, Link* highlink) checkBothDone(); return true; } - delete ev; - return true; + return warnAndDropUnknownIntercept(ev, controlAddrBase_); } void PingPongAgent::notifyPartnerDone(unsigned iteration) diff --git a/src/sst/elements/carcosa/components/pingPongAgent.h b/src/sst/elements/carcosa/components/pingPongAgent.h index 3c4b3d3dfd..6d1d2e0379 100644 --- a/src/sst/elements/carcosa/components/pingPongAgent.h +++ b/src/sst/elements/carcosa/components/pingPongAgent.h @@ -6,12 +6,10 @@ // All rights reserved. // // Portions are copyright of other developers: -// See the file CONTRIBUTORS.TXT in the top level directory -// of the distribution for more information. +// See the file CONTRIBUTORS.TXT in the top level directory of the distribution. // -// This file is part of the SST software package. For license -// information, see the LICENSE file in the top level directory of the -// distribution. +// 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 CARCOSA_PINGPONGAGENT_H #define CARCOSA_PINGPONGAGENT_H @@ -27,10 +25,7 @@ namespace SST { namespace Carcosa { /** - * InterceptionAgent that implements the MMIO ping-pong coordination protocol - * used with Vanadis (command register at base+0, status register at base+4). - * Coordinates with a partner Hali via the ring; supports initial_command, - * max_iterations, and exit (-1) semantics. + * MMIO ping-pong (cmd@+0, status@+4) with ring partner; exit on -1. */ class PingPongAgent : public InterceptionAgentAPI { diff --git a/src/sst/elements/carcosa/components/pipelineStateRegistry.h b/src/sst/elements/carcosa/components/pipelineStateRegistry.h new file mode 100644 index 0000000000..23ecec8809 --- /dev/null +++ b/src/sst/elements/carcosa/components/pipelineStateRegistry.h @@ -0,0 +1,199 @@ +// 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. +// +// Portions are copyright of other developers: +// See the file CONTRIBUTORS.TXT in the top level directory of the distribution. +// +// 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 SST_ELEMENTS_CARCOSA_PIPELINE_STATE_REGISTRY_H +#define SST_ELEMENTS_CARCOSA_PIPELINE_STATE_REGISTRY_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace SST { +namespace Carcosa { + +/** + * Labeled memory range for address-in-region predicates (slot index = region id). + */ +struct MemoryRegion { + uint64_t base = 0; + uint64_t size = 0; + bool valid = false; + int id = -1; + /** Optional label for region_names predicates (e.g. "weights"). */ + std::string name; +}; + +/** + * FSM snapshot for PortModules; currentKernelName is the canonical key. + */ +struct PipelineStateBase { + int currentKernel = -1; + std::string currentKernelName; + std::string actuationKernelName = "ACTUATE"; + int pipelineCycle = 0; + + uint64_t stagedBase = 0; + uint64_t stagedSize = 0; + + std::vector regions; + +/** + * EccGuard DUE+drop_frame flag; agents fast-forward FSM then clear. + */ + bool frameAbortRequested = false; + + /** Cumulative count of pipeline cycles that were aborted due to DUE. */ + int framesDropped = 0; + +/** + * Per-frame record for ActionScorer (checksum + escape snapshot at close). + */ + struct FrameRecord { + int pipelineCycle = 0; + int kernelAtClose = -1; + std::string kernelAtCloseName; + // Kernel with the most EccGuard escapes this frame (else kernelAtClose). + int attributingKernel = -1; + std::string attributingKernelName; + bool dropped = false; + uint64_t actionChecksum = 0; + // Quantized-action fingerprint via HYADES_ACTION_TOKEN (sub-bin noise + // insensitive). 0 = unpublished; scorer falls back to checksum. + uint64_t actionToken = 0; + uint64_t cumulativeEscapes = 0; + uint64_t cumulativeFlips = 0; + uint64_t simTimePs = 0; + }; + std::vector frames; + +/** + * Per-frame per-kernel SilentEscape counts; agent argmaxes then resets. + */ + std::unordered_map eccPerFrameEscapesByKernel; + +/** + * Argmax over eccPerFrameEscapesByKernel; "" if empty/all-zero. + */ + std::string argmaxEccPerFrameEscapesByKernel() const { + std::string best_name; + uint64_t best_v = 0; + for (const auto& kv : eccPerFrameEscapesByKernel) { + if (kv.second > best_v) { + best_v = kv.second; + best_name = kv.first; + } + } + return best_name; + } + + /** Helper: zero out the per-frame escape map (consumer at frame close). */ + void resetEccPerFrameEscapesByKernel() { + for (auto& kv : eccPerFrameEscapesByKernel) kv.second = 0u; + } + + /** Cumulative flip/escape counters from EccGuard for per-frame deltas. */ + uint64_t eccCumulativeEscapes = 0; + uint64_t eccCumulativeFlips = 0; + + /** Watcher checksum during ACTUATE; prefer over MMIO when valid. */ + uint64_t watcherActionChecksum = 0; + bool watcherActionChecksumValid = false; + /** True if any CPU-observed byte in the critical window differed this frame. */ + bool watcherCriticalCorrupted = false; + /** Per-run count of frames where watcherCriticalCorrupted was set (finish stat). */ + uint64_t framesCriticalRegionCorrupted = 0; + + /** Returns the region id (== slot index) whose range contains addr, or -1. */ + int regionIdForAddress(uint64_t addr) const { + for (size_t i = 0; i < regions.size(); ++i) { + const MemoryRegion& r = regions[i]; + if (r.valid && addr >= r.base && addr < r.base + r.size) + return static_cast(i); + } + return -1; + } + + /** Grows the region table so that slot `id` is valid (filling intermediate slots). */ + void ensureRegionSlot(size_t id) { + if (regions.size() <= id) { + size_t old = regions.size(); + regions.resize(id + 1); + for (size_t i = old; i <= id; ++i) regions[i].id = static_cast(i); + } + } + + /** Promote staged base/size into regions[id] (HYADES_REGION_* ABI). */ + void commitStagedRegion(size_t id) { + ensureRegionSlot(id); + regions[id].base = stagedBase; + regions[id].size = stagedSize; + regions[id].valid = stagedSize > 0; + regions[id].id = static_cast(id); + stagedBase = 0; + stagedSize = 0; + } + + virtual ~PipelineStateBase() = default; +}; + +/** + * String-keyed snapshot rendezvous; agents getOrCreate, PortModules get() lazily. + */ +template +class PipelineStateRegistry { + static_assert(std::is_base_of::value || + std::is_same::value, + "PipelineStateRegistry: T must derive from PipelineStateBase"); + +public: + /** Returns a pointer to the snapshot for `key`, creating a default-constructed entry if absent. */ + static T* getOrCreate(const std::string& key) { + auto& m = map_(); + auto it = m.find(key); + if (it == m.end()) it = m.emplace(key, T{}).first; + return &it->second; + } + + /** Read-only lookup; returns nullptr if no entry exists for `key`. */ + static const T* get(const std::string& key) { + const auto& m = map_(); + auto it = m.find(key); + return it == m.end() ? nullptr : &it->second; + } + + /** Mutable lookup without insertion; returns nullptr if no entry exists for `key`. */ + static T* getMutable(const std::string& key) { + auto& m = map_(); + auto it = m.find(key); + return it == m.end() ? nullptr : &it->second; + } + + static void clear() { map_().clear(); } + static size_t size() { return map_().size(); } + +private: + static std::map& map_() { + static std::map m; + return m; + } +}; + +} // namespace Carcosa +} // namespace SST + +#endif /* SST_ELEMENTS_CARCOSA_PIPELINE_STATE_REGISTRY_H */ diff --git a/src/sst/elements/carcosa/components/pmDataRegistry.h b/src/sst/elements/carcosa/components/pmDataRegistry.h index 064e138a4f..52159d80b8 100644 --- a/src/sst/elements/carcosa/components/pmDataRegistry.h +++ b/src/sst/elements/carcosa/components/pmDataRegistry.h @@ -26,10 +26,7 @@ namespace SST { namespace Carcosa { -/** - * Parsed Port Module command: " [param1 [param2 ...]]". - * Parameters are kept as strings and converted on-demand via getParam(). - */ +/** Parsed PM command string: command + string params. */ struct PMData { std::string command; std::vector params; @@ -192,10 +189,7 @@ struct ManagerMessage { } }; -/** - * Per-manager registry: maps event IDs to PM command strings, plus a queue of - * PortModule -> Manager messages (e.g. RegisterPM). Look up by id via PMRegistryResolver. - */ +/** Event-id -> PM command map, plus PortModule->Manager message queue. */ class PMDataRegistry { public: PMDataRegistry() = default; diff --git a/src/sst/elements/carcosa/components/vlaRegions.h b/src/sst/elements/carcosa/components/vlaRegions.h new file mode 100644 index 0000000000..d8ba355fd3 --- /dev/null +++ b/src/sst/elements/carcosa/components/vlaRegions.h @@ -0,0 +1,115 @@ +// 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 CARCOSA_VLA_REGIONS_H +#define CARCOSA_VLA_REGIONS_H + +// Publish labeled regions into PipelineStateRegistry for region-aware ECC. +// Slot 0 = MMIO control; CSV NAME:BASE:SIZE fills slots 1..N (hex or decimal). + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace SST { +namespace Carcosa { + +inline uint64_t parseUint64Token(const std::string& tok) { + if (tok.empty()) return 0; + try { + if (tok.size() > 2 && (tok.substr(0, 2) == "0x" || tok.substr(0, 2) == "0X")) + return std::stoull(tok, nullptr, 16); + return std::stoull(tok, nullptr, 10); + } catch (...) { + return 0; + } +} + +inline void trimRegionTok(std::string& s) { + size_t b = 0; + while (b < s.size() && std::isspace(static_cast(s[b]))) ++b; + size_t e = s.size(); + while (e > b && std::isspace(static_cast(s[e - 1]))) --e; + s = s.substr(b, e - b); +} + +/** + * Publish CSV regions into slots 1..N for stateKey; creates snapshot if missing. + */ +inline int publishUserRegions(const std::string& stateKey, + const std::string& csv, + SST::Output* log, + const char* who) { + if (stateKey.empty() || csv.empty()) return 0; + + PipelineStateBase* s = + PipelineStateRegistry::getOrCreate(stateKey); + + int published = 0; + int slot = 1; // slot 0 reserved for mmio_control + std::stringstream ss(csv); + std::string entry; + while (std::getline(ss, entry, ',')) { + trimRegionTok(entry); + if (entry.empty()) continue; + + std::vector parts; + { + std::stringstream es(entry); + std::string p; + while (std::getline(es, p, ':')) { + trimRegionTok(p); + parts.push_back(p); + } + } + if (parts.size() != 3) { + if (log) log->output("%s: regions CSV: malformed entry '%s' (expected name:base:size)\n", + who, entry.c_str()); + continue; + } + + const std::string& name = parts[0]; + uint64_t base = parseUint64Token(parts[1]); + uint64_t size = parseUint64Token(parts[2]); + + s->ensureRegionSlot(slot); + s->regions[slot].base = base; + s->regions[slot].size = size; + s->regions[slot].valid = (size > 0); + s->regions[slot].id = slot; + s->regions[slot].name = name; + ++slot; + ++published; + } + return published; +} + +/** Pop frameAbortRequested once so one DUE => one frame drop. */ +inline bool consumeFrameAbort(const std::string& stateKey) { + if (stateKey.empty()) return false; + PipelineStateBase* s = + PipelineStateRegistry::getMutable(stateKey); + if (!s) return false; + if (!s->frameAbortRequested) return false; + s->frameAbortRequested = false; + s->framesDropped += 1; + return true; +} + +} // namespace Carcosa +} // namespace SST + +#endif /* CARCOSA_VLA_REGIONS_H */ diff --git a/src/sst/elements/carcosa/configure.m4 b/src/sst/elements/carcosa/configure.m4 index a47dc7ab12..a93b6ba299 100644 --- a/src/sst/elements/carcosa/configure.m4 +++ b/src/sst/elements/carcosa/configure.m4 @@ -5,5 +5,21 @@ dnl AC_DEFUN([SST_carcosa_CONFIG], [ carcosa_happy="yes" - AS_IF([test "$carcosa_happy" = "yes"], [$1], [$2]) + dnl Optional balar ring bridge (components/balarRingBridge.{cc,h}). + dnl The bridge #includes balar's CUDA-call packet HEADERS (header-only: packet + dnl structs + encode/decode templates), so it is compiled into libcarcosa only + dnl when GPGPU-Sim (hence balar / CUDA) is available -- mirroring balar's own + dnl SST_CHECK_GPGPUSIM guard. It needs no GPGPU-Sim LIBS (it only exchanges + dnl StandardMem packets with balar), just the CUDA include path (CUDA_CPPFLAGS, + dnl AC_SUBST'd globally by balar's SST_CHECK_CUDA). When GPGPU-Sim is absent the + dnl bridge compiles out and libcarcosa is unchanged -- the standalone invariant. + dnl NOTE: do NOT call SST_CHECK_CUDA here; it defines AM_CONDITIONAL([USE_CUDA]) + dnl which balar already defines -- calling it twice is an automake error. + SST_CHECK_GPGPUSIM([carcosa_have_balar=1],[carcosa_have_balar=0],[carcosa_have_balar=0]) + AS_IF([test "x$carcosa_have_balar" = "x1"], + [AC_DEFINE([HAVE_BALAR_BRIDGE], [1], + [Define if balar/GPGPU-Sim headers are available for the carcosa ring bridge])]) + AM_CONDITIONAL([SST_CARCOSA_HAVE_BALAR], [test "x$carcosa_have_balar" = "x1"]) + + AS_IF([test "$carcosa_happy" = "yes"], [$1], [$2]) ]) diff --git a/src/sst/elements/carcosa/faultlogic/corruptMemFault.h b/src/sst/elements/carcosa/faultlogic/corruptMemFault.h index c56df51da6..de77ff2cc5 100644 --- a/src/sst/elements/carcosa/faultlogic/corruptMemFault.h +++ b/src/sst/elements/carcosa/faultlogic/corruptMemFault.h @@ -24,13 +24,7 @@ namespace SST::Carcosa { typedef std::vector dataVec; typedef SST::MemHierarchy::Addr Addr; -/** - * This fault is intended to be placed on the input/output ports - * of memory components such as DRAM or HBM. Events that pass through - * it, and whose data addresses fall within the ranges set in this - * module's parameters, will have their payloads randomly altered - * to simulate corruption in the affected region of memory. - */ +/** Corrupt payloads whose addresses fall in configured regions. */ class CorruptMemFault : public FaultBase { public: @@ -40,12 +34,6 @@ class CorruptMemFault : public FaultBase CorruptMemFault() = default; ~CorruptMemFault() {} - /** - * 1. Read in event - * 2. Test if event is in specified region - * 3. Corrupt event payload if necessary - * 4. Replace payload - */ bool faultLogic(Event*& ev) override; std::vector* checkAddrUsage(Event*& ev); diff --git a/src/sst/elements/carcosa/faultlogic/randomFlipFault.h b/src/sst/elements/carcosa/faultlogic/randomFlipFault.h index 776d137925..3ee438e3ec 100644 --- a/src/sst/elements/carcosa/faultlogic/randomFlipFault.h +++ b/src/sst/elements/carcosa/faultlogic/randomFlipFault.h @@ -25,10 +25,7 @@ class RandomFlipFault : public FaultBase { bool faultLogic(Event*& ev) override; protected: - /** - * Randomly choose which bit in which byte to flip - * @return (byte, bit) - */ + /** Pick a random (byte, bit) within the payload. */ std::pair pickByteAndBit(size_t payload_sz); protected: void serialize_order(SST::Core::Serialization::serializer& ser) override { diff --git a/src/sst/elements/carcosa/faultlogic/randomFlipMemHFault.h b/src/sst/elements/carcosa/faultlogic/randomFlipMemHFault.h index 47a486fff3..98462a4e29 100644 --- a/src/sst/elements/carcosa/faultlogic/randomFlipMemHFault.h +++ b/src/sst/elements/carcosa/faultlogic/randomFlipMemHFault.h @@ -16,12 +16,7 @@ namespace SST::Carcosa { -/** - * MemHierarchy-aware variant of RandomFlipFault. - * Safely skips events that are not MemEvents or carry no payload - * (e.g. GetS requests, Inv, AckInv), avoiding the modulo-by-zero - * that occurs when pickByteAndBit receives payload_sz == 0. - */ +/** RandomFlipFault that skips non-MemEvent / empty-payload events. */ class RandomFlipMemHFault : public RandomFlipFault { public: RandomFlipMemHFault(Params& params, FaultInjectorBase* injector); diff --git a/src/sst/elements/carcosa/faultlogic/stuckAtFault.h b/src/sst/elements/carcosa/faultlogic/stuckAtFault.h index aad843d30e..dcc6a7c6f6 100644 --- a/src/sst/elements/carcosa/faultlogic/stuckAtFault.h +++ b/src/sst/elements/carcosa/faultlogic/stuckAtFault.h @@ -24,17 +24,7 @@ namespace SST::Carcosa { typedef std::vector dataVec; typedef SST::MemHierarchy::Addr Addr; -/** - * This fault is used to simulate a stuck bit fault. - * To ensure correct operation, make sure that the port module - * using this fault is attached at every point where the data - * for this bit could be read. For example, a stuck bit in the L2 - * cache would need a port module with this fault installed on all - * input OR all output ports to the L2; if the simulator has forwarding enabled, - * but the actual system being simulated does not do the forwarding from memory - * directly into the L1 or the core (bypassing L2 ops in simulation), it may be - * advisable to also place these port modules on the ports used to forward these events. - */ +/** Force configured bits to stuck-at-0 / stuck-at-1 values. */ class StuckAtFault : public FaultBase { public: @@ -44,12 +34,6 @@ class StuckAtFault : public FaultBase StuckAtFault() = default; ~StuckAtFault() {} - /** - * Read event payload and perform the following: - * - If stuckAtMap.at(addr) exists, compare all listed bits with payload value - * - If payload value does not match mapped value, add bit to flip mask - * - Once all stored bit values have been compared, use flip mask to modify address data - */ bool faultLogic(Event*& ev) override; protected: diff --git a/src/sst/elements/carcosa/hyades.h b/src/sst/elements/carcosa/hyades.h index e49184b971..71cc5ed059 100644 --- a/src/sst/elements/carcosa/hyades.h +++ b/src/sst/elements/carcosa/hyades.h @@ -10,104 +10,7 @@ // distribution. /* - * Hyades - Vanadis control abstraction - * - * Single header that encapsulates the MMIO coordination protocol used when - * running under SST/Vanadis with a Hali in the data path. Hali intercepts - * accesses to a small MMIO region and coordinates multiple cores (e.g. ping-pong). - * - * ----------------------------------------------------------------------------- - * BUILD-TIME PARAMETERS (define before #include "hyades.h" if not using default) - * ----------------------------------------------------------------------------- - * - * HYADES_MMIO_BASE - * Optional. Base virtual address of the MMIO control region. Must match - * the Hali parameter "control_addr_base" in the SST config. - * Default: 0xBEEF0000 - * Example: #define HYADES_MMIO_BASE 0xBEEF0000 - * - * ----------------------------------------------------------------------------- - * PROTOCOL (must match Hali MMIO handling) - * ----------------------------------------------------------------------------- - * - * - Base + 0x0 (command register, read): - * The CPU reads the next action index. Value < 0 means "exit" (end the - * run loop). The read may block until Hali has a command ready. - * - * - Base + 0x4 (status register, write): - * The CPU writes the completed action index to signal done. Hali - * coordinates with the partner core, then advances and may unblock - * the next command read. - * - * ----------------------------------------------------------------------------- - * RUNTIME API - * ----------------------------------------------------------------------------- - * - * hyades_command_read(void) - * Returns the next command index (>= 0) or exit sentinel (< 0). - * Parameters: none. - * - * hyades_status_write(int idx) - * Signals completion of the action for index idx. - * Parameters: idx - the command index that was just executed. - * - * hyades_run(hyades_handler_t *handlers, int n_handlers) - * Runs the control loop: read command -> call handler -> write status, - * until the command is exit. Required for the standard use pattern. - * Parameters: - * handlers - array of function pointers (jump table); may be NULL for - * unused indices. - * n_handlers - number of valid entries in handlers (e.g. 2 for ping/pong). - * - * hyades_role_from_argv(int argc, char *argv[]) - * Optional helper. Parses role from argv[1] (e.g. "0" or "1"). Used to - * select which action map this process uses (e.g. role 0: [ping, pong], - * role 1: [pong, ping]). - * Parameters: argc, argv - standard main() arguments. - * Returns: role as int, or 0 if argv[1] is missing or invalid. - * - * hyades_handler_t - * Type: void (*)(void). Each entry in the jump table must have this type. - * - * ----------------------------------------------------------------------------- - * EXAMPLE: pingpong - * ----------------------------------------------------------------------------- - * - * Two processes run the same binary; each gets a role via argv[1]. Role - * selects the order of actions (ping then pong, or pong then ping). The - * run loop is entirely inside hyades_run(). - * - * Build (from the tests/ directory, so hyades.h is found): - * riscv64-unknown-linux-gnu-gcc -static -I.. -o pingpong pingpong.c - * - * Example code: - * - * #include "hyades.h" - * #include - * - * static void ping(void) { write(1, "PING\n", 5); } - * static void pong(void) { write(1, "PONG\n", 5); } - * - * int main(int argc, char *argv[]) { - * int role = hyades_role_from_argv(argc, argv); // optional helper - * - * hyades_handler_t jump_table[2]; - * if (role == 0) { - * jump_table[0] = ping; - * jump_table[1] = pong; - * } else { - * jump_table[0] = pong; - * jump_table[1] = ping; - * } - * - * hyades_run(jump_table, 2); // required: run until Hali sends exit - * return 0; - * } - * - * Required usage: same MMIO base as Hali (default 0xBEEF0000), jump table - * populated for indices 0..n_handlers-1, and exactly one call to - * hyades_run(handlers, n_handlers) so the process participates in the - * coordinated loop until exit. + * Hyades MMIO ABI for Vanadis+Hali (cmd@+0, status@+4). Default base 0xBEEF0000. */ #ifndef HYADES_H #define HYADES_H @@ -118,11 +21,32 @@ #define HYADES_MMIO_BASE 0xBEEF0000 #endif -#define HYADES_COMMAND_OFFSET 0 -#define HYADES_STATUS_OFFSET 4 +#define HYADES_COMMAND_OFFSET 0 +#define HYADES_STATUS_OFFSET 4 +#define HYADES_SEQ_LEN_OFFSET 8 +/* Region-publish ABI: write base_lo/hi, size, then COMMIT; agent latches VAs. */ +#define HYADES_REGION_BASE_LO_OFFSET 0x40 +#define HYADES_REGION_BASE_HI_OFFSET 0x80 +#define HYADES_REGION_SIZE_OFFSET 0xC0 +#define HYADES_REGION_COMMIT_OFFSET 0x100 +/* Action checksum LO (commit). Publish HI then LO after ACTUATE through EccGuard. */ +#define HYADES_ACTION_CHECKSUM_OFFSET 0x140 +/* Decoded-action token: quantized fingerprint; ActionScorer headline metric. */ +#define HYADES_ACTION_TOKEN_OFFSET 0x180 +/* Action-checksum HI; write before LO so latch-on-LO agents see a coherent 64-bit. */ +#define HYADES_ACTION_CHECKSUM_HI_OFFSET 0x1C0 #define HYADES_COMMAND ((volatile int *)(HYADES_MMIO_BASE + HYADES_COMMAND_OFFSET)) #define HYADES_STATUS ((volatile int *)(HYADES_MMIO_BASE + HYADES_STATUS_OFFSET)) +#define HYADES_SEQ_LEN ((volatile int *)(HYADES_MMIO_BASE + HYADES_SEQ_LEN_OFFSET)) + +#define HYADES_REGION_BASE_LO ((volatile unsigned int *)(HYADES_MMIO_BASE + HYADES_REGION_BASE_LO_OFFSET)) +#define HYADES_REGION_BASE_HI ((volatile unsigned int *)(HYADES_MMIO_BASE + HYADES_REGION_BASE_HI_OFFSET)) +#define HYADES_REGION_SIZE ((volatile unsigned int *)(HYADES_MMIO_BASE + HYADES_REGION_SIZE_OFFSET)) +#define HYADES_REGION_COMMIT ((volatile int *)(HYADES_MMIO_BASE + HYADES_REGION_COMMIT_OFFSET)) +#define HYADES_ACTION_CHECKSUM ((volatile unsigned int *)(HYADES_MMIO_BASE + HYADES_ACTION_CHECKSUM_OFFSET)) +#define HYADES_ACTION_CHECKSUM_HI ((volatile unsigned int *)(HYADES_MMIO_BASE + HYADES_ACTION_CHECKSUM_HI_OFFSET)) +#define HYADES_ACTION_TOKEN ((volatile unsigned int *)(HYADES_MMIO_BASE + HYADES_ACTION_TOKEN_OFFSET)) /** * Read next command index from Hali. Value < 0 means exit. @@ -138,16 +62,50 @@ static inline void hyades_status_write(int idx) { *HYADES_STATUS = idx; } +/* Read the VLA agent's current sequence length (decoder-only helper; KV-cache write index). */ +static inline int hyades_seq_len_read(void) { + return *HYADES_SEQ_LEN; +} + +/* + * Register labeled region into regions[slot] on COMMIT; base is the touched VA. + */ +static inline void hyades_register_region(int slot, + unsigned long base, + unsigned long size) { + *HYADES_REGION_BASE_LO = (unsigned int)(base & 0xFFFFFFFFul); + *HYADES_REGION_BASE_HI = (unsigned int)((base >> 32) & 0xFFFFFFFFul); + *HYADES_REGION_SIZE = (unsigned int)size; + *HYADES_REGION_COMMIT = slot; + /* Vanadis store buffer does not drain early-startup stores without a fence */ + __asm__ volatile ("fence iorw, iorw" ::: "memory"); +} + +/* + * Publish ACTUATE action checksum; unpublished => synthetic cycle^seqLen hash. + */ +static inline void hyades_action_checksum_write(unsigned long long checksum) { + /* Publish 64-bit as HI then LO (LO commits); fences order HI/LO/status. + * Legacy 32-bit callers use HI = 0. */ + *HYADES_ACTION_CHECKSUM_HI = (unsigned int)((checksum >> 32) & 0xFFFFFFFFull); + __asm__ volatile ("fence iorw, iorw" ::: "memory"); + *HYADES_ACTION_CHECKSUM = (unsigned int)(checksum & 0xFFFFFFFFull); + __asm__ volatile ("fence iorw, iorw" ::: "memory"); +} + +/* + * Publish decoded-action token before checksum; 0 => scorer uses checksum. + */ +static inline void hyades_action_token_write(unsigned int token) { + *HYADES_ACTION_TOKEN = token; +} + /** * Handler type: no args, no return. */ typedef void (*hyades_handler_t)(void); -/** - * Run the Hyades control loop: repeatedly read command, run handlers[cmd], write status. - * Exits when command is < 0. n_handlers is the number of valid entries in handlers[]. - * If cmd is in [0, n_handlers), handlers[cmd] is called; otherwise only status is written. - */ +/** Control loop: read cmd, run handlers[cmd], write status until cmd < 0. */ static inline void hyades_run(hyades_handler_t *handlers, int n_handlers) { for (;;) { int idx = hyades_command_read(); @@ -159,6 +117,20 @@ static inline void hyades_run(hyades_handler_t *handlers, int n_handlers) { } } +/** Index-passing hyades_run for single-dispatcher workloads. */ +typedef void (*hyades_handler_idx_t)(int idx); + +static inline void hyades_run_idx(hyades_handler_idx_t handler) { + for (;;) { + int idx = hyades_command_read(); + if (idx < 0) + break; + if (handler) + handler(idx); + hyades_status_write(idx); + } +} + /** * Parse role from argv (e.g. argv[1] "0" or "1"). Returns 0 if missing/invalid. */ diff --git a/src/sst/elements/carcosa/injectors/dropFlipFaultInjector.cc b/src/sst/elements/carcosa/injectors/dropFlipFaultInjector.cc index 8671872faf..f17fc8bd44 100644 --- a/src/sst/elements/carcosa/injectors/dropFlipFaultInjector.cc +++ b/src/sst/elements/carcosa/injectors/dropFlipFaultInjector.cc @@ -68,10 +68,7 @@ bool DropFlipFaultInjector::doInjection() { return this->triggered_injection_[0] || this->triggered_injection_[1]; } -/** - * Overridden execution function to cause faults to be chosen at random - * from the vector once a fault has been triggered - */ +/** On trigger, pick drop vs flip at random from the fault vector. */ void DropFlipFaultInjector::executeFaults(Event*& ev) { if (this->triggered_injection_[0]) { // do drop diff --git a/src/sst/elements/carcosa/injectors/faultInjectorBase.cc b/src/sst/elements/carcosa/injectors/faultInjectorBase.cc index 2269593dae..7f0572d945 100644 --- a/src/sst/elements/carcosa/injectors/faultInjectorBase.cc +++ b/src/sst/elements/carcosa/injectors/faultInjectorBase.cc @@ -36,10 +36,7 @@ FaultInjectorBase::FaultInjectorBase(SST::Params& params) : PortModule() } } -/** - * Default behavior is to delete all fault objects in the order they were - * added to the vector - */ +/** Default: delete all fault objects in insertion order. */ FaultInjectorBase::~FaultInjectorBase() { for (int i = 0; i < fault.size(); i++) { if (fault[i]) { @@ -54,7 +51,7 @@ FaultInjectorBase::eventSent(uintptr_t key, Event*& ev) if (!valid_installs_set) { out_->fatal(CALL_INFO_LONG, -1, "Valid installation directions not set -- did you forget to call setValidInstallation() in your constructor?\n"); } - if (doInjection()){ + if (doInjection(ev)){ #ifdef __SST_DEBUG_OUTPUT__ dbg_->debug(CALL_INFO_LONG, 3, 0, "Injection triggered.\n"); #endif @@ -77,7 +74,7 @@ FaultInjectorBase::interceptHandler(uintptr_t key, Event*& ev, bool& cancel) cancel = false; cancel_ = &cancel; - if (doInjection()){ + if (doInjection(ev)){ #ifdef __SST_DEBUG_OUTPUT__ dbg_->debug(CALL_INFO_LONG, 3, 0, "Injection triggered.\n"); #endif @@ -151,10 +148,7 @@ void FaultInjectorBase::setValidInstallation(Params& params, std::array valid_installs_set = true; } -/** - * Default behavior is to execute faults in the order they were - * added to the vector - */ +/** Default: execute faults in insertion order. */ void FaultInjectorBase::executeFaults(Event*& ev) { for (int i = 0; i < fault.size(); i++) { if (fault[i]) { diff --git a/src/sst/elements/carcosa/injectors/faultInjectorBase.h b/src/sst/elements/carcosa/injectors/faultInjectorBase.h index 5f43ca6bb3..30886cec84 100644 --- a/src/sst/elements/carcosa/injectors/faultInjectorBase.h +++ b/src/sst/elements/carcosa/injectors/faultInjectorBase.h @@ -42,14 +42,7 @@ enum installDirection { }; /** - * Base class containing required functions and basic data for - * creating fault injection on component ports. - * - * Injectors are used to execute the logic that tests for - * whether or not an injection should occur. Upon triggering - * an injection, a fault object which inherits from the - * FaultBase class but be used to execute the fault logic - * on the triggering message. + * Port fault-injector base: decide inject, then run FaultBase on the message. */ class FaultInjectorBase : public SST::PortModule { @@ -146,13 +139,16 @@ class FaultInjectorBase : public SST::PortModule protected: virtual bool doInjection(); + + /** + * Event-aware inject decision; default forwards to parameterless doInjection(). + */ + virtual bool doInjection(Event* ev) { (void)ev; return doInjection(); } + virtual void executeFaults(Event*& ev); /** - * This function MUST be called by the derived class constructor - * @arg params pass the same params object to this function - * @arg valid_install_ pass either SEND_VALID, RECEIVE_VALID, - * or SEND_RECEIVE_VALID + * MUST be called from derived ctor with params + SEND/RECEIVE validity. */ void setValidInstallation(Params& params, std::array valid_install); diff --git a/src/sst/elements/carcosa/injectors/faultInjectorMemH.h b/src/sst/elements/carcosa/injectors/faultInjectorMemH.h index 5308c256ff..8956d476cf 100644 --- a/src/sst/elements/carcosa/injectors/faultInjectorMemH.h +++ b/src/sst/elements/carcosa/injectors/faultInjectorMemH.h @@ -22,11 +22,7 @@ namespace SST::Carcosa { -/** - * PortModule for MemHierarchy fault injection with PMDataRegistry support. - * Inherits from FaultInjectorBase; adds PM registry integration so events - * carrying PM data skip fault injection. - */ +/** MemHierarchy fault PortModule; skips events carrying PM registry data. */ class FaultInjectorMemH : public FaultInjectorBase { public: diff --git a/src/sst/elements/carcosa/injectors/randomDropFaultInjector.cc b/src/sst/elements/carcosa/injectors/randomDropFaultInjector.cc index 07b944e2b2..3cde6e4ec9 100644 --- a/src/sst/elements/carcosa/injectors/randomDropFaultInjector.cc +++ b/src/sst/elements/carcosa/injectors/randomDropFaultInjector.cc @@ -36,15 +36,7 @@ bool RandomDropFaultInjector::doInjection() { } } -/** - * Custom execution is required to ensure delivery is canceled - * - * In the base interceptHandler, a reference to a boolean called - * 'cancel' is accepted as an argument. That function assigns the - * injector's pointer (called 'cancel_') to that reference's address, - * and that reference must be updated here after the event is destroyed - * if the installation direction of this PortModule was set to 'Receive'. - */ +/** Drop path must cancel delivery; base interceptHandler is not enough. */ void RandomDropFaultInjector::executeFaults(Event*& ev) { if (fault[0]) { if (this->doInjection()) { diff --git a/src/sst/elements/carcosa/tests/fourstate.c b/src/sst/elements/carcosa/tests/fourstate.c new file mode 100644 index 0000000000..d7de9439d5 --- /dev/null +++ b/src/sst/elements/carcosa/tests/fourstate.c @@ -0,0 +1,104 @@ +/* + * fourstate: 4-kernel demo for FourStateAgent; lowlink traffic for state-gate tests. + */ +#include "hyades.h" +#include + +/* Working-set size: span multiple 64B lines; keep small for Vanadis sim time. */ +#define BUF_LEN 64 + +/* Per-kernel working sets; volatile so loads/stores hit cache/lowlink. */ +static volatile unsigned char K0_buf[BUF_LEN]; /* K0: byte reads */ +static volatile unsigned int K1_tab[BUF_LEN]; /* K1: strided reads */ +static volatile unsigned int K2_src[BUF_LEN]; /* K2: read side */ +static volatile unsigned int K2_dst[BUF_LEN]; /* K2: write side */ +static volatile unsigned int K3_sink[BUF_LEN]; /* K3: writes */ + +/* Call counter once per 0..3 iteration; K2/K3 vary writes for distinct checksums. */ +static unsigned int k_iter = 0; + +static const char *role_tag = "r?"; + +/* Minimal "unsigned -> 8 hex chars + newline" emitter. Avoids printf (and + * its libc-static-init cost) and any dynamic allocation. */ +static void write_hex8(unsigned int v) { + char buf[9]; + for (int i = 7; i >= 0; i--) { + unsigned int nib = v & 0xF; + buf[i] = (nib < 10) ? (char)('0' + nib) : (char)('a' + nib - 10); + v >>= 4; + } + buf[8] = '\n'; + write(1, buf, 9); +} + +/* Emit " v=\n" — e.g. "K2 r0 v=deadbeef\n". */ +static void emit(const char *tag3, unsigned int v) { + write(1, tag3, 3); + write(1, role_tag, 2); + write(1, " v=", 3); + write_hex8(v); +} + +/* K0 — sequential byte reads; dense contiguous 8-bit loads. */ +static void kernel0(void) { + unsigned int sum = 0; + for (int i = 0; i < BUF_LEN; i++) sum += K0_buf[i]; + emit("K0 ", sum); +} + +/* K1 — strided word reads (every 4th); one word per line => cold-line fills. */ +static void kernel1(void) { + unsigned int xorv = 0; + for (int i = 0; i < BUF_LEN; i += 4) xorv ^= K1_tab[i]; + emit("K1 ", xorv); +} + +/* K2 — RMW: load/combine/store. Checksum depends on every store completing. */ +static void kernel2(void) { + for (int i = 0; i < BUF_LEN; i++) { + K2_dst[i] = K2_src[i] + k_iter + (unsigned int)i; + } + emit("K2 ", K2_dst[0] ^ K2_dst[BUF_LEN - 1]); +} + +/* K3 — sequential word writes then read-back checksum. */ +static void kernel3(void) { + unsigned int pat = k_iter * 0x9E3779B1u; /* fractional golden ratio */ + for (int i = 0; i < BUF_LEN; i++) { + K3_sink[i] = pat + (unsigned int)i; + } + unsigned int sum = 0; + for (int i = 0; i < BUF_LEN; i++) sum += K3_sink[i]; + emit("K3 ", sum); + k_iter++; +} + +/* Initialize the read-side working sets with non-zero, address-dependent + * values. Done once at startup so subsequent K0/K1/K2 reads see stable + * deterministic data (absent fault injection). */ +static void init_data(void) { + for (int i = 0; i < BUF_LEN; i++) { + K0_buf[i] = (unsigned char)(i * 3u + 7u); + K1_tab[i] = (unsigned int)i * 0x01010101u + 0x13579BDFu; + K2_src[i] = (unsigned int)i * 0xDEADBEEFu; + K2_dst[i] = 0; + K3_sink[i] = 0; + } +} + +int main(int argc, char *argv[]) { + int role = hyades_role_from_argv(argc, argv); + role_tag = (role == 0) ? "r0" : "r1"; + + init_data(); + + hyades_handler_t jump_table[4]; + jump_table[0] = kernel0; + jump_table[1] = kernel1; + jump_table[2] = kernel2; + jump_table[3] = kernel3; + + hyades_run(jump_table, 4); + return 0; +} diff --git a/src/sst/elements/carcosa/tests/pingpong.c b/src/sst/elements/carcosa/tests/pingpong.c index 53ce68ff9c..98c954353f 100644 --- a/src/sst/elements/carcosa/tests/pingpong.c +++ b/src/sst/elements/carcosa/tests/pingpong.c @@ -13,16 +13,7 @@ // information, see the LICENSE file in the top level directory of the // distribution. -/* - * Ping-pong executable for Vanadis + Hali MMIO coordination. - * Uses hyades.h for all Vanadis control logic (MMIO and run loop). - * - * Build from tests/ with -I.. so hyades.h (in parent carcosa/) is found. - * Example (from carcosa/tests): run from tests/ with parent mounted: - * docker run --rm -v "$(pwd)/..:/src" -w /src/tests ubuntu:22.04 bash -c \ - * 'apt-get update -qq && apt-get install -y -qq gcc-riscv64-linux-gnu && \ - * riscv64-linux-gnu-gcc -static -I.. -o pingpong pingpong.c' - */ +/* Ping-pong workload for Vanadis + Hali MMIO (via hyades.h). */ #include "hyades.h" #include diff --git a/src/sst/elements/carcosa/tests/testcarcosaPingPong.py b/src/sst/elements/carcosa/tests/testCarcosaPingPong.py similarity index 100% rename from src/sst/elements/carcosa/tests/testcarcosaPingPong.py rename to src/sst/elements/carcosa/tests/testCarcosaPingPong.py diff --git a/src/sst/elements/carcosa/tests/testdynamicPM.py b/src/sst/elements/carcosa/tests/testDynamicPM.py similarity index 100% rename from src/sst/elements/carcosa/tests/testdynamicPM.py rename to src/sst/elements/carcosa/tests/testDynamicPM.py diff --git a/src/sst/elements/carcosa/tests/testEccGuardJedecMix.py b/src/sst/elements/carcosa/tests/testEccGuardJedecMix.py new file mode 100644 index 0000000000..f1d821e2e7 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccGuardJedecMix.py @@ -0,0 +1,96 @@ +"""Phase 6 smoke test: EccGuard JEDEC fault-mode mixture. + +Wires CarcosaCPU -> Hali (passthrough) -> L1 -> EccGuard -> CarcosaMemCtrl with +fault_model='jedec_mix', a high fault_event_rate, and the default Sridharan +2015 mode weights. Pass = sst exits 0; the '=== EccGuard ... Fault-Mode Draws +===' block is printed and (when run with -v) cells dominate, devices are rare. + +A statistical assertion that mode counts match the configured weights within ++/-5% over 1e6 events lives in the analyzer (fault_mode_mix.csv); the harness +test runner only checks exit code, so this script focuses on correct wiring + +parser acceptance. +""" +import sst + +# Force memHierarchy library load BEFORE any Carcosa component is constructed. +# On macOS dyld uses a flat-namespace lookup for SimpleMemBackendConvertor's +# RTTI symbol, which is exported by libmemHierarchy.so. If libCarcosa.so loads +# first that lookup fails. Instantiating any memHierarchy component first +# brings memHierarchy.so into dyld's namespace and unblocks the lookup. +l1 = sst.Component("l1", "memHierarchy.Cache") +l1.addParams({ + "access_latency_cycles": "2", + "cache_frequency": "1GHz", + "replacement_policy": "lru", + "coherence_protocol": "MESI", + "associativity": "4", + "cache_line_size": "64", + "cache_size": "8KiB", + "L1": "1", +}) + +cpu = sst.Component("cpu", "carcosa.CarcosaCPU") +cpu.addParams({ + "clock": "1GHz", + "memFreq": "2", + "rngseed": "29", + "memSize": "1MiB", + "verbose": 0, + "maxOutstanding": 8, + "opCount": 500, + "reqsPerIssue": 1, + "write_freq": 30, + "read_freq": 70, +}) +iface = cpu.setSubComponent("memory", "memHierarchy.standardInterface") + +hali = sst.Component("hali", "carcosa.Hali") +hali.addParams({ + "intercept_ranges": "0xBEEF0000,4096", + "verbose": "false", +}) + +ecc = sst.Component("ecc", "carcosa.EccGuard") +ecc.addParams({ + "verbose": "false", + "state_key": "", # no state needed for the mix test + "ecc_scheme": "secded", + "ber": "0.0", + "correctable_latency_ps": "5000", + "due_latency_ps": "20000", + "escape_latency_ps": "0", + "fault_model": "jedec_mix", + "fault_event_rate": "0.5", + # Distinct, non-default weights so the test exercises the parser; values + # are normalized in EccGuard.cc so they need not sum to 1. + "fault_mode_weights": "60:15:10:8:5:2", + "payload_dtype": "bf16", + "due_action": "drop_frame", + "apply_on_responses_only": "true", + "seed": "7", +}) +ecc.enableAllStatistics() + +memctrl = sst.Component("memory", "memHierarchy.MemController") +memctrl.addParams({ + "clock": "1GHz", + "addr_range_end": 1 * 1024 * 1024 - 1, + "backing": "malloc", +}) +backend = memctrl.setSubComponent("backend", "memHierarchy.simpleDRAM") +backend.addParams({ + "mem_size": "1MiB", + "tCAS": 3, "tRCD": 3, "tRP": 3, + "cycle_time": "5ns", + "row_size": "8KiB", + "row_policy": "open", +}) + +sst.Link("cpu_hali_ctrl").connect((cpu, "haliToCPU", "1ns"), (hali, "cpu", "1ns")) +sst.Link("iface_hali").connect((iface, "lowlink", "1ns"), (hali, "highlink", "1ns")) +sst.Link("hali_l1").connect((hali, "lowlink", "1ns"), (l1, "highlink", "1ns")) +sst.Link("l1_ecc").connect((l1, "lowlink", "1ns"), (ecc, "highlink", "1ns")) +sst.Link("ecc_mem").connect((ecc, "lowlink", "1ns"), (memctrl, "highlink", "1ns")) + +sst.setStatisticLoadLevel(4) +sst.setProgramOption("stop-at", "100 us") diff --git a/src/sst/elements/carcosa/tests/testEccGuardRegionPolicy.py b/src/sst/elements/carcosa/tests/testEccGuardRegionPolicy.py new file mode 100644 index 0000000000..7455bbfadc --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccGuardRegionPolicy.py @@ -0,0 +1,113 @@ +"""Phase 6 smoke test: region-aware EccGuard policy parser/lookup. + +Wires CarcosaCPU -> Hali (with FourStateAgent publishing two named DRAM +regions) -> L1 -> EccGuard -> CarcosaMemCtrl. The EccGuard's kernel_policy CSV +exercises the multi-tag '*@REGION', 'KERNEL@REGION', and 'KERNEL' forms in one +pass. Pass = sst exits 0 and the per-kernel and per-kernel-per-region blocks +are printed (visually verifiable in -v mode). +""" +import sst +from mhlib import componentlist # noqa: F401 (registers component types) + +DEBUG = 0 +DEBUG_LVL = 0 + +# Force memHierarchy library load BEFORE any Carcosa component is constructed. +# Same dyld flat-namespace workaround as testEccGuardJedecMix.py. +l1 = sst.Component("l1", "memHierarchy.Cache") +l1.addParams({ + "access_latency_cycles": "2", + "cache_frequency": "1GHz", + "replacement_policy": "lru", + "coherence_protocol": "MESI", + "associativity": "4", + "cache_line_size": "64", + "cache_size": "8KiB", + "L1": "1", + "debug": DEBUG, "debug_level": DEBUG_LVL, +}) + +# 1) CPU + memory interface. +cpu = sst.Component("cpu", "carcosa.CarcosaCPU") +cpu.addParams({ + "clock": "1GHz", + "memFreq": "2", + "rngseed": "13", + "memSize": "1MiB", + "verbose": 0, + "maxOutstanding": 8, + "opCount": 500, + "reqsPerIssue": 1, + "write_freq": 30, + "read_freq": 70, +}) +iface = cpu.setSubComponent("memory", "memHierarchy.standardInterface") + +# 2) Hali in the data path. We use FourStateAgent to publish a state snapshot +# (state_key='ecc_region_smoke') with the MMIO region in slot 0 plus the two +# user-supplied DRAM regions ('weights' and 'kv_cache') in slots 1 and 2 via +# the new 'regions' parameter. EccGuard reads addr -> region from this snapshot. +hali = sst.Component("hali", "carcosa.Hali") +hali.addParams({ + "intercept_ranges": "0xBEEF0000,4096", + "verbose": "false", +}) +agent = hali.setSubComponent("interceptionAgent", "carcosa.FourStateAgent") +agent.addParams({ + "state_key": "ecc_region_smoke", + "initial_command":"0", + "num_commands": "2", + "max_iterations": "0", # CPU never hits MMIO; agent stays idle. + "verbose": "false", + # Two named regions covering most of the [0, 1MiB) address space the + # CarcosaCPU touches. EccGuard routes by the first matching base..base+size. + "regions": "weights:0x0:0x80000,kv_cache:0x80000:0x80000", +}) + +ecc = sst.Component("ecc", "carcosa.EccGuard") +ecc.addParams({ + "verbose": "false", + "state_key": "ecc_region_smoke", + "ecc_scheme": "secded", + "ber": "1e-7", + "correctable_latency_ps": "5000", + "due_latency_ps": "20000", + "escape_latency_ps": "0", + # Multi-tag kernel_policy: kernel-only, region-only, and kernel x region. + "kernel_policy": ( + "IDLE:none:0:0:0:0," + "*@weights:chipkill:1e-7:8000:30000:0," + "*@kv_cache:secded:1e-7:5000:20000:0" + ), + "apply_on_responses_only": "true", + "fault_model": "poisson", + "due_action": "latency_only", + "seed": "1", +}) +ecc.enableAllStatistics() + +memctrl = sst.Component("memory", "memHierarchy.MemController") +memctrl.addParams({ + "clock": "1GHz", + "addr_range_end": 1 * 1024 * 1024 - 1, + "backing": "malloc", +}) +backend = memctrl.setSubComponent("backend", "memHierarchy.simpleDRAM") +backend.addParams({ + "mem_size": "1MiB", + "tCAS": 3, "tRCD": 3, "tRP": 3, + "cycle_time": "5ns", + "row_size": "8KiB", + "row_policy": "open", +}) + +# Wire up. CarcosaCPU.haliToCPU connects to Hali.cpu; Hali sits in the data path +# between iface and L1. EccGuard sits between L1 and the memCtrl. +sst.Link("cpu_hali_ctrl").connect((cpu, "haliToCPU", "1ns"), (hali, "cpu", "1ns")) +sst.Link("iface_hali").connect((iface, "lowlink", "1ns"), (hali, "highlink", "1ns")) +sst.Link("hali_l1").connect((hali, "lowlink", "1ns"), (l1, "highlink", "1ns")) +sst.Link("l1_ecc").connect((l1, "lowlink", "1ns"), (ecc, "highlink", "1ns")) +sst.Link("ecc_mem").connect((ecc, "lowlink", "1ns"), (memctrl, "highlink", "1ns")) + +sst.setStatisticLoadLevel(4) +sst.setProgramOption("stop-at", "100 us") diff --git a/src/sst/elements/carcosa/tests/testEccGuardResident.py b/src/sst/elements/carcosa/tests/testEccGuardResident.py new file mode 100644 index 0000000000..03c3f6e84b --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccGuardResident.py @@ -0,0 +1,98 @@ +"""EccGuard resident fault-map smoke test (fault_model='resident'). + +Wires CarcosaCPU -> Hali (passthrough) -> L1 -> EccGuard -> CarcosaMemCtrl with +a persistent, address-keyed fault map: faults are born at t=0 and by a Poisson +process in sim time, live at physical locations with mode-shaped footprints +(cell/word/row/column/bank/device confined to one x4 chip), corrupt reads +deterministically, and are cleared only by patrol scrub. + +Pass = sst exits 0 and the '=== EccGuard ... Resident Fault Map Summary ===' +block is printed. Paired-comparison property (same seed => identical fault set +under a different ecc_scheme) is checked by rerunning with +ECC_TEST_SCHEME=chipkill and diffing the Fault-Mode Draws block. +""" +import os +import sst + +# Force memHierarchy library load BEFORE any Carcosa component is constructed +# (macOS dyld flat-namespace workaround; see testEccGuardJedecMix.py). +l1 = sst.Component("l1", "memHierarchy.Cache") +l1.addParams({ + "access_latency_cycles": "2", + "cache_frequency": "1GHz", + "replacement_policy": "lru", + "coherence_protocol": "MESI", + "associativity": "4", + "cache_line_size": "64", + "cache_size": "8KiB", + "L1": "1", +}) + +cpu = sst.Component("cpu", "carcosa.CarcosaCPU") +cpu.addParams({ + "clock": "1GHz", + "memFreq": "2", + "rngseed": "29", + "memSize": "1MiB", + "verbose": 0, + "maxOutstanding": 8, + "opCount": 20000, + "reqsPerIssue": 1, + "write_freq": 30, + "read_freq": 70, +}) +iface = cpu.setSubComponent("memory", "memHierarchy.standardInterface") + +hali = sst.Component("hali", "carcosa.Hali") +hali.addParams({ + "intercept_ranges": "0xBEEF0000,4096", + "verbose": "false", +}) + +ecc = sst.Component("ecc", "carcosa.EccGuard") +ecc.addParams({ + "verbose": "false", + "state_key": "", + "ecc_scheme": os.environ.get("ECC_TEST_SCHEME", "secded"), + "ber": "0.0", + "correctable_latency_ps": "5000", + "due_latency_ps": "20000", + "escape_latency_ps": "0", + "fault_model": "resident", + "resident_addr_start": "0", + "resident_addr_len": str(1024 * 1024), + "resident_faults_at_start": os.environ.get("ECC_TEST_FAULTS", "12"), + "resident_fault_rate_per_ms": os.environ.get("ECC_TEST_RATE", "100"), + "resident_scrub_interval_us": "50", + "resident_permanent_fraction": "0.3", + "resident_mode": os.environ.get("ECC_TEST_MODE", "mix"), + "payload_dtype": "bf16", + "due_action": "latency_only", + "apply_on_responses_only": "true", + "seed": "7", +}) +ecc.enableAllStatistics() + +memctrl = sst.Component("memory", "memHierarchy.MemController") +memctrl.addParams({ + "clock": "1GHz", + "addr_range_end": 1 * 1024 * 1024 - 1, + "backing": "malloc", +}) +backend = memctrl.setSubComponent("backend", "memHierarchy.simpleDRAM") +backend.addParams({ + "mem_size": "1MiB", + "tCAS": 3, "tRCD": 3, "tRP": 3, + "cycle_time": "5ns", + "row_size": "8KiB", + "row_policy": "open", +}) + +sst.Link("cpu_hali_ctrl").connect((cpu, "haliToCPU", "1ns"), (hali, "cpu", "1ns")) +sst.Link("iface_hali").connect((iface, "lowlink", "1ns"), (hali, "highlink", "1ns")) +sst.Link("hali_l1").connect((hali, "lowlink", "1ns"), (l1, "highlink", "1ns")) +sst.Link("l1_ecc").connect((l1, "lowlink", "1ns"), (ecc, "highlink", "1ns")) +sst.Link("ecc_mem").connect((ecc, "lowlink", "1ns"), (memctrl, "highlink", "1ns")) + +sst.setStatisticLoadLevel(4) +sst.setProgramOption("stop-at", "2000 us") diff --git a/src/sst/elements/carcosa/tests/testEccGuardSmoke.py b/src/sst/elements/carcosa/tests/testEccGuardSmoke.py new file mode 100644 index 0000000000..b959bafa2e --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccGuardSmoke.py @@ -0,0 +1,96 @@ +"""Fast smoke test for carcosa.EccGuard: CarcosaCPU -> Hali -> L1 -> EccGuard -> memCtrl. + +Knobs come from the environment (ECC_SCHEME, ECC_BER, ECC_*_LATENCY_PS, +ECC_KERNEL_POLICY) so the same config doubles as a quick spot-check driver. +Pass = sst exits 0 and the EccGuard outcome tables print. + +Traffic comes from carcosa.CarcosaCPU rather than miranda: the local +build-sst-carcosa.sh tree compiles only the VLA subset of sst-elements +(carcosa, memHierarchy, vanadis, merlin, mmu), so miranda is not available. +""" +import os +import sst + +ecc_scheme = os.getenv("ECC_SCHEME", "none") +ecc_ber = os.getenv("ECC_BER", "0.0") +ecc_correctable_ps = os.getenv("ECC_CORRECTABLE_LATENCY_PS", "0") +ecc_due_ps = os.getenv("ECC_DUE_LATENCY_PS", "0") +ecc_escape_ps = os.getenv("ECC_ESCAPE_LATENCY_PS", "0") +ecc_kernel_policy = os.getenv("ECC_KERNEL_POLICY", "") + +sst.setStatisticLoadLevel(6) + +# Force memHierarchy library load BEFORE any Carcosa component is constructed. +# On macOS dyld uses a flat-namespace lookup for SimpleMemBackendConvertor's +# RTTI symbol, which is exported by libmemHierarchy.so. If libCarcosa.so loads +# first that lookup fails. Instantiating any memHierarchy component first +# brings memHierarchy.so into dyld's namespace and unblocks the lookup. +l1 = sst.Component("l1cache", "memHierarchy.Cache") +l1.addParams({ + "access_latency_cycles": "2", + "cache_frequency": "2.4 GHz", + "replacement_policy": "lru", + "coherence_protocol": "MESI", + "associativity": "4", + "cache_line_size": "64", + "L1": "1", + "cache_size": "32KB", +}) + +cpu = sst.Component("cpu", "carcosa.CarcosaCPU") +cpu.addParams({ + "clock": "2.4GHz", + "memFreq": "2", + "rngseed": "29", + "memSize": "1MiB", + "verbose": 0, + "maxOutstanding": 8, + "opCount": 1000, + "reqsPerIssue": 1, + "write_freq": 30, + "read_freq": 70, +}) +iface = cpu.setSubComponent("memory", "memHierarchy.standardInterface") + +hali = sst.Component("hali", "carcosa.Hali") +hali.addParams({ + "intercept_ranges": "0xBEEF0000,4096", + "verbose": "false", +}) + +memctrl = sst.Component("memory", "memHierarchy.MemController") +memctrl.addParams({ + "clock": "1GHz", + "addr_range_end": 1 * 1024 * 1024 - 1, + "backing": "malloc", +}) +backend = memctrl.setSubComponent("backend", "memHierarchy.simpleMem") +backend.addParams({ + "access_time": "100 ns", + "mem_size": "1MiB", +}) + +ecc = sst.Component("ecc_guard", "carcosa.EccGuard") +ecc.addParams({ + "verbose": "true", + "state_key": "", + "ecc_scheme": ecc_scheme, + "ber": ecc_ber, + "correctable_latency_ps": ecc_correctable_ps, + "due_latency_ps": ecc_due_ps, + "escape_latency_ps": ecc_escape_ps, + "kernel_policy": ecc_kernel_policy, + "apply_on_responses_only": "true", + "seed": "1", +}) +ecc.enableAllStatistics() + +sst.Link("cpu_hali_ctrl").connect((cpu, "haliToCPU", "1ns"), (hali, "cpu", "1ns")) +sst.Link("iface_hali").connect((iface, "lowlink", "1ns"), (hali, "highlink", "1ns")) +sst.Link("hali_l1").connect((hali, "lowlink", "1ns"), (l1, "highlink", "1ns")) +sst.Link("l1_ecc").connect((l1, "lowlink", "50ps"), (ecc, "highlink", "50ps")) +sst.Link("ecc_mem").connect((ecc, "lowlink", "50ps"), (memctrl, "highlink", "50ps")) + +# No stop-at: the CPU votes to end the sim once opCount drains, so a stalled +# run hangs visibly (CI timeout) instead of hitting a deadline that still +# prints plausible-looking outcome tables and exits 0. diff --git a/src/sst/elements/carcosa/tests/testFourStateRegistry.py b/src/sst/elements/carcosa/tests/testFourStateRegistry.py new file mode 100644 index 0000000000..6b9959c714 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testFourStateRegistry.py @@ -0,0 +1,361 @@ +""" +Vanadis + memHierarchy + Hali + FourStateAgent stack, demonstrating +PipelineStateRegistry publishing from a full-stack simulation. + +This is the same wiring as testCarcosaPingPong.py, with the PingPongAgent +replaced by FourStateAgent on each Hali. The binary, hyades.h ABI, and +ring-coordination behavior are unchanged: each core runs pingpong.c, reads +commands from MMIO, executes ping/pong, and writes status. What's new is +that each FourStateAgent publishes its FSM state into +PipelineStateRegistry under a per-core `state_key`: + + state_key="core0" for Hali on node0.cpu0 + state_key="core1" for Hali on node0.cpu1 + +Each registry snapshot contains: + - currentKernel : the CPU command index currently in flight (0..3 when a + handler is actively running, -1 when the CPU is idle + between kernels). FourStateAgent transitions this to + the command value at the instant the response is sent + back to the core, and to -1 when the status write + arrives. A PortModule with kernels="2" therefore + matches exactly the window during which the CPU is + running K2. + - pipelineCycle : number of completed full passes through all 4 kernels + (= floor(currentIteration / num_commands)). + - regions[0] : the MMIO control region, name="mmio_control", + base=0xBEEF0000, size=4096 + +Binary prerequisites (cross-compile fourstate.c for RISC-V): + riscv64-unknown-linux-gnu-gcc -static -I.. -o fourstate fourstate.c + (run from tests/, or set VANADIS_EXE to the binary path) + +The binary exposes 4 kernels (K0..K3) matching the agent's 4 command +indices. Per-iteration per-core output is written to stdout-100 / +stdout-101 (cat after the run to see K0/K1/K2/K3 interleaved with role +tags r0/r1). + +No gate is configured here; see testFourStateRegistryGated.py for the variant +that attaches PortModuleStateGate to Hali's lowlink port and consumes the +registry snapshot this test produces. +""" +import os +import sst + +mh_debug_level = 10 +mh_debug = 0 +checkpointDir = "" +checkpoint = "" +pythonDebug = False + +vanadis_isa = os.getenv("VANADIS_ISA", "RISCV64") +isa = "riscv64" +loader_mode = os.getenv("VANADIS_LOADER_MODE", "0") +lib = "vanadis" + +full_exe_name = os.getenv("VANADIS_EXE", "./fourstate") +exe_name = full_exe_name.split("/")[-1] + +physMemSize = "4GiB" +tlbType = "simpleTLB" +mmuType = "simpleMMU" + +sst.setProgramOption("timebase", "1ps") +sst.setProgramOption("stop-at", "0 ns") +sst.setStatisticLoadLevel(4) +sst.setStatisticOutput("sst.statOutputConsole") + +verbosity = int(os.getenv("VANADIS_VERBOSE", 0)) +os_verbosity = os.getenv("VANADIS_OS_VERBOSE", str(verbosity)) +pipe_trace_file = os.getenv("VANADIS_PIPE_TRACE", "") +lsq_ld_entries = os.getenv("VANADIS_LSQ_LD_ENTRIES", 16) +lsq_st_entries = os.getenv("VANADIS_LSQ_ST_ENTRIES", 8) +rob_slots = os.getenv("VANADIS_ROB_SLOTS", 64) +retires_per_cycle = os.getenv("VANADIS_RETIRES_PER_CYCLE", 4) +issues_per_cycle = os.getenv("VANADIS_ISSUES_PER_CYCLE", 4) +decodes_per_cycle = os.getenv("VANADIS_DECODES_PER_CYCLE", 4) +integer_arith_cycles = int(os.getenv("VANADIS_INTEGER_ARITH_CYCLES", 2)) +integer_arith_units = int(os.getenv("VANADIS_INTEGER_ARITH_UNITS", 2)) +fp_arith_cycles = int(os.getenv("VANADIS_FP_ARITH_CYCLES", 8)) +fp_arith_units = int(os.getenv("VANADIS_FP_ARITH_UNITS", 2)) +branch_arith_cycles = int(os.getenv("VANADIS_BRANCH_ARITH_CYCLES", 2)) +cpu_clock = os.getenv("VANADIS_CPU_CLOCK", "2.3GHz") + +numCpus = 2 +numThreads = 1 + +vanadis_cpu_type = lib + "." + os.getenv("VANADIS_CPU_ELEMENT_NAME", "dbg_VanadisCPU") +vanadis_decoder = lib + ".Vanadis" + vanadis_isa + "Decoder" +vanadis_os_hdlr = lib + ".Vanadis" + vanadis_isa + "OSHandler" +protocol = "MESI" + +# Per-core registry keys. PortModuleStateGate in the gated variant uses these. +STATE_KEYS = ["core0", "core1"] + +# If set, attach a PortModuleStateGate to each Hali's lowlink port with these +# params. The baseline test leaves it None (no gate installed). +GATE_PARAMS_PER_CORE = None + +osParams = { + "processDebugLevel": 0, + "dbgLevel": os_verbosity, + "dbgMask": 8, + "cores": numCpus, + "hardwareThreadCount": numThreads, + "page_size": 4096, + "physMemSize": physMemSize, + "useMMU": True, + "checkpointDir": checkpointDir, + "checkpoint": checkpoint, +} + +processList = ( + (1, { + "env_count": 1, + "env0": "OMP_NUM_THREADS=2", + "exe": full_exe_name, + "arg0": exe_name, + "arg1": "0", + "argc": 2, + }), + (1, { + "env_count": 1, + "env0": "OMP_NUM_THREADS=2", + "exe": full_exe_name, + "arg0": exe_name, + "arg1": "1", + "argc": 2, + }), +) + +osl1cacheParams = {"access_latency_cycles": "2", "cache_frequency": cpu_clock, "replacement_policy": "lru", + "coherence_protocol": protocol, "associativity": "8", "cache_line_size": "64", + "cache_size": "32 KB", "L1": "1", "debug": mh_debug, "debug_level": mh_debug_level} +mmuParams = {"debug_level": 0, "num_cores": numCpus, "num_threads": numThreads, "page_size": 4096} +memRtrParams = {"xbar_bw": "1GB/s", "link_bw": "1GB/s", "input_buf_size": "2KB", "num_ports": str(numCpus + 2), + "flit_size": "72B", "output_buf_size": "2KB", "id": "0", "topology": "merlin.singlerouter"} +dirCtrlParams = {"coherence_protocol": protocol, "entry_cache_size": "1024", "debug": mh_debug, + "debug_level": mh_debug_level, "addr_range_start": "0x0", "addr_range_end": "0xFFFFFFFF"} +dirNicParams = {"network_bw": "25GB/s", "group": 2} +memCtrlParams = {"clock": cpu_clock, "backend.mem_size": physMemSize, "backing": "malloc", "initBacking": 1, + "addr_range_start": 0, "addr_range_end": 0xffffffff, "debug_level": mh_debug_level, + "debug": mh_debug, "checkpointDir": checkpointDir, "checkpoint": checkpoint} +memParams = {"mem_size": "4GiB", "access_time": "1 ns"} +tlbParams = {"debug_level": 0, "hit_latency": 1, "num_hardware_threads": numThreads, + "num_tlb_entries_per_thread": 64, "tlb_set_size": 4} +tlbWrapperParams = {"debug_level": 0} +decoderParams = {"loader_mode": loader_mode, "uop_cache_entries": 1536, "predecode_cache_entries": 4} +osHdlrParams = {} +branchPredParams = {"branch_entries": 32} +cpuParams = {"clock": cpu_clock, "verbose": verbosity, "hardware_threads": numThreads, + "physical_fp_registers": 168 * numThreads, "physical_integer_registers": 180 * numThreads, + "integer_arith_cycles": integer_arith_cycles, "integer_arith_units": integer_arith_units, + "fp_arith_cycles": fp_arith_cycles, "fp_arith_units": fp_arith_units, + "branch_unit_cycles": branch_arith_cycles, "print_int_reg": False, "print_fp_reg": False, + "pipeline_trace_file": pipe_trace_file, "reorder_slots": rob_slots, + "decodes_per_cycle": decodes_per_cycle, "issues_per_cycle": issues_per_cycle, + "retires_per_cycle": retires_per_cycle, "pause_when_retire_address": 0, + "start_verbose_when_issue_address": "0", "stop_verbose_when_retire_address": "0", + "print_rob": False, "checkpointDir": checkpointDir, "checkpoint": checkpoint} +lsqParams = {"verbose": verbosity, "address_mask": 0xFFFFFFFF, "max_stores": lsq_st_entries, "max_loads": lsq_ld_entries} +l1dcacheParams = {"access_latency_cycles": "2", "cache_frequency": cpu_clock, "replacement_policy": "lru", + "coherence_protocol": protocol, "associativity": "8", "cache_line_size": "64", + "cache_size": "32 KB", "L1": "1", "debug": mh_debug, "debug_level": mh_debug_level} +l1icacheParams = {"access_latency_cycles": "2", "cache_frequency": cpu_clock, "replacement_policy": "lru", + "coherence_protocol": protocol, "associativity": "8", "cache_line_size": "64", + "cache_size": "32 KB", "prefetcher": "cassini.NextBlockPrefetcher", "prefetcher.reach": 1, + "L1": "1", "debug": mh_debug, "debug_level": mh_debug_level} +l2cacheParams = {"access_latency_cycles": "14", "cache_frequency": cpu_clock, "replacement_policy": "lru", + "coherence_protocol": protocol, "associativity": "16", "cache_line_size": "64", + "cache_size": "1MB", "mshr_latency_cycles": 3, "debug": mh_debug, "debug_level": mh_debug_level} +busParams = {"bus_frequency": cpu_clock} +l2memLinkParams = {"group": 1, "network_bw": "25GB/s"} + + +def addParamsPrefix(prefix, params): + return {prefix + "." + k: v for k, v in params.items()} + + +class CPU_Builder: + def __init__(self): + pass + + def build(self, prefix, nodeId, cpuId): + if pythonDebug: + print("build {}".format(prefix)) + + cpu = sst.Component(prefix, vanadis_cpu_type) + cpu.addParams(cpuParams) + cpu.addParam("core_id", cpuId) + cpu.enableAllStatistics() + + for n in range(numThreads): + decode = cpu.setSubComponent("decoder", vanadis_decoder, n) + decode.addParams(decoderParams) + decode.enableAllStatistics() + os_hdlr = decode.setSubComponent("os_handler", vanadis_os_hdlr) + os_hdlr.addParams(osHdlrParams) + branch_pred = decode.setSubComponent("branch_unit", lib + ".VanadisBasicBranchUnit") + branch_pred.addParams(branchPredParams) + branch_pred.enableAllStatistics() + + cpu_lsq = cpu.setSubComponent("lsq", lib + ".VanadisBasicLoadStoreQueue") + cpu_lsq.addParams(lsqParams) + cpu_lsq.enableAllStatistics() + cpuDcacheIf = cpu_lsq.setSubComponent("memory_interface", "memHierarchy.standardInterface") + cpuIcacheIf = cpu.setSubComponent("mem_interface_inst", "memHierarchy.standardInterface") + + cpu_l1dcache = sst.Component(prefix + ".l1dcache", "memHierarchy.Cache") + cpu_l1dcache.addParams(l1dcacheParams) + cpu_l1icache = sst.Component(prefix + ".l1icache", "memHierarchy.Cache") + cpu_l1icache.addParams(l1icacheParams) + cpu_l2cache = sst.Component(prefix + ".l2cache", "memHierarchy.Cache") + cpu_l2cache.addParams(l2cacheParams) + l2cache_2_mem = cpu_l2cache.setSubComponent("lowlink", "memHierarchy.MemNIC") + l2cache_2_mem.addParams(l2memLinkParams) + cache_bus = sst.Component(prefix + ".bus", "memHierarchy.Bus") + cache_bus.addParams(busParams) + + dtlbWrapper = sst.Component(prefix + ".dtlb", "mmu.tlb_wrapper") + dtlbWrapper.addParams(tlbWrapperParams) + dtlb = dtlbWrapper.setSubComponent("tlb", "mmu." + tlbType) + dtlb.addParams(tlbParams) + itlbWrapper = sst.Component(prefix + ".itlb", "mmu.tlb_wrapper") + itlbWrapper.addParams(tlbWrapperParams) + itlbWrapper.addParam("exe", True) + itlb = itlbWrapper.setSubComponent("tlb", "mmu." + tlbType) + itlb.addParams(tlbParams) + + # Hali in data path: CPU -> Hali -> dTLB -> L1D + hali = sst.Component(prefix + ".hali", "carcosa.Hali") + hali.addParams({ + "intercept_ranges": "0xBEEF0000,4096", + "verbose": "true", + }) + agent = hali.setSubComponent("interceptionAgent", "carcosa.FourStateAgent") + agent.addParams({ + "state_key": STATE_KEYS[cpuId], + "initial_command": "0", + "num_commands": "4", + "max_iterations": "12", + "region_size": "4096", + "verbose": "true", + }) + + # Optional gate on the Hali lowlink port (toward L1D). The gated + # variant of this test sets GATE_PARAMS_PER_CORE to a dict mapping + # state_key -> params; see testFourStateRegistryGated.py. + if GATE_PARAMS_PER_CORE is not None: + gate_params = GATE_PARAMS_PER_CORE.get(STATE_KEYS[cpuId]) + if gate_params is not None: + hali.addPortModule("lowlink", "carcosa.PortModuleStateGate", gate_params) + + link_cpu_hali = sst.Link(prefix + ".link_cpu_hali") + link_cpu_hali.connect((cpuDcacheIf, "lowlink", "1ns"), (hali, "highlink", "1ns")) + link_cpu_hali.setNoCut() + link_hali_dtlb = sst.Link(prefix + ".link_hali_dtlb") + link_hali_dtlb.connect((hali, "lowlink", "1ns"), (dtlbWrapper, "cpu_if", "1ns")) + link_hali_dtlb.setNoCut() + + link_cpu_l1dcache_link = sst.Link(prefix + ".link_cpu_l1dcache_link") + link_cpu_l1dcache_link.connect((dtlbWrapper, "cache_if", "1ns"), (cpu_l1dcache, "highlink", "1ns")) + link_cpu_l1dcache_link.setNoCut() + + link_cpu_itlb_link = sst.Link(prefix + ".link_cpu_itlb_link") + link_cpu_itlb_link.connect((cpuIcacheIf, "lowlink", "1ns"), (itlbWrapper, "cpu_if", "1ns")) + link_cpu_itlb_link.setNoCut() + link_cpu_l1icache_link = sst.Link(prefix + ".link_cpu_l1icache_link") + link_cpu_l1icache_link.connect((itlbWrapper, "cache_if", "1ns"), (cpu_l1icache, "highlink", "1ns")) + link_cpu_l1icache_link.setNoCut() + + link_l1dcache_l2cache_link = sst.Link(prefix + ".link_l1dcache_l2cache_link") + link_l1dcache_l2cache_link.connect((cpu_l1dcache, "lowlink", "1ns"), (cache_bus, "highlink0", "1ns")) + link_l1dcache_l2cache_link.setNoCut() + link_l1icache_l2cache_link = sst.Link(prefix + ".link_l1icache_l2cache_link") + link_l1icache_l2cache_link.connect((cpu_l1icache, "lowlink", "1ns"), (cache_bus, "highlink1", "1ns")) + link_l1icache_l2cache_link.setNoCut() + link_bus_l2cache_link = sst.Link(prefix + ".link_bus_l2cache_link") + link_bus_l2cache_link.connect((cache_bus, "lowlink0", "1ns"), (cpu_l2cache, "highlink", "1ns")) + link_bus_l2cache_link.setNoCut() + + return (cpu, "os_link", "5ns"), (l2cache_2_mem, "port", "1ns"), (dtlb, "mmu", "1ns"), (itlb, "mmu", "1ns"), hali + + +def build_topology(): + """Build the full Vanadis/memH/Hali topology. Returns nothing; side-effects + only (adds components/links to the SST configuration). + + Callers that want to enable the PortModuleStateGate should set + GATE_PARAMS_PER_CORE at module scope before invoking build_topology(). + """ + node_os = sst.Component("os", lib + ".VanadisNodeOS") + node_os.addParams(osParams) + num = 0 + for i, process in processList: + for _ in range(i): + node_os.addParams(addParamsPrefix("process" + str(num), process)) + num += 1 + + node_os_mmu = node_os.setSubComponent("mmu", "mmu." + mmuType) + node_os_mmu.addParams(mmuParams) + node_os_mem_if = node_os.setSubComponent("mem_interface", "memHierarchy.standardInterface") + os_cache = sst.Component("node_os.cache", "memHierarchy.Cache") + os_cache.addParams(osl1cacheParams) + os_cache_2_mem = os_cache.setSubComponent("lowlink", "memHierarchy.MemNIC") + os_cache_2_mem.addParams(l2memLinkParams) + + comp_chiprtr = sst.Component("chiprtr", "merlin.hr_router") + comp_chiprtr.addParams(memRtrParams) + comp_chiprtr.setSubComponent("topology", "merlin.singlerouter") + dirctrl = sst.Component("dirctrl", "memHierarchy.DirectoryController") + dirctrl.addParams(dirCtrlParams) + dirNIC = dirctrl.setSubComponent("highlink", "memHierarchy.MemNIC") + dirNIC.addParams(dirNicParams) + memctrl = sst.Component("memory", "memHierarchy.MemController") + memctrl.addParams(memCtrlParams) + memory = memctrl.setSubComponent("backend", "memHierarchy.simpleMem") + memory.addParams(memParams) + + link_dir_2_rtr = sst.Link("link_dir_2_rtr") + link_dir_2_rtr.connect((comp_chiprtr, "port" + str(numCpus), "1ns"), (dirNIC, "port", "1ns")) + link_dir_2_rtr.setNoCut() + link_dir_2_mem = sst.Link("link_dir_2_mem") + link_dir_2_mem.connect((dirctrl, "lowlink", "1ns"), (memctrl, "highlink", "1ns")) + link_dir_2_mem.setNoCut() + link_os_cache_link = sst.Link("link_os_cache_link") + link_os_cache_link.connect((node_os_mem_if, "lowlink", "1ns"), (os_cache, "highlink", "1ns")) + link_os_cache_link.setNoCut() + os_cache_2_rtr = sst.Link("os_cache_2_rtr") + os_cache_2_rtr.connect((os_cache_2_mem, "port", "1ns"), (comp_chiprtr, "port" + str(numCpus + 1), "1ns")) + os_cache_2_rtr.setNoCut() + + cpuBuilder = CPU_Builder() + nodeId = 0 + halis = [] + for cpu in range(numCpus): + prefix = "node" + str(nodeId) + ".cpu" + str(cpu) + os_hdlr, l2cache, dtlb, itlb, hali = cpuBuilder.build(prefix, nodeId, cpu) + halis.append(hali) + + link_mmu_dtlb_link = sst.Link(prefix + ".link_mmu_dtlb_link") + link_mmu_dtlb_link.connect((node_os_mmu, "core" + str(cpu) + ".dtlb", "1ns"), dtlb) + link_mmu_itlb_link = sst.Link(prefix + ".link_mmu_itlb_link") + link_mmu_itlb_link.connect((node_os_mmu, "core" + str(cpu) + ".itlb", "1ns"), itlb) + link_core_os_link = sst.Link(prefix + ".link_core_os_link") + link_core_os_link.connect(os_hdlr, (node_os, "core" + str(cpu), "5ns")) + link_l2cache_2_rtr = sst.Link(prefix + ".link_l2cache_2_rtr") + link_l2cache_2_rtr.connect(l2cache, (comp_chiprtr, "port" + str(cpu), "1ns")) + + hali_ring_left = sst.Link("hali_ring_left") + hali_ring_left.connect((halis[0], "left", "5ns"), (halis[1], "right", "5ns")) + hali_ring_right = sst.Link("hali_ring_right") + hali_ring_right.connect((halis[0], "right", "5ns"), (halis[1], "left", "5ns")) + + +# Only build the default (no-gate) topology when this script is the one +# that SST is executing directly. When imported by the gated variant +# (testFourStateRegistryGated.py), __name__ is "testFourStateRegistry" and +# that variant is responsible for setting GATE_PARAMS_PER_CORE and calling +# build_topology() itself. +if __name__ == "__main__": + build_topology() diff --git a/src/sst/elements/carcosa/tests/testFourStateRegistryGated.py b/src/sst/elements/carcosa/tests/testFourStateRegistryGated.py new file mode 100644 index 0000000000..4754b68c70 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testFourStateRegistryGated.py @@ -0,0 +1,71 @@ +""" +Same Vanadis + memHierarchy + Hali + FourStateAgent stack as +testFourStateRegistry.py, but with a PortModuleStateGate installed on each +Hali's lowlink port. The gate reads the FourStateAgent-published snapshot in +PipelineStateRegistry for the matching state_key and +flips a single bit in MemEvent payloads that transit the lowlink while the +agent's currentKernel is in the configured set. + +Wiring note: CPU -> Hali(highlink <-> lowlink) -> dTLB -> L1D. The gate is +installed on Hali's lowlink (the L1D-facing side) on Receive direction by +default. Receive direction means the gate intercepts responses coming back +from the cache toward the CPU; Send would corrupt requests heading out. + +The gate is configured to: + - fault_mode=flip : corrupt one payload byte rather than drop + (drop would hang the CPU waiting on the load). + - flip_probability=0.05 : ~5% of matching events get flipped. High + enough to be visibly observable in the + checksums K0..K3 print (compare stdout-100 + from gated vs. ungated runs) and low + enough that the run still completes. + - kernels="2" : engage only while currentKernel == 2. + FourStateAgent publishes the CPU command + index currently in flight, so kernels="2" + exactly targets CPU handler K2 (the + read-modify-write kernel in fourstate.c) + and leaves K0/K1/K3 traffic untouched. + Diffing stdout against an ungated run + should show divergent "K2 ... v=..." lines + and identical K0/K1/K3 lines. + - region_names is NOT set here; we want all cache traffic during K2, not + just MMIO. Add "region_names": "mmio_control" to restrict faults to + MMIO writes only, or "region_ids": "" for numeric region IDs. + +Build fourstate.c (same prereq as the ungated test): + riscv64-unknown-linux-gnu-gcc -static -I.. -o fourstate fourstate.c + +Run: + sst testFourStateRegistryGated.py +""" + +# Import the baseline module but suppress its auto-build. We override +# GATE_PARAMS_PER_CORE before invoking build_topology() explicitly. +import importlib +_base = importlib.import_module("testFourStateRegistry") + +# Must match STATE_KEYS from the baseline (one gate entry per core). +_base.GATE_PARAMS_PER_CORE = { + "core0": { + "state_key": "core0", + "fault_mode": "flip", + "flip_probability": "0.05", + "kernels": "2", + "verbose": "1", + "debug": "1", + "debug_level": "1", + "install_direction": "Receive", + }, + "core1": { + "state_key": "core1", + "fault_mode": "flip", + "flip_probability": "0.05", + "kernels": "2", + "verbose": "1", + "debug": "1", + "debug_level": "1", + "install_direction": "Receive", + }, +} + +_base.build_topology() diff --git a/src/sst/elements/carcosa/tests/testhaliBacking.py b/src/sst/elements/carcosa/tests/testHaliBacking.py similarity index 100% rename from src/sst/elements/carcosa/tests/testhaliBacking.py rename to src/sst/elements/carcosa/tests/testHaliBacking.py diff --git a/src/sst/elements/carcosa/tests/testhaliMemH.py b/src/sst/elements/carcosa/tests/testHaliMemH.py similarity index 100% rename from src/sst/elements/carcosa/tests/testhaliMemH.py rename to src/sst/elements/carcosa/tests/testHaliMemH.py diff --git a/src/sst/elements/carcosa/tests/testhaliPM.py b/src/sst/elements/carcosa/tests/testHaliPM.py similarity index 100% rename from src/sst/elements/carcosa/tests/testhaliPM.py rename to src/sst/elements/carcosa/tests/testHaliPM.py diff --git a/src/sst/elements/carcosa/tests/testmanagerLogic.py b/src/sst/elements/carcosa/tests/testManagerLogic.py similarity index 99% rename from src/sst/elements/carcosa/tests/testmanagerLogic.py rename to src/sst/elements/carcosa/tests/testManagerLogic.py index 128baf6fb9..07367de630 100644 --- a/src/sst/elements/carcosa/tests/testmanagerLogic.py +++ b/src/sst/elements/carcosa/tests/testManagerLogic.py @@ -1,7 +1,7 @@ -# testmanagerLogic.py +# testManagerLogic.py # Puts a faultInjectorMemH on each L1 cache and assigns each Hali to manage exactly one # injector via separate PM registries: hali_0 -> l1_0 (c0_l1cache), hali_1 -> l1_1, etc. -# Run with: sst testmanagerLogic.py +# Run with: sst testManagerLogic.py # Look for "[ManagerLogic]" debug lines to verify each manager only sees its own PM. import sst From 7629273bb9a4b5df56467c9d5a654b5bfea28a81 Mon Sep 17 00:00:00 2001 From: nab880 Date: Fri, 10 Jul 2026 16:03:13 -0700 Subject: [PATCH 2/6] carcosa: add PortModuleStateGate and pipeline examples --- .../MmioControl/mmioControlExample.cc | 127 +++++++++ .../examples/MmioControl/mmioControlExample.h | 138 +++++++++ .../MmioControl/tests/testMmioControl.py | 40 +++ .../SimplePipeline/simplePipelineExample.cc | 261 +++++++++++++++++ .../SimplePipeline/simplePipelineExample.h | 215 ++++++++++++++ .../tests/testPortModuleStateGate.py | 85 ++++++ .../tests/testSimplePipeline.py | 73 +++++ .../carcosa/injectors/portModuleStateGate.cc | 266 ++++++++++++++++++ .../carcosa/injectors/portModuleStateGate.h | 132 +++++++++ .../tests/testStateGateRegionNoOverlap.py | 24 ++ .../tests/testStateGateRegionOverlap.py | 31 ++ 11 files changed, 1392 insertions(+) create mode 100644 src/sst/elements/carcosa/examples/MmioControl/mmioControlExample.cc create mode 100644 src/sst/elements/carcosa/examples/MmioControl/mmioControlExample.h create mode 100644 src/sst/elements/carcosa/examples/MmioControl/tests/testMmioControl.py create mode 100644 src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.cc create mode 100644 src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.h create mode 100644 src/sst/elements/carcosa/examples/SimplePipeline/tests/testPortModuleStateGate.py create mode 100644 src/sst/elements/carcosa/examples/SimplePipeline/tests/testSimplePipeline.py create mode 100644 src/sst/elements/carcosa/injectors/portModuleStateGate.cc create mode 100644 src/sst/elements/carcosa/injectors/portModuleStateGate.h create mode 100644 src/sst/elements/carcosa/tests/testStateGateRegionNoOverlap.py create mode 100644 src/sst/elements/carcosa/tests/testStateGateRegionOverlap.py diff --git a/src/sst/elements/carcosa/examples/MmioControl/mmioControlExample.cc b/src/sst/elements/carcosa/examples/MmioControl/mmioControlExample.cc new file mode 100644 index 0000000000..00a86d35db --- /dev/null +++ b/src/sst/elements/carcosa/examples/MmioControl/mmioControlExample.cc @@ -0,0 +1,127 @@ +// 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. +// +// Portions are copyright of other developers: +// See the file CONTRIBUTORS.TXT in the top level directory of the distribution. +// +// 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 "sst_config.h" +#include "sst/elements/carcosa/examples/MmioControl/mmioControlExample.h" +#include +#include + +using namespace SST; +using namespace SST::Interfaces; +using namespace SST::Carcosa; + +ExampleMmioDriver::ExampleMmioDriver(ComponentId_t id, Params& params) + : Component(id) +{ + out_ = new Output("ExampleMmioDriver: ", 1, 0, Output::STDOUT); + requireLibrary("memHierarchy"); + + mmioBase_ = params.find("mmio_base", 0xBEEF0000); + armValue_ = params.find("arm_value", 0xABCD); + + std::string clock = params.find("clock", "1GHz"); + TimeConverter tc = getTimeConverter(clock); + + respHandler_ = new RespHandler(this, out_); + iface_ = loadUserSubComponent( + "mem_iface", ComponentInfo::SHARE_NONE, tc, + new StandardMem::Handler(this)); + if (!iface_) + out_->fatal(CALL_INFO, -1, "ExampleMmioDriver: no 'mem_iface' loaded\n"); + + registerClock(tc, new Clock::Handler(this)); + registerAsPrimaryComponent(); + primaryComponentDoNotEndSim(); +} + +ExampleMmioDriver::~ExampleMmioDriver() +{ + delete respHandler_; + delete out_; +} + +void ExampleMmioDriver::init(unsigned phase) { iface_->init(phase); } +void ExampleMmioDriver::setup() { iface_->setup(); } +void ExampleMmioDriver::finish() {} + +void ExampleMmioDriver::sendRead(uint64_t offset) +{ + iface_->send(new StandardMem::Read(mmioBase_ + offset, 4)); +} + +void ExampleMmioDriver::sendWrite(uint64_t offset, uint32_t value) +{ + std::vector data(4); + std::memcpy(data.data(), &value, 4); + iface_->send(new StandardMem::Write(mmioBase_ + offset, 4, data, false)); +} + +bool ExampleMmioDriver::tick(Cycle_t) +{ + switch (step_) { + case 0: + out_->output("step 0: read value@0x%" PRIx64 + " (peripheral should park it until armed)\n", + mmioBase_ + ExampleControlAgent::kValueOffset); + sendRead(ExampleControlAgent::kValueOffset); + step_ = 1; + return false; + case 1: + out_->output("step 1: arm 0x%x via write@0x%" PRIx64 + " (should complete the parked read)\n", + armValue_, mmioBase_ + ExampleControlAgent::kArmOffset); + sendWrite(ExampleControlAgent::kArmOffset, armValue_); + step_ = 2; + return false; + case 2: + if (gotReadResp_) { + if (readValue_ == armValue_) + out_->output("PASS: deferred read returned 0x%x via completePendingRead\n", + readValue_); + else + out_->output("FAIL: deferred read returned 0x%x (expected 0x%x)\n", + readValue_, armValue_); + primaryComponentOKToEndSim(); + return true; + } + if (++waitCycles_ > 1000) { + out_->output("FAIL: parked read never completed after %d cycles\n", waitCycles_); + primaryComponentOKToEndSim(); + return true; + } + return false; + default: + primaryComponentOKToEndSim(); + return true; + } +} + +void ExampleMmioDriver::handleResponse(StandardMem::Request* req) +{ + req->handle(respHandler_); +} + +void ExampleMmioDriver::RespHandler::handle(StandardMem::ReadResp* resp) +{ + uint32_t v = 0; + if (resp->data.size() >= sizeof(uint32_t)) + std::memcpy(&v, resp->data.data(), sizeof(uint32_t)); + drv_->readValue_ = v; + drv_->gotReadResp_ = true; + delete resp; +} + +void ExampleMmioDriver::RespHandler::handle(StandardMem::WriteResp* resp) +{ + delete resp; +} diff --git a/src/sst/elements/carcosa/examples/MmioControl/mmioControlExample.h b/src/sst/elements/carcosa/examples/MmioControl/mmioControlExample.h new file mode 100644 index 0000000000..36ebc40e50 --- /dev/null +++ b/src/sst/elements/carcosa/examples/MmioControl/mmioControlExample.h @@ -0,0 +1,138 @@ +// 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. +// +// Portions are copyright of other developers: +// See the file CONTRIBUTORS.TXT in the top level directory of the distribution. +// +// 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 CARCOSA_MMIO_CONTROL_EXAMPLE_H +#define CARCOSA_MMIO_CONTROL_EXAMPLE_H + +#include +#include +#include +#include "sst/elements/carcosa/components/interceptionAgentAPI.h" +#include +#include + +namespace SST { +namespace Carcosa { + +/** + * Control agent: Deferred read@value until write@arm completes the parked read. + */ +class ExampleControlAgent : public InterceptionAgentAPI { +public: + SST_ELI_REGISTER_SUBCOMPONENT( + ExampleControlAgent, "carcosa", "ExampleControlAgent", + SST_ELI_ELEMENT_VERSION(1, 0, 0), + "Minimal handleControlAccess example agent (read parks until armed)", + SST::Carcosa::InterceptionAgentAPI) + + static constexpr uint64_t kValueOffset = 0x00; // R: armed value (blocks until armed) + static constexpr uint64_t kArmOffset = 0x04; // W: arm a value, completes a parked read + + ExampleControlAgent(ComponentId_t id, Params& params) + : InterceptionAgentAPI(id, params) {} + ExampleControlAgent() : InterceptionAgentAPI() {} + + ControlResult handleControlAccess(ControlAccess& acc) override { + if (!acc.isWrite) { + if (acc.offset == kValueOffset) { + if (armed_) { acc.readValue = value_; return ControlResult::Handled; } + parked_ = true; + return ControlResult::Deferred; + } + return ControlResult::Ignored; + } + if (acc.offset == kArmOffset) { + value_ = acc.value; + armed_ = true; + if (parked_) { + parked_ = false; + if (channel_) channel_->completePendingRead(value_); + } + return ControlResult::Handled; + } + return ControlResult::Ignored; + } + + void setControlChannel(ControlChannel* ch) override { channel_ = ch; } + + bool handleInterceptedEvent(SST::MemHierarchy::MemEvent* ev, + SST::Link* highlink) override { + (void)ev; (void)highlink; return false; + } + +private: + ControlChannel* channel_ = nullptr; + uint32_t value_ = 0; + bool armed_ = false; + bool parked_ = false; +}; + +/** + * Scripted MMIO requestor: park read -> arm write -> verify (watchdog on stall). + */ +class ExampleMmioDriver : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT( + ExampleMmioDriver, "carcosa", "ExampleMmioDriver", + SST_ELI_ELEMENT_VERSION(1, 0, 0), + "Scripted StandardMem requestor that tests an MMIO control peripheral", + COMPONENT_CATEGORY_UNCATEGORIZED) + + SST_ELI_DOCUMENT_PARAMS( + {"clock", "Clock frequency.", "1GHz"}, + {"mmio_base", "Base address of the MMIO peripheral.", "0xBEEF0000"}, + {"arm_value", "Value to arm and expect on round-trip.", "43981"}) + + SST_ELI_DOCUMENT_SUBCOMPONENT_SLOTS( + {"mem_iface", "StandardMem interface to the MMIO peripheral", + "SST::Interfaces::StandardMem"}) + + ExampleMmioDriver(ComponentId_t id, Params& params); + ~ExampleMmioDriver(); + + void init(unsigned phase) override; + void setup() override; + void finish() override; + +private: + bool tick(SST::Cycle_t cycle); + void handleResponse(SST::Interfaces::StandardMem::Request* req); + void sendRead(uint64_t offset); + void sendWrite(uint64_t offset, uint32_t value); + + class RespHandler : public SST::Interfaces::StandardMem::RequestHandler { + public: + RespHandler(ExampleMmioDriver* d, SST::Output* o) + : SST::Interfaces::StandardMem::RequestHandler(o), drv_(d) {} + void handle(SST::Interfaces::StandardMem::ReadResp* resp) override; + void handle(SST::Interfaces::StandardMem::WriteResp* resp) override; + private: + ExampleMmioDriver* drv_; + }; + + SST::Output* out_ = nullptr; + SST::Interfaces::StandardMem* iface_ = nullptr; + RespHandler* respHandler_ = nullptr; + + uint64_t mmioBase_ = 0xBEEF0000; + uint32_t armValue_ = 0xABCD; + int step_ = 0; + int waitCycles_ = 0; + bool gotReadResp_ = false; + uint32_t readValue_ = 0; +}; + +} // namespace Carcosa +} // namespace SST + +#endif // CARCOSA_MMIO_CONTROL_EXAMPLE_H diff --git a/src/sst/elements/carcosa/examples/MmioControl/tests/testMmioControl.py b/src/sst/elements/carcosa/examples/MmioControl/tests/testMmioControl.py new file mode 100644 index 0000000000..818754dbbd --- /dev/null +++ b/src/sst/elements/carcosa/examples/MmioControl/tests/testMmioControl.py @@ -0,0 +1,40 @@ +"""Self-contained MMIO control-transport smoke test. + +A scripted StandardMem requestor (carcosa.ExampleMmioDriver) drives carcosa.Hali +in MMIO-peripheral mode, which dispatches the control accesses to +ExampleControlAgent via the transport-neutral handleControlAccess hook. The +agent parks the value read (Deferred) until the driver arms a value, then +completes it via completePendingRead. Exercises Hali's MMIO transport plus the +Deferred path end to end -- no VLA, no external host, no QEMU. + +Expect output lines: 'step 0', 'step 1', then + 'PASS: deferred read returned 0xabcd via completePendingRead' +and 'Simulation is complete'. +""" +import sst + +mmio_base = 0xBEEF0000 +arm_value = 0xABCD + +sst.setProgramOption("stop-at", "1us") # backstop; driver ends the sim itself + +driver = sst.Component("driver", "carcosa.ExampleMmioDriver") +driver.addParams({ + "clock": "1GHz", + "mmio_base": str(mmio_base), + "arm_value": str(arm_value), +}) +driver_iface = driver.setSubComponent("mem_iface", "memHierarchy.standardInterface") + +hali = sst.Component("hali", "carcosa.Hali") +hali.addParams({ + "clock": "1GHz", + "mmio_base": str(mmio_base), + "verbose": "false", +}) +hali.setSubComponent("interceptionAgent", "carcosa.ExampleControlAgent") +hali_iface = hali.setSubComponent("mmio_iface", "memHierarchy.standardInterface") + +link = sst.Link("driver_to_hali") +link.connect((driver_iface, "lowlink", "1ns"), (hali_iface, "lowlink", "1ns")) +link.setNoCut() diff --git a/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.cc b/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.cc new file mode 100644 index 0000000000..c0bf7568fc --- /dev/null +++ b/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.cc @@ -0,0 +1,261 @@ +// 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 "sst_config.h" + +#include "sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.h" + +#include + +using namespace SST; +using namespace SST::Carcosa; +using namespace SST::Carcosa::Examples; + +// ============================================================================ +// Stage name table +// ============================================================================ + +const char* SST::Carcosa::Examples::simpleStageName(int id) +{ + switch (id) { + case STAGE_FETCH: return "FETCH"; + case STAGE_DECODE: return "DECODE"; + case STAGE_EXECUTE: return "EXECUTE"; + case STAGE_COMMIT: return "COMMIT"; + default: return "UNKNOWN"; + } +} + +// ============================================================================ +// SimplePipelineProducer +// ============================================================================ + +SimplePipelineProducer::SimplePipelineProducer(ComponentId_t id, Params& params) + : SST::Component(id) +{ + out_ = new Output("", 1, 0, Output::STDOUT); + + stateKey_ = params.find("state_key", ""); + totalCycles_ = params.find("total_cycles", 4); + verbose_ = params.find("verbose", false); + + if (stateKey_.empty()) stateKey_ = getName(); + + outLink_ = configureLink("out"); + sst_assert(outLink_, CALL_INFO, -1, + "Error in %s: 'out' link configuration failed\n", + getName().c_str()); + + // Publish the initial snapshot. The gate may consult this before our first + // tick fires; seed currentKernel=-1 so it doesn't accidentally match any + // real stage id in the drop set. + state_ = PipelineStateRegistry::getOrCreate(stateKey_); + state_->currentKernel = -1; + state_->pipelineCycle = 0; + + // Demonstrate the region-publish API with a single dummy region; not used + // by the gate in this example but exercised so the pattern is visible. + state_->stagedBase = 0x10000; + state_->stagedSize = 0x1000; + state_->commitStagedRegion(0); + + const std::string clock_freq = params.find("clock", "1MHz"); + registerClock(clock_freq, + new Clock::Handler(this)); + + registerAsPrimaryComponent(); + primaryComponentDoNotEndSim(); + + if (verbose_) { + out_->output("%s: producer ready, state_key='%s', total_cycles=%d, clock=%s\n", + getName().c_str(), stateKey_.c_str(), totalCycles_, clock_freq.c_str()); + } +} + +SimplePipelineProducer::~SimplePipelineProducer() +{ + delete out_; +} + +bool SimplePipelineProducer::tick(Cycle_t /*cycle*/) +{ + const int stage = tickCount_ % NUM_STAGES; + const int cycle = tickCount_ / NUM_STAGES; + + // Publish BEFORE sending, so any PortModule on the receiving side observes + // the snapshot that corresponds to the event it is about to see. + state_->currentKernel = stage; + state_->pipelineCycle = cycle; + + outLink_->send(new SimpleStageEvent(stage)); + + if (verbose_) { + out_->output("%s: tick=%d stage=%s cycle=%d -> sent event\n", + getName().c_str(), tickCount_, simpleStageName(stage), cycle); + } + + ++tickCount_; + + // After COMMIT of the last configured pipeline cycle, stop the clock and + // release the simulation end-gate. + if (totalCycles_ > 0 && cycle + 1 >= totalCycles_ && stage == STAGE_COMMIT) { + if (verbose_) { + out_->output("%s: reached total_cycles=%d, ending simulation\n", + getName().c_str(), totalCycles_); + } + primaryComponentOKToEndSim(); + return true; + } + return false; +} + +// ============================================================================ +// SimplePipelineSink +// ============================================================================ + +SimplePipelineSink::SimplePipelineSink(ComponentId_t id, Params& params) + : SST::Component(id) +{ + out_ = new Output("", 1, 0, Output::STDOUT); + verbose_ = params.find("verbose", false); + + inLink_ = configureLink("in", + new Event::Handler(this)); + sst_assert(inLink_, CALL_INFO, -1, + "Error in %s: 'in' link configuration failed\n", + getName().c_str()); +} + +SimplePipelineSink::~SimplePipelineSink() +{ + delete out_; +} + +void SimplePipelineSink::handleEvent(Event* ev) +{ + auto* sev = dynamic_cast(ev); + if (!sev) { + out_->fatal(CALL_INFO, -1, + "Error in %s: received unexpected event type\n", + getName().c_str()); + } + if (sev->stageId >= 0 && sev->stageId < NUM_STAGES) { + ++received_[sev->stageId]; + } + if (verbose_) { + out_->output("%s: received stage=%s\n", + getName().c_str(), simpleStageName(sev->stageId)); + } + delete sev; +} + +void SimplePipelineSink::finish() +{ + out_->output("[%s] received per-stage counts:", getName().c_str()); + for (int i = 0; i < NUM_STAGES; ++i) { + out_->output(" %s=%" PRIu64, simpleStageName(i), received_[i]); + } + out_->output("\n"); +} + +// ============================================================================ +// SimpleStageGate (PortModule) +// ============================================================================ + +std::set SimpleStageGate::parseIntCsv(const std::string& s) +{ + std::set out; + std::stringstream ss(s); + std::string tok; + while (std::getline(ss, tok, ',')) { + size_t b = 0; + while (b < tok.size() && std::isspace(static_cast(tok[b]))) ++b; + size_t e = tok.size(); + while (e > b && std::isspace(static_cast(tok[e - 1]))) --e; + if (e == b) continue; + try { out.insert(std::stoi(tok.substr(b, e - b))); } + catch (...) { /* ignore malformed token */ } + } + return out; +} + +SimpleStageGate::SimpleStageGate(Params& params) + : SST::PortModule() +{ + verbose_ = params.find("verbose", false); + out_ = new Output("", verbose_ ? 1 : 0, 0, Output::STDOUT); + + stateKey_ = params.find("state_key", ""); + dropStages_ = parseIntCsv(params.find("drop_stages", "")); + + if (stateKey_.empty()) { + out_->fatal(CALL_INFO, -1, + "SimpleStageGate: 'state_key' is required (must match the producer's state_key or getName()).\n"); + } + + if (verbose_) { + std::string list; + for (int s : dropStages_) { + if (!list.empty()) list += ","; + list += simpleStageName(s); + } + out_->output("SimpleStageGate: state_key='%s' drop_stages=[%s]\n", + stateKey_.c_str(), list.c_str()); + } +} + +SimpleStageGate::~SimpleStageGate() +{ + if (out_) { + out_->output("[SimpleStageGate %s] summary: evaluated=%" PRIu64 " dropped=%" PRIu64 "\n", + stateKey_.c_str(), evaluated_, dropped_); + delete out_; + } +} + +const PipelineStateBase* SimpleStageGate::resolveState() const +{ + if (cached_) return cached_; + cached_ = PipelineStateRegistry::get(stateKey_); + return cached_; +} + +void SimpleStageGate::eventSent(uintptr_t /*key*/, Event*& /*ev*/) +{ + // Send-side intercept is a no-op in this example; the gate installs only + // on receives. Required override because PortModule declares it = 0. +} + +void SimpleStageGate::interceptHandler(uintptr_t /*key*/, Event*& ev, bool& cancel) +{ + cancel = false; + ++evaluated_; + + const PipelineStateBase* st = resolveState(); + if (!st) return; + if (!dropStages_.count(st->currentKernel)) return; + + cancel = true; + ++dropped_; + + if (verbose_) { + out_->output("[SimpleStageGate %s] DROP stage=%s cycle=%d\n", + stateKey_.c_str(), + simpleStageName(st->currentKernel), + st->pipelineCycle); + } + + // When cancel=true, the PortModule contract requires deleting the event + // and setting it to nullptr (see portModule.h::interceptHandler docs). + delete ev; + ev = nullptr; +} diff --git a/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.h b/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.h new file mode 100644 index 0000000000..2a5711ab56 --- /dev/null +++ b/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.h @@ -0,0 +1,215 @@ +// 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. +// +// Portions are copyright of other developers: +// See the file CONTRIBUTORS.TXT in the top level directory of the distribution. +// +// This file is part of the SST software package. For license information, +// see the LICENSE file in the top level directory of the distribution. +// +// ============================================================================ +// SimplePipelineExample +// ---------------------------------------------------------------------------- +// A minimal four-stage pipeline (FETCH -> DECODE -> EXECUTE -> COMMIT) that +// demonstrates how a user-level simulator component can publish its FSM state +// through PipelineStateRegistry so that PortModule fault +// injectors attached to its downstream links can make state-aware decisions. +// +// Wiring: +// +// +-----------------------+ +-----------------------+ +// | SimplePipelineProducer| -- out ->| SimplePipelineSink | +// | (4-stage FSM) | | (counts per stage) | +// +-----------------------+ +-----------------------+ +// | ^ +// | publishes PipelineStateBase | PortModule on "in" +// v | reads snapshot and +// PipelineStateRegistry | cancels events whose +// ^ | currentKernel is in +// | subscribed by key | drop_stages +// +-----------------------> SimpleStageGate (PortModule) +// +// The registry lookup rendezvous key is a plain string; the producer publishes +// under `state_key` (defaults to getName()) and the gate subscribes with the +// same key. +// ============================================================================ + +#ifndef SST_ELEMENTS_CARCOSA_EXAMPLES_SIMPLE_PIPELINE_H +#define SST_ELEMENTS_CARCOSA_EXAMPLES_SIMPLE_PIPELINE_H + +#include +#include +#include +#include +#include +#include + +#include "sst/elements/carcosa/components/pipelineStateRegistry.h" + +#include +#include +#include + +namespace SST { +namespace Carcosa { +namespace Examples { + +/** The four pipeline stages the producer cycles through. */ +enum SimpleStage { + STAGE_FETCH = 0, + STAGE_DECODE = 1, + STAGE_EXECUTE = 2, + STAGE_COMMIT = 3, + NUM_STAGES = 4 +}; + +const char* simpleStageName(int id); + +/** Event carrying the producer's stage id at send time. */ +class SimpleStageEvent : public SST::Event { +public: + SimpleStageEvent() : SST::Event() {} + explicit SimpleStageEvent(int stage) : SST::Event(), stageId(stage) {} + + int stageId = -1; + + void serialize_order(SST::Core::Serialization::serializer& ser) override { + Event::serialize_order(ser); + SST_SER(stageId); + } + ImplementSerializable(SST::Carcosa::Examples::SimpleStageEvent) +}; + +/** + * Clocked 4-stage FSM publisher; emits SimpleStageEvent on `out` each tick. + */ +class SimplePipelineProducer : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT( + SimplePipelineProducer, + "carcosa", + "SimplePipelineProducer", + SST_ELI_ELEMENT_VERSION(1, 0, 0), + "Four-stage FSM driver that publishes stage state via PipelineStateRegistry", + COMPONENT_CATEGORY_PROCESSOR + ) + + SST_ELI_DOCUMENT_PARAMS( + {"state_key", "PipelineStateRegistry key to publish under (empty = getName()).", ""}, + {"total_cycles","Number of full FETCH..COMMIT iterations before signalling sim-end. 0 = run forever.", "4"}, + {"clock", "Clock frequency; one stage advance per tick.", "1MHz"}, + {"verbose", "Log each transition to stdout.", "false"} + ) + + SST_ELI_DOCUMENT_PORTS( + {"out", "Outgoing stage events", { "carcosa.Examples.SimpleStageEvent" } } + ) + + SimplePipelineProducer(SST::ComponentId_t id, SST::Params& params); + ~SimplePipelineProducer() override; + +private: + bool tick(SST::Cycle_t cycle); + + SST::Output* out_ = nullptr; + SST::Link* outLink_ = nullptr; + PipelineStateBase* state_ = nullptr; + + std::string stateKey_; + int totalCycles_ = 0; + int tickCount_ = 0; + bool verbose_ = false; +}; + +/** Counts events per stage id and prints the histogram in finish(). */ +class SimplePipelineSink : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT( + SimplePipelineSink, + "carcosa", + "SimplePipelineSink", + SST_ELI_ELEMENT_VERSION(1, 0, 0), + "Counts stage events per stage id (sink for SimplePipelineProducer)", + COMPONENT_CATEGORY_PROCESSOR + ) + + SST_ELI_DOCUMENT_PARAMS( + {"verbose", "Log each received event to stdout.", "false"} + ) + + SST_ELI_DOCUMENT_PORTS( + {"in", "Incoming stage events", { "carcosa.Examples.SimpleStageEvent" } } + ) + + SimplePipelineSink(SST::ComponentId_t id, SST::Params& params); + ~SimplePipelineSink() override; + + void finish() override; + +private: + void handleEvent(SST::Event* ev); + + SST::Output* out_ = nullptr; + SST::Link* inLink_ = nullptr; + bool verbose_ = false; + uint64_t received_[NUM_STAGES] = {}; +}; + +/** + * Drops events when producer's currentKernel is in drop_stages (lazy snapshot). + */ +class SimpleStageGate : public SST::PortModule { +public: + SST_ELI_REGISTER_PORTMODULE( + SimpleStageGate, + "carcosa", + "SimpleStageGate", + SST_ELI_ELEMENT_VERSION(0, 1, 0), + "Drops events when the subscribed producer's stage id matches a configured set" + ) + + SST_ELI_DOCUMENT_PARAMS( + {"state_key", "PipelineStateRegistry key published by the producer.", ""}, + {"drop_stages", "Comma-separated stage ids (0..3) on which to drop. Empty = never drop.", ""}, + {"verbose", "Log each drop to stdout.", "false"} + ) + + SimpleStageGate(SST::Params& params); + SimpleStageGate() = default; + ~SimpleStageGate() override; + + bool installOnReceive() override { return true; } + bool installOnSend() override { return false; } + + void eventSent(uintptr_t key, SST::Event*& ev) override; + void interceptHandler(uintptr_t key, SST::Event*& ev, bool& cancel) override; + + void serialize_order(SST::Core::Serialization::serializer& ser) override { + SST::PortModule::serialize_order(ser); + } + ImplementVirtualSerializable(SST::Carcosa::Examples::SimpleStageGate) + +private: + const PipelineStateBase* resolveState() const; + static std::set parseIntCsv(const std::string& s); + + SST::Output* out_ = nullptr; + std::string stateKey_; + std::set dropStages_; + bool verbose_ = false; + + uint64_t evaluated_ = 0; + uint64_t dropped_ = 0; + + mutable const PipelineStateBase* cached_ = nullptr; +}; + +} // namespace Examples +} // namespace Carcosa +} // namespace SST + +#endif /* SST_ELEMENTS_CARCOSA_EXAMPLES_SIMPLE_PIPELINE_H */ diff --git a/src/sst/elements/carcosa/examples/SimplePipeline/tests/testPortModuleStateGate.py b/src/sst/elements/carcosa/examples/SimplePipeline/tests/testPortModuleStateGate.py new file mode 100644 index 0000000000..6bc326a2e5 --- /dev/null +++ b/src/sst/elements/carcosa/examples/SimplePipeline/tests/testPortModuleStateGate.py @@ -0,0 +1,85 @@ +""" +Exercise the generic PortModuleStateGate against SimplePipelineProducer. + +SimplePipelineProducer writes its FSM state into PipelineStateRegistry under +`state_key`, stashing the current stage id in PipelineStateBase.currentKernel +and advancing PipelineStateBase.pipelineCycle once per full pass: + + stage 0 -> FETCH + stage 1 -> DECODE + stage 2 -> EXECUTE + stage 3 -> COMMIT + +PortModuleStateGate on the sink's "in" port is configured with +`kernels="1,3"` + `fault_mode="drop"` + `drop_probability=1.0`, so any event +it intercepts while the producer's registered currentKernel is DECODE or +COMMIT is dropped. This is the same behavior SimpleStageGate demonstrates, +but routed through the generic predicate machinery. + +NOTE on timing: because the producer->sink link has a 1us delay, the gate +sees the *next* registry snapshot at the moment each event arrives (producer +has already advanced one stage). The histogram you'll see below therefore +reflects "gated on stage-at-arrival", not "gated on stage-at-send": + + tick t: producer sends stage X; registry says currentKernel=X + tick t+1: event arrives at gate; registry says currentKernel=(X+1) mod 4 + +With drop_stages={DECODE=1, COMMIT=3}, the gate drops events whose arrival- +time stage is in {1,3} -- i.e. the events that were *emitted* during FETCH +and EXECUTE. Events emitted during DECODE/COMMIT arrive when the producer +is at EXECUTE/FETCH, which are not in the drop set, and go through. +Expected histogram: FETCH=0 DECODE=N EXECUTE=0 COMMIT=N-1 (last COMMIT never +arrives because the producer exits on reaching total_cycles). + +Run: + sst testPortModuleStateGate.py +""" + +import sst + +# See testSimplePipeline.py for rationale: side-effect import so SST's +# factory dlopens libmemHierarchy.so before libCarcosa.so. The import +# itself raises ModuleNotFoundError (memHierarchy has no Python module), +# but by then the shared object is already resident in the process. +try: + import sst.memHierarchy # noqa: F401 +except ModuleNotFoundError: + pass + +sst.setProgramOption("timebase", "1ps") +sst.setProgramOption("stop-at", "0 ns") + +STATE_KEY = "gate_test" +TOTAL_CYCLES = 4 + +producer = sst.Component("producer", "carcosa.SimplePipelineProducer") +producer.addParams({ + "state_key": STATE_KEY, + "total_cycles": TOTAL_CYCLES, + "clock": "1MHz", + "verbose": "true", +}) + +sink = sst.Component("sink", "carcosa.SimplePipelineSink") +sink.addParams({ + "verbose": "true", +}) + +# Generic state-gated drop injector. +# - state_key: same key the producer publishes into. +# - kernels="1,3": match when registered currentKernel is DECODE or COMMIT. +# - fault_mode=drop: cancel delivery on match. +# - drop_probability=1: every matched event is dropped (deterministic). +sink.addPortModule("in", "carcosa.PortModuleStateGate", { + "state_key": STATE_KEY, + "fault_mode": "drop", + "drop_probability": "1.0", + "kernels": "1,3", + "verbose": "1", + "debug": "1", + "debug_level": "2", +}) + +link = sst.Link("producer_to_sink") +link.connect((producer, "out", "1us"), + (sink, "in", "1us")) diff --git a/src/sst/elements/carcosa/examples/SimplePipeline/tests/testSimplePipeline.py b/src/sst/elements/carcosa/examples/SimplePipeline/tests/testSimplePipeline.py new file mode 100644 index 0000000000..e902466f33 --- /dev/null +++ b/src/sst/elements/carcosa/examples/SimplePipeline/tests/testSimplePipeline.py @@ -0,0 +1,73 @@ +""" +Minimal SST config for SimplePipeline example. + +Demonstrates PipelineStateRegistry rendezvous: + - SimplePipelineProducer advances a 4-stage FSM (FETCH, DECODE, EXECUTE, + COMMIT) and publishes the current stage + pipeline cycle into the registry + under `state_key`. + - SimpleStageGate (a PortModule installed on the sink's "in" port) reads the + same key and cancels delivery of events whose stage id is in drop_stages. + - SimplePipelineSink counts per-stage deliveries and prints the histogram in + finish(), so you can observe that the gated stages show zero arrivals. + +Run: + sst testSimplePipeline.py + +Expected behavior with drop_stages="1,3" (DECODE and COMMIT): + - FETCH and EXECUTE counts equal total_cycles + - DECODE and COMMIT counts are zero +""" + +import sst + +# libCarcosa.so has link-time references to SST::MemHierarchy symbols +# (CarcosaMemCtrl pulls in SimpleMemBackendConvertor, etc.). On macOS's +# two-level/flat dlopen semantics those symbols must already be in the +# process image when libCarcosa.so is loaded, or dlopen fails with +# "symbol not found in flat namespace". Importing sst.memHierarchy runs +# SST's Python import hook (pymodel.cc::mlFindModule), which calls +# Factory::hasLibrary -> findLibrary and dlopens libmemHierarchy.so now, +# before Carcosa is touched. Every other cross-element test in the tree +# gets this for free because they instantiate memHierarchy.* components +# first; this example doesn't use memHierarchy, so we force it explicitly. +# Note: we don't actually use any Python-side memHierarchy symbols; we just +# need SST's import hook to dlopen libmemHierarchy.so. The import itself will +# raise ModuleNotFoundError because memHierarchy exposes no Python module, +# but Factory::findLibrary has already been called by that point, so the +# shared object is loaded into the process and its symbols are available +# when libCarcosa.so is loaded next. +try: + import sst.memHierarchy # noqa: F401 +except ModuleNotFoundError: + pass + +sst.setProgramOption("timebase", "1ps") +sst.setProgramOption("stop-at", "0 ns") + +STATE_KEY = "pipe0" +TOTAL_CYCLES = 4 + +producer = sst.Component("producer", "carcosa.SimplePipelineProducer") +producer.addParams({ + "state_key": STATE_KEY, + "total_cycles": TOTAL_CYCLES, + "clock": "1MHz", + "verbose": "true", +}) + +sink = sst.Component("sink", "carcosa.SimplePipelineSink") +sink.addParams({ + "verbose": "true", +}) + +# PortModule on the sink's receive side: reads the same registry key that +# `producer` publishes under, and cancels DECODE (1) and COMMIT (3) events. +sink.addPortModule("in", "carcosa.SimpleStageGate", { + "state_key": STATE_KEY, + "drop_stages": "1,3", + "verbose": "true", +}) + +link = sst.Link("producer_to_sink") +link.connect((producer, "out", "1us"), + (sink, "in", "1us")) diff --git a/src/sst/elements/carcosa/injectors/portModuleStateGate.cc b/src/sst/elements/carcosa/injectors/portModuleStateGate.cc new file mode 100644 index 0000000000..ee14953b16 --- /dev/null +++ b/src/sst/elements/carcosa/injectors/portModuleStateGate.cc @@ -0,0 +1,266 @@ +// 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 "sst/elements/carcosa/injectors/portModuleStateGate.h" +#include "sst/elements/carcosa/faultlogic/randomFlipFault.h" +#include "sst/core/params.h" + +#include +#include +#include +#include +#include +#include + +using namespace SST::Carcosa; + +namespace { + +std::string trim(const std::string& s) { + size_t b = 0; + while (b < s.size() && std::isspace(static_cast(s[b]))) ++b; + size_t e = s.size(); + while (e > b && std::isspace(static_cast(s[e - 1]))) --e; + return s.substr(b, e - b); +} + +std::vector splitCsv(const std::string& csv) { + std::vector out; + std::stringstream ss(csv); + std::string tok; + while (std::getline(ss, tok, ',')) { + tok = trim(tok); + if (!tok.empty()) out.push_back(tok); + } + return out; +} + +std::set parseIntSet(const std::string& csv) { + std::set out; + for (const auto& tok : splitCsv(csv)) { + try { out.insert(std::stoi(tok)); } catch (...) { /* skip bad tokens */ } + } + return out; +} + +std::set parseStringSet(const std::string& csv) { + std::set out; + for (auto& tok : splitCsv(csv)) out.insert(std::move(tok)); + return out; +} + +} // namespace + +PortModuleStateGate::Mode +PortModuleStateGate::parseMode(const std::string& s) { + std::string v; + v.reserve(s.size()); + for (char c : s) v.push_back(static_cast(std::tolower(static_cast(c)))); + if (v == "drop") return Mode::Drop; + if (v == "flip") return Mode::Flip; + if (v == "drop_flip" || v == "dropflip" || v == "both") return Mode::DropFlip; + return Mode::Drop; +} + +PortModuleStateGate::PortModuleStateGate(Params& params) + : FaultInjectorBase(params) +{ + stateKey_ = params.find("state_key", ""); + if (stateKey_.empty()) { + out_->fatal(CALL_INFO_LONG, -1, + "PortModuleStateGate: 'state_key' is required.\n"); + } + + mode_ = parseMode(params.find("fault_mode", "drop")); + dropProb_ = params.find("drop_probability", 1.0); + flipProb_ = params.find("flip_probability", 1.0); + + // Fixed layout [drop=0, flip=1]. Drop is inline (cancelDelivery on any Event); + // fault[0] stays null. fault[1] is RandomFlipFault only when flip is enabled. + fault.resize(2); + fault[0] = nullptr; + fault[1] = (mode_ == Mode::Flip || mode_ == Mode::DropFlip) + ? new RandomFlipFault(params, this) + : nullptr; + + buildPredicates(params); + setValidInstallation(params, SEND_RECEIVE_VALID); + +#ifdef __SST_DEBUG_OUTPUT__ + dbg_->debug(CALL_INFO_LONG, 1, 0, + "PortModuleStateGate: state_key='%s' mode=%d drop_p=%f flip_p=%f " + "predicates=%zu\n", + stateKey_.c_str(), static_cast(mode_), + dropProb_, flipProb_, predicates_.size()); +#endif +} + +void +PortModuleStateGate::buildPredicates(Params& params) +{ + // Parse into serializable members first; rebuildPredicates() turns them + // into lambdas and runs again after checkpoint restore. + kernelsCsv_ = params.find("kernels", ""); + hasCycleRange_ = params.contains("pipeline_cycle_start") + || params.contains("pipeline_cycle_end"); + cycleStart_ = params.find("pipeline_cycle_start", 0); + cycleEnd_ = params.find("pipeline_cycle_end", INT32_MAX); + regionIdsCsv_ = params.find("region_ids", ""); + regionNamesCsv_ = params.find("region_names", ""); + + rebuildPredicates(); +} + +void +PortModuleStateGate::rebuildPredicates() +{ + predicates_.clear(); + + // kernel-id set predicate: matches when currentKernel is in the set. + if (!kernelsCsv_.empty()) { + auto allowed = parseIntSet(kernelsCsv_); + predicates_.emplace_back( + [allowed = std::move(allowed)](const PipelineStateBase& s, + const EventAddress&) { + return allowed.count(s.currentKernel) > 0; + }); + } + + // pipeline cycle range predicate: [start, end] inclusive; either bound optional. + // Use a large sentinel for "unset" to keep the comparison branch-free. + if (hasCycleRange_) { + const int start = cycleStart_; + const int end = cycleEnd_; + predicates_.emplace_back( + [start, end](const PipelineStateBase& s, const EventAddress&) { + return s.pipelineCycle >= start && s.pipelineCycle <= end; + }); + } + + // Region predicates are address filters: the event's range must overlap + // an allowed published region. An event with no address (ea.valid == + // false) touches no region and never matches. + auto overlaps = [](const MemoryRegion& r, const EventAddress& ea) { + if (!ea.valid) return false; + uint64_t sz = ea.size ? ea.size : 1; + return ea.addr < r.base + r.size && ea.addr + sz > r.base; + }; + + if (!regionIdsCsv_.empty()) { + auto allowed = parseIntSet(regionIdsCsv_); + predicates_.emplace_back( + [allowed = std::move(allowed), overlaps](const PipelineStateBase& s, + const EventAddress& ea) { + for (const auto& r : s.regions) { + if (r.valid && allowed.count(r.id) > 0 && overlaps(r, ea)) + return true; + } + return false; + }); + } + + if (!regionNamesCsv_.empty()) { + auto allowed = parseStringSet(regionNamesCsv_); + predicates_.emplace_back( + [allowed = std::move(allowed), overlaps](const PipelineStateBase& s, + const EventAddress& ea) { + for (const auto& r : s.regions) { + if (r.valid && allowed.count(r.name) > 0 && overlaps(r, ea)) + return true; + } + return false; + }); + } +} + +bool +PortModuleStateGate::matchesState(const PipelineStateBase& state, + const EventAddress& ea) const +{ + for (const auto& p : predicates_) { + if (!p(state, ea)) return false; + } + return true; +} + +bool +PortModuleStateGate::doInjection(Event* ev) +{ + triggered_ = {{false, false}}; + + const PipelineStateBase* state = + PipelineStateRegistry::get(stateKey_); + if (!state) { + // Agent hasn't published yet; no gate can match. + return false; + } + + EventAddress ea; + if (auto* mev = dynamic_cast(ev)) { + // Same address convention as CriticalActionWatcher/EccGuard: prefer + // the virtual address when the CPU supplied one (published regions + // are workload-virtual), else the physical address. + uint64_t vaddr = mev->getVirtualAddress(); + ea.valid = true; + ea.addr = (vaddr != 0) ? vaddr : mev->getAddr(); + ea.size = mev->getSize() ? mev->getSize() : mev->getPayload().size(); + } + + if (!matchesState(*state, ea)) { + return false; + } + + switch (mode_) { + case Mode::Drop: + triggered_[0] = (this->randFloat(0.0, 1.0) <= dropProb_); + return triggered_[0]; + case Mode::Flip: + triggered_[1] = (this->randFloat(0.0, 1.0) <= flipProb_); + return triggered_[1]; + case Mode::DropFlip: + triggered_[0] = (this->randFloat(0.0, 1.0) <= dropProb_); + // Only roll for flip if we didn't already decide to drop the event; + // a dropped event has nothing left to flip. + triggered_[1] = !triggered_[0] && + (this->randFloat(0.0, 1.0) <= flipProb_); + return triggered_[0] || triggered_[1]; + } + return false; +} + +void +PortModuleStateGate::executeFaults(Event*& ev) +{ + if (triggered_[0]) { + // Generic drop via cancelDelivery(); interceptor must delete the event + // (same contract as RandomDropFault::faultLogic). + if (getInstallDirection() == installDirection::Receive) { + delete ev; + ev = nullptr; + this->cancelDelivery(); + } else { +#ifdef __SST_DEBUG_OUTPUT__ + dbg_->debug(CALL_INFO_LONG, 1, 0, + "PortModuleStateGate: drop requested in Send direction is a no-op " + "(the framework doesn't expose a cancel hook on Send).\n"); +#endif + } + return; + } + if (triggered_[1]) { + if (!fault[1]) { + out_->fatal(CALL_INFO_LONG, -1, + "PortModuleStateGate: flip triggered but fault[1] is null " + "(fault_mode must be 'flip' or 'drop_flip' to enable flip).\n"); + } + fault[1]->faultLogic(ev); + } +} diff --git a/src/sst/elements/carcosa/injectors/portModuleStateGate.h b/src/sst/elements/carcosa/injectors/portModuleStateGate.h new file mode 100644 index 0000000000..54473c29a2 --- /dev/null +++ b/src/sst/elements/carcosa/injectors/portModuleStateGate.h @@ -0,0 +1,132 @@ +// 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 SST_ELEMENTS_CARCOSA_PORTMODULESTATEGATE_H +#define SST_ELEMENTS_CARCOSA_PORTMODULESTATEGATE_H + +#include "sst/elements/carcosa/injectors/faultInjectorBase.h" +#include "sst/elements/carcosa/components/pipelineStateRegistry.h" + +#include +#include +#include +#include +#include + +namespace SST::Carcosa { + +/** + * State-gated drop/flip PortModule; predicates AND (empty list matches all). + */ +class PortModuleStateGate : public FaultInjectorBase { +public: + SST_ELI_REGISTER_PORTMODULE( + PortModuleStateGate, + "carcosa", + "PortModuleStateGate", + SST_ELI_ELEMENT_VERSION(0, 1, 0), + "State-gated drop/flip fault injector. Consults PipelineStateRegistry " + "before firing the wrapped fault." + ) + + SST_ELI_DOCUMENT_PARAMS( + {"state_key", "Required. Key into PipelineStateRegistry."}, + {"fault_mode", "One of 'drop', 'flip', or 'drop_flip'. Default 'drop'."}, + {"drop_probability", "Probability a matching event is dropped (used by 'drop'/'drop_flip'). Default 1.0."}, + {"flip_probability", "Probability a matching event has a bit flipped (used by 'flip'/'drop_flip'). Default 1.0."}, + {"kernels", "Optional CSV of kernel ids that enable the gate when currentKernel is in the set."}, + {"pipeline_cycle_start", "Optional inclusive lower bound for pipelineCycle."}, + {"pipeline_cycle_end", "Optional inclusive upper bound for pipelineCycle."}, + {"region_ids", "Optional CSV of MemoryRegion ids; gate enables if the event's address range overlaps a valid region whose id is in the set."}, + {"region_names", "Optional CSV of MemoryRegion::name strings; gate enables if the event's address range overlaps a valid region with a matching name."} + ) + + PortModuleStateGate(Params& params); + PortModuleStateGate() = default; + ~PortModuleStateGate() override = default; + +protected: + /** Event address range; valid=false for non-MemEvents. */ + struct EventAddress { + bool valid = false; + uint64_t addr = 0; + uint64_t size = 0; + }; + + /** Predicate over PipelineStateBase + event address. */ + using Predicate = std::function; + + enum class Mode { Drop, Flip, DropFlip }; + + // Configuration + std::string stateKey_; + Mode mode_ = Mode::Drop; + double dropProb_ = 1.0; + double flipProb_ = 1.0; + + // Predicate configuration, kept in serializable form so checkpoint + // restore can rebuild predicates_ (std::function is not serializable). + std::string kernelsCsv_; + bool hasCycleRange_ = false; + int cycleStart_ = 0; + int cycleEnd_ = INT32_MAX; + std::string regionIdsCsv_; + std::string regionNamesCsv_; + + // Composed predicates (AND semantics; empty list => always-match). + std::vector predicates_; + + // drop_flip-style dual-trigger state, set in doInjection(), read in executeFaults(). + std::array triggered_ = {{false, false}}; + + /** + * Parse params then rebuildPredicates() — that rebuild also runs on restore. + */ + virtual void buildPredicates(Params& params); + + /** Rebuild predicates_ from serializable config (also after deserialize). */ + virtual void rebuildPredicates(); + + /** Default: AND of predicates_. Override for arbitrary match logic. */ + virtual bool matchesState(const PipelineStateBase& state, + const EventAddress& ea) const; + + bool doInjection(Event* ev) override; + void executeFaults(Event*& ev) override; + + void serialize_order(SST::Core::Serialization::serializer& ser) override { + FaultInjectorBase::serialize_order(ser); + SST_SER(stateKey_); + SST_SER(mode_); + SST_SER(dropProb_); + SST_SER(flipProb_); + SST_SER(triggered_); + SST_SER(kernelsCsv_); + SST_SER(hasCycleRange_); + SST_SER(cycleStart_); + SST_SER(cycleEnd_); + SST_SER(regionIdsCsv_); + SST_SER(regionNamesCsv_); + // predicates_ is a vector and cannot be serialized; + // checkpoint restore uses the serialization ctor (NOT the params + // ctor), so rebuild the lambdas from the config members here. + if (ser.mode() == SST::Core::Serialization::serializer::UNPACK) + rebuildPredicates(); + } + ImplementVirtualSerializable(SST::Carcosa::PortModuleStateGate) + +private: + static Mode parseMode(const std::string& s); +}; + +} // namespace SST::Carcosa + +#endif // SST_ELEMENTS_CARCOSA_PORTMODULESTATEGATE_H diff --git a/src/sst/elements/carcosa/tests/testStateGateRegionNoOverlap.py b/src/sst/elements/carcosa/tests/testStateGateRegionNoOverlap.py new file mode 100644 index 0000000000..fa41d9461f --- /dev/null +++ b/src/sst/elements/carcosa/tests/testStateGateRegionNoOverlap.py @@ -0,0 +1,24 @@ +"""Regression test for the region-predicate address-overlap fix (#19). + +The driver publishes a valid 'decoy' region that no traffic ever touches, +and a PortModuleStateGate on the driver's mem_side (Send) is told to flip +every event with region_names="decoy". Under the fixed semantics the +predicate tests the EVENT's address range for overlap, so the gate never +fires and the run must produce exact fault-free checksums. Under the old +published-region-exists semantics every response would be flipped and the +checksum / corruption expectations here would fail. + +Run: sst testStateGateRegionNoOverlap.py +""" +import framePipelineCommon as common + +common.build( + extra_region="decoy:0x80000:64", + mem_side_gate={ + "state_key": common.STATE_KEY, + "fault_mode": "flip", + "flip_probability": "1.0", + "region_names": "decoy", + "install_direction": "Send", + }, +) diff --git a/src/sst/elements/carcosa/tests/testStateGateRegionOverlap.py b/src/sst/elements/carcosa/tests/testStateGateRegionOverlap.py new file mode 100644 index 0000000000..69e15d5c17 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testStateGateRegionOverlap.py @@ -0,0 +1,31 @@ +"""Positive half of the region-predicate address-overlap fix (#19). + +A PortModuleStateGate on the driver's mem_side (Send) flips every ACTUATE +event overlapping the action_queue region (kernels="1" AND region_names). +The fully-in-region ACTUATE read gets one payload bit flipped every frame, +so all 3 frames must classify corrupted at the watcher and diverged at the +scorer. Exact checksums are RNG-dependent and not checked; the +classification counts are deterministic because ANY in-payload bit flip of +the in-region read changes the snapshot. PREFILL/POST responses also +overlap the region but are outside the gated kernel and outside the +watcher's actuation window, so they change nothing. + +Run: sst testStateGateRegionOverlap.py +""" +import framePipelineCommon as common + +common.build( + expect_argmax_diff=3, + expect_unsafe=3, + expect_corrupted=3, + check_exact_checksums=False, + mem_side_gate={ + "state_key": common.STATE_KEY, + "fault_mode": "flip", + "flip_probability": "1.0", + "kernels": "1", + "region_names": "action_queue", + "install_direction": "Send", + "seed": "7", + }, +) From 54774226ba7beb103a6bf599ac24b8f673e57fa8 Mon Sep 17 00:00:00 2001 From: nab880 Date: Fri, 10 Jul 2026 16:03:14 -0700 Subject: [PATCH 3/6] carcosa: add ring protocol and optional BalarRingBridge --- .../carcosa/components/balarRingBridge.cc | 763 ++++++++++++++++++ .../carcosa/components/balarRingBridge.h | 212 +++++ .../elements/carcosa/components/carcosaHash.h | 35 + .../carcosa/components/ringProtocol.h | 39 + 4 files changed, 1049 insertions(+) create mode 100644 src/sst/elements/carcosa/components/balarRingBridge.cc create mode 100644 src/sst/elements/carcosa/components/balarRingBridge.h create mode 100644 src/sst/elements/carcosa/components/carcosaHash.h create mode 100644 src/sst/elements/carcosa/components/ringProtocol.h diff --git a/src/sst/elements/carcosa/components/balarRingBridge.cc b/src/sst/elements/carcosa/components/balarRingBridge.cc new file mode 100644 index 0000000000..9f8756dd49 --- /dev/null +++ b/src/sst/elements/carcosa/components/balarRingBridge.cc @@ -0,0 +1,763 @@ +// 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. +// +// Portions are copyright of other developers: +// See the file CONTRIBUTORS.TXT in the top level directory of the distribution. +// +// 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 "sst_config.h" + +#ifdef HAVE_BALAR_BRIDGE + +#include "sst/elements/carcosa/components/balarRingBridge.h" +#include "sst/elements/carcosa/components/pipelineStateRegistry.h" + +// balar packet encode/decode helpers (templates). +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace SST; +using namespace SST::Interfaces; +using namespace SST::Carcosa; +// Bring in balar's CUDA-call ABI: the BalarCudaCall*_t packets, the encode/decode +// templates, CudaAPIEnumToString, and the CudaAPI_t enum constants (CUDA_MALLOC, +// CUDA_MEMCPY, ...) which live in this namespace (mirrors balar's forked test CPU.cc). +using namespace SST::BalarComponent; + +// Chained FNV-1a (carcosaHash.h) over D2H chunks; serialized packet SM makes +// chunk order deterministic so this equals one FNV over concatenated bytes — +// same construction as CriticalActionWatcher (do not fork the FNV constant). + +namespace { + +std::string brbTrim(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 brbSplit(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 brbMapFromVec(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[brbTrim(p.substr(0, pos))] = brbTrim(p.substr(pos + delim.size())); + } + return m; +} + +std::string brbLookupParam(const std::map& params, const std::string& key, SST::Output* out) +{ + auto it = params.find(key); + if (it != params.end()) return brbTrim(it->second); + for (const auto& param : params) + if (brbTrim(param.first) == key) return brbTrim(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 ""; +} + +} // namespace + +// --------------------------------------------------------------------------- +// StandardMem double-dispatch handler adapters. +// --------------------------------------------------------------------------- +class BalarRingBridge::CacheHandlers : public StandardMem::RequestHandler { +public: + CacheHandlers(BalarRingBridge* b, SST::Output* out) : StandardMem::RequestHandler(out), b_(b) {} + ~CacheHandlers() override {} + void handle(StandardMem::ReadResp* resp) override { b_->onCacheReadResp(resp); } + void handle(StandardMem::WriteResp* resp) override { b_->onCacheWriteResp(resp); } + void handle(StandardMem::FlushResp* resp) override { b_->onCacheFlushResp(resp); } +private: + BalarRingBridge* b_; +}; + +class BalarRingBridge::MmioHandlers : public StandardMem::RequestHandler { +public: + MmioHandlers(BalarRingBridge* b, SST::Output* out) : StandardMem::RequestHandler(out), b_(b) {} + ~MmioHandlers() override {} + void handle(StandardMem::ReadResp* resp) override { b_->onMmioReadResp(resp); } + void handle(StandardMem::WriteResp* resp) override { b_->onMmioWriteResp(resp); } +private: + BalarRingBridge* b_; +}; + +// CUDA-API trace parser (from balar/testcpu/). H2D weight payloads stage into +// weightStageAddr_ (not scratch after the command packet) so EccGuard can +// confine injection to weights only. +class BalarRingBridge::CudaAPITraceParser { +public: + CudaAPITraceParser(BalarRingBridge* b, SST::Output* out, + const std::string& trace_file, const std::string& cuda_executable) + : b_(b), out_(out), cuda_executable_(cuda_executable), fat_cubin_handle_(0), has_peeked_packet_(false) + { + trace_file_ = trace_file; + size_t sep = trace_file.rfind("/"); + trace_base_path_ = (sep == std::string::npos) ? "./" : trace_file.substr(0, sep + 1); + rewind(); + } + + // Reopen the trace from the top and re-queue the fatbin registration. Called + // per replay so each ring Cmd re-runs the same staged GEMM sequence. + void rewind() + { + if (trace_stream_.is_open()) trace_stream_.close(); + trace_stream_.open(trace_file_, std::ifstream::in); + if (!trace_stream_.is_open()) + out_->fatal(CALL_INFO, -1, "BalarRingBridge: trace file '%s' does not exist\n", trace_file_.c_str()); + has_peeked_packet_ = false; + std::queue empty; + std::swap(init_packets_, empty); + // Register the fatbin only on the first replay; balar keeps the handle. + if (!registered_fatbin_) { + 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); + } + } + + 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; } + if (trace_stream_.eof()) return false; + + std::string line; + std::getline(trace_stream_, line); + if (line.empty()) return false; + 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 = brbTrim(rest); } + auto params_map = brbMapFromVec(brbSplit(line, ","), ":"); + + if (cuda_call_type.find("memalloc") != std::string::npos) { + pack.cuda_call_id = CUDA_MALLOC; + std::string dptr_name = brbLookupParam(params_map, "dptr", out_); + size_t size = 0; + std::stringstream(brbLookupParam(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 = brbLookupParam(params_map, "device_ptr", out_); + size_t size = 0; + std::stringstream(brbLookupParam(params_map, "size", out_)) >> size; + std::string data_path = trace_base_path_ + brbLookupParam(params_map, "data_file", out_); + std::ifstream data_stream(data_path, std::ios::binary); + if (!data_stream.is_open()) + out_->fatal(CALL_INFO, -1, "BalarRingBridge: 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); + memcpy(real_data, file_data.data(), size); + auto it = dptr_map_.find(dptr_name); + if (it == dptr_map_.end()) + out_->fatal(CALL_INFO, -1, "BalarRingBridge: 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; + // Separable staging: the H2D source is the dedicated weight region, + // NOT contiguous with the command packet. + pack.cuda_memcpy.src = b_->weightStageAddr_; + b_->pending_weight_payload_ = std::move(file_data); + } else { + pack.cuda_memcpy.kind = cudaMemcpyDeviceToHost; + pack.cuda_memcpy.src = (uint64_t)*it->second; + pack.cuda_memcpy.count = size; + pack.cuda_memcpy.payload = (uint64_t)real_data; + if (size >= b_->cacheLineSize_) { + pack.isSSTmem = true; + pack.cuda_memcpy.dst = b_->scratchMemAddr_ + sizeof(BalarCudaCallPacket_t); + pack.cuda_memcpy.dst_buf = nullptr; + b_->pending_d2h_is_sst_ = true; + b_->pending_d2h_sst_addr_ = pack.cuda_memcpy.dst; + } else { + uint8_t* buf = size ? (uint8_t*)malloc(size) : nullptr; + pack.cuda_memcpy.dst = (uint64_t)buf; + b_->pending_d2h_host_buf_ = buf; + } + b_->pending_d2h_bytes_ = size; + } + return true; + } + if (cuda_call_type.find("kernel launch") != std::string::npos) { + std::string func_name = brbLookupParam(params_map, "name", out_); + std::string ptx_name = brbLookupParam(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 = std::stoul(brbLookupParam(params_map, "gdx", out_)); + config.configure_call.gdy = std::stoul(brbLookupParam(params_map, "gdy", out_)); + config.configure_call.gdz = std::stoul(brbLookupParam(params_map, "gdz", out_)); + config.configure_call.bdx = std::stoul(brbLookupParam(params_map, "bdx", out_)); + config.configure_call.bdy = std::stoul(brbLookupParam(params_map, "bdy", out_)); + config.configure_call.bdz = std::stoul(brbLookupParam(params_map, "bdz", out_)); + config.configure_call.sharedMem = std::stoul(brbLookupParam(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 = brbLookupParam(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 = 0; + std::stringstream(arg_size_str) >> 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) { + set_arg.setup_argument.arg = (uint64_t)*dptr_map_.at(arg_val); + } else if (arg_val.find(".") != std::string::npos) { + double val = std::stod(arg_val); + 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 { + int val = std::stoi(arg_val); + set_arg.setup_argument.arg = 0; + 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 = brbLookupParam(params_map, "dptr", out_); + pack.cuda_free.devPtr = (void*)*dptr_map_.at(dptr_name); + return true; + } + 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; registered_fatbin_ = true; } + +private: + BalarRingBridge* b_; + SST::Output* out_; + std::string cuda_executable_; + std::string trace_file_; + std::string trace_base_path_; + std::ifstream trace_stream_; + std::queue init_packets_; + std::map dptr_map_; + std::map func_map_; + uint64_t fat_cubin_handle_; + bool registered_fatbin_ = false; + bool has_peeked_packet_; + BalarCudaCallPacket_t peeked_packet_; +}; + +// --------------------------------------------------------------------------- +// Construction / lifecycle +// --------------------------------------------------------------------------- +BalarRingBridge::BalarRingBridge(ComponentId_t id, Params& params) + : InterceptionAgentAPI(id, params) +{ + out_ = new Output("BalarRingBridge[@p:@l] ", 1, 0, Output::STDOUT); + verbose_ = params.find("verbose", false); + stateKey_ = params.find("state_key", "cpu0_vla"); + mmioAddr_ = params.find("mmio_addr", 0); + scratchMemAddr_ = params.find("scratch_mem_addr", 0); + weightStageAddr_ = params.find("weight_stage_addr", 0x20000000); + cacheLineSize_ = params.find("cache_line_size", 64); + traceFile_ = params.find("trace_file", "cuda_calls.trace"); + replayEachCmd_ = params.find("replay_each_cmd", false); + + bool found = false; + cudaExecutable_ = params.find("cuda_executable", found); + if (!found) + out_->fatal(CALL_INFO, -1, "BalarRingBridge: 'cuda_executable' is required (fatbin registration)\n"); + if (cacheLineSize_ == 0) + out_->fatal(CALL_INFO, -1, "BalarRingBridge: cache_line_size must be > 0\n"); + + TimeConverter tc = getTimeConverter(params.find("clock", "1GHz")); + cache_link_ = loadUserSubComponent( + "cache_link", ComponentInfo::SHARE_NONE, tc, + new StandardMem::Handler(this)); + mmio_link_ = loadUserSubComponent( + "mmio_link", ComponentInfo::SHARE_NONE, tc, + new StandardMem::Handler(this)); + if (!cache_link_ || !mmio_link_) + out_->fatal(CALL_INFO, -1, + "BalarRingBridge: requires 'cache_link' and 'mmio_link' StandardMem slots into balar\n"); + + cache_handlers_ = new CacheHandlers(this, out_); + mmio_handlers_ = new MmioHandlers(this, out_); +} + +BalarRingBridge::~BalarRingBridge() +{ + releasePendingD2H(); + delete out_; + delete trace_parser_; + delete cache_handlers_; + delete mmio_handlers_; +} + +void BalarRingBridge::agentInit(unsigned phase) +{ + if (cache_link_) cache_link_->init(phase); + if (mmio_link_) mmio_link_->init(phase); +} + +void BalarRingBridge::agentSetup() +{ + if (cache_link_) cache_link_->setup(); + if (mmio_link_) mmio_link_->setup(); + trace_parser_ = new CudaAPITraceParser(this, out_, traceFile_, cudaExecutable_); + if (verbose_) + out_->output("BalarRingBridge: setup, scratch=0x%" PRIx64 " weights=0x%" PRIx64 + " mmio=0x%" PRIx64 " trace=%s\n", + scratchMemAddr_, weightStageAddr_, mmioAddr_, traceFile_.c_str()); +} + +void BalarRingBridge::handleCacheEvent(StandardMem::Request* req) { req->handle(cache_handlers_); } +void BalarRingBridge::handleMmioEvent(StandardMem::Request* req) { req->handle(mmio_handlers_); } + +void BalarRingBridge::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 BalarRingBridge::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; +} + +// --------------------------------------------------------------------------- +// Ring trigger +// --------------------------------------------------------------------------- +void BalarRingBridge::handleRingEvent(HaliEvent* ev) +{ + // The driver dispatches SeqLen then Cmd per GPU-resident kernel. On Cmd we run + // the staged GEMM replay onto balar and reply Done only when the D2H result has + // been read. SeqLen/Exit and other ring traffic are ignored. + if (!ev || ev->getStr() != RingTag::Cmd) return; + + if (verbose_) out_->output("BalarRingBridge: ring Cmd %u -> GEMM replay\n", ev->getNum()); + + // If we already replayed once and are not replaying per-Cmd, just release the + // barrier: the resident-weight result is stable, so re-launching is redundant. + if (replayed_once_ && !replayEachCmd_) { + if (ringLink_) ringLink_->send(new HaliEvent(RingTag::Done, 0u)); + return; + } + if (replay_active_) { ++cmd_pending_; return; } // serialize; drain on finish + + beginTrace(); +} + +void BalarRingBridge::beginTrace() +{ + replay_active_ = true; + checksum_ = kFnv1a64OffsetBasis; // fresh per-replay fold + if (!trace_parser_) { finishReplay(); return; } + trace_parser_->rewind(); + issueNextPacket(); +} + +void BalarRingBridge::issueNextPacket() +{ + BalarCudaCallPacket_t pack{}; + if (trace_parser_ && trace_parser_->getNextPacket(pack)) { + beginPacketIssue(pack); + } else { + finishReplay(); + } +} + +// --------------------------------------------------------------------------- +// Packet-issue state machine (mirrors balar's forked test CPU, ring-driven) +// --------------------------------------------------------------------------- +void BalarRingBridge::beginPacketIssue(const BalarCudaCallPacket_t& pack) +{ + BalarCudaCallPacket_t pack_copy = pack; + std::vector* encoded = encode_balar_packet(&pack_copy); + + stage_segments_.clear(); + flush_ranges_.clear(); + + // Segment 0: the encoded command packet, in the control scratch region. + stage_segments_.push_back({scratchMemAddr_, std::vector(encoded->begin(), encoded->end())}); + delete encoded; + + // Segment 1 (H2D only): the weight payload, in the DEDICATED separable region. + if (!pending_weight_payload_.empty()) { + stage_segments_.push_back({weightStageAddr_, std::move(pending_weight_payload_)}); + pending_weight_payload_.clear(); + } + + seg_index_ = 0; + seg_offset_ = 0; + writes_outstanding_ = 0; + packet_issue_active_ = true; + + if (verbose_) + out_->output("BalarRingBridge: issue %s (%zu segments) scratch=0x%" PRIx64 "\n", + CudaAPIEnumToString(pack.cuda_call_id), stage_segments_.size(), scratchMemAddr_); + + sendNextStageChunk(); +} + +void BalarRingBridge::sendNextStageChunk() +{ + // Walk segments in order, emitting cache-line-sized writes. When the last chunk + // of the last segment is issued we flip to flushing (once its WriteResp lands). + while (seg_index_ < stage_segments_.size()) { + StageSegment& seg = stage_segments_[seg_index_]; + if (seg_offset_ >= seg.bytes.size()) { ++seg_index_; seg_offset_ = 0; continue; } + size_t chunk = std::min(seg.bytes.size() - seg_offset_, cacheLineSize_); + std::vector payload(seg.bytes.begin() + seg_offset_, + seg.bytes.begin() + seg_offset_ + chunk); + auto* req = new StandardMem::Write(seg.addr + seg_offset_, chunk, payload, false); + requests_[req->getID()] = std::make_pair("StageWrite", IfacePath::CACHE); + ++writes_outstanding_; + cache_link_->send(req); + seg_offset_ += chunk; + return; // one outstanding chunk at a time (in-order, like balar's forked test CPU) + } +} + +void BalarRingBridge::onCacheWriteResp(StandardMem::WriteResp* resp) +{ + auto it = requests_.find(resp->getID()); + if (it == requests_.end()) out_->fatal(CALL_INFO, -1, "BalarRingBridge: unknown cache WriteResp\n"); + if (it->second.first == "StageWrite") { + if (writes_outstanding_ > 0) --writes_outstanding_; + // More chunks to write? + bool more = (seg_index_ < stage_segments_.size()) && + (seg_offset_ < stage_segments_[seg_index_].bytes.size() || + seg_index_ + 1 < stage_segments_.size()); + if (more) { + sendNextStageChunk(); + } else if (writes_outstanding_ == 0) { + // All segments staged; build per-region flush ranges and flush. + flush_ranges_.clear(); + for (auto& seg : stage_segments_) flush_ranges_.push_back({seg.addr, seg.bytes.size()}); + // A prior replay may have cached the D2H destination. Invalidate its + // lines before balar's DMA writes beneath this interface so the + // completion readback cannot observe stale cache data. + if (pending_d2h_is_sst_ && pending_d2h_bytes_ > 0) { + uint64_t first_line = pending_d2h_sst_addr_ - (pending_d2h_sst_addr_ % cacheLineSize_); + uint64_t last_addr = pending_d2h_sst_addr_ + pending_d2h_bytes_ - 1; + uint64_t last_line = last_addr - (last_addr % cacheLineSize_); + flush_ranges_.push_back({first_line, (size_t)(last_line - first_line + cacheLineSize_)}); + } + size_t total_lines = 0; + for (auto& r : flush_ranges_) + total_lines += (r.second + cacheLineSize_ - 1) / cacheLineSize_; + flushes_remaining_ = total_lines; + flush_line_index_ = 0; + sendNextFlush(); + } + } else { + out_->fatal(CALL_INFO, -1, "BalarRingBridge: unexpected cache WriteResp %s\n", it->second.first.c_str()); + } + requests_.erase(it); + delete resp; +} + +void BalarRingBridge::sendNextFlush() +{ + if (flushes_remaining_ == 0) { sendDoorbell(); return; } + // Map a linear line index onto (region, line-within-region). + size_t idx = flush_line_index_; + StandardMem::Addr line_addr = 0; + for (auto& r : flush_ranges_) { + size_t lines = (r.second + cacheLineSize_ - 1) / cacheLineSize_; + if (idx < lines) { line_addr = r.first + idx * cacheLineSize_; break; } + idx -= lines; + } + auto* req = new StandardMem::FlushAddr(line_addr, cacheLineSize_, true, 1); + requests_[req->getID()] = std::make_pair("StageFlush", IfacePath::CACHE); + cache_link_->send(req); + ++flush_line_index_; +} + +void BalarRingBridge::onCacheFlushResp(StandardMem::FlushResp* resp) +{ + auto it = requests_.find(resp->getID()); + if (it == requests_.end()) out_->fatal(CALL_INFO, -1, "BalarRingBridge: unknown cache FlushResp\n"); + if (flushes_remaining_ > 0) --flushes_remaining_; + if (flushes_remaining_ > 0) sendNextFlush(); + else sendDoorbell(); + requests_.erase(it); + delete resp; +} + +void BalarRingBridge::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); + mmio_link_->send(req); +} + +void BalarRingBridge::onMmioWriteResp(StandardMem::WriteResp* resp) +{ + auto it = requests_.find(resp->getID()); + if (it == requests_.end()) out_->fatal(CALL_INFO, -1, "BalarRingBridge: unknown mmio WriteResp\n"); + if (it->second.first == "Doorbell") sendStartCudaRetRead(); + else out_->fatal(CALL_INFO, -1, "BalarRingBridge: unexpected mmio WriteResp %s\n", it->second.first.c_str()); + requests_.erase(it); + delete resp; +} + +void BalarRingBridge::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 BalarRingBridge::onMmioReadResp(StandardMem::ReadResp* resp) +{ + auto it = requests_.find(resp->getID()); + if (it == requests_.end()) out_->fatal(CALL_INFO, -1, "BalarRingBridge: unknown mmio ReadResp\n"); + if (it->second.first == "Start_CUDA_ret") { + uint64_t ret_addr = dataToUInt64(&(resp->data)); + sendReadRetPacket(ret_addr); + } else { + out_->fatal(CALL_INFO, -1, "BalarRingBridge: unexpected mmio ReadResp %s\n", it->second.first.c_str()); + } + requests_.erase(it); + delete resp; +} + +void BalarRingBridge::sendReadRetPacket(uint64_t ret_addr) +{ + auto* req = new StandardMem::Read(ret_addr, sizeof(BalarCudaCallReturnPacket_t)); + requests_[req->getID()] = std::make_pair("Read_CUDA_ret_packet", IfacePath::CACHE); + cache_link_->send(req); +} + +void BalarRingBridge::sendNextD2HRead() +{ + size_t remaining = pending_d2h_read_bytes_ - pending_d2h_read_offset_; + uint64_t addr = pending_d2h_sst_addr_ + pending_d2h_read_offset_; + size_t line_remaining = cacheLineSize_ - (addr % cacheLineSize_); + pending_d2h_read_chunk_ = std::min(remaining, line_remaining); + + auto* req = new StandardMem::Read(addr, pending_d2h_read_chunk_); + requests_[req->getID()] = std::make_pair("Read_D2H_payload", IfacePath::CACHE); + cache_link_->send(req); +} + +void BalarRingBridge::onCacheReadResp(StandardMem::ReadResp* resp) +{ + auto it = requests_.find(resp->getID()); + if (it == requests_.end()) out_->fatal(CALL_INFO, -1, "BalarRingBridge: unknown cache ReadResp\n"); + if (it->second.first == "Read_CUDA_ret_packet") { + auto* ret = decode_balar_packet(&(resp->data)); + bool complete = completeCudaCall(ret); + delete ret; + requests_.erase(it); + delete resp; + if (complete) finishCudaCall(); + else sendNextD2HRead(); + return; + } + if (it->second.first == "Read_D2H_payload") { + if (resp->data.size() != pending_d2h_read_chunk_) + out_->fatal(CALL_INFO, -1, + "BalarRingBridge: D2H readback returned %zu bytes, expected %zu\n", + resp->data.size(), pending_d2h_read_chunk_); + + checksum_ = fnv1a64(resp->data.data(), pending_d2h_read_chunk_, checksum_); + pending_d2h_read_offset_ += pending_d2h_read_chunk_; + bool complete = pending_d2h_read_offset_ == pending_d2h_read_bytes_; + requests_.erase(it); + delete resp; + + if (complete) { + if (verbose_) + out_->output("BalarRingBridge: D2H %zu bytes from SST memory -> checksum=0x%" PRIx64 "\n", + pending_d2h_read_bytes_, checksum_); + releasePendingD2H(); + finishCudaCall(); + } else { + sendNextD2HRead(); + } + return; + } + out_->fatal(CALL_INFO, -1, "BalarRingBridge: unexpected cache ReadResp %s\n", it->second.first.c_str()); + requests_.erase(it); + delete resp; +} + +bool BalarRingBridge::completeCudaCall(const BalarCudaCallReturnPacket_t* ret_pack) +{ + 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) { + if (ret_pack->cudamalloc.devptr_addr) + *(CUdeviceptr*)ret_pack->cudamalloc.devptr_addr = ret_pack->cudamalloc.malloc_addr; + if (verbose_) out_->output("BalarRingBridge: cudaMalloc -> 0x%" PRIx64 "\n", + (uint64_t)ret_pack->cudamalloc.malloc_addr); + } else if (ret_pack->cuda_call_id == CUDA_MEMCPY && + ret_pack->cudamemcpy.kind == cudaMemcpyDeviceToHost) { + // Fold GPU D2H into the action checksum. sim_data is NULL for SST-memory + // copies — keep this call active while cache-line reads fetch the DMA + // bytes balar committed to simulated memory. + size_t n = ret_pack->cudamemcpy.size; + const uint8_t* data = (const uint8_t*)ret_pack->cudamemcpy.sim_data; + if (n != pending_d2h_bytes_) + out_->fatal(CALL_INFO, -1, + "BalarRingBridge: D2H completion size %zu does not match request size %zu\n", + n, pending_d2h_bytes_); + if (data && n) { + checksum_ = fnv1a64(data, n, checksum_); + if (verbose_) out_->output("BalarRingBridge: D2H %zu bytes -> checksum=0x%" PRIx64 "\n", + n, checksum_); + releasePendingD2H(); + } else if (pending_d2h_is_sst_ && n) { + pending_d2h_read_bytes_ = n; + pending_d2h_read_offset_ = 0; + return false; + } else if (n) { + out_->fatal(CALL_INFO, -1, + "BalarRingBridge: D2H completion returned no data for a non-SST copy\n"); + } else { + releasePendingD2H(); + } + } + return true; +} + +void BalarRingBridge::finishCudaCall() +{ + packet_issue_active_ = false; + issueNextPacket(); +} + +void BalarRingBridge::releasePendingD2H() +{ + free(pending_d2h_host_buf_); + pending_d2h_host_buf_ = nullptr; + pending_d2h_sst_addr_ = 0; + pending_d2h_bytes_ = 0; + pending_d2h_read_bytes_ = 0; + pending_d2h_read_offset_ = 0; + pending_d2h_read_chunk_ = 0; + pending_d2h_is_sst_ = false; +} + +void BalarRingBridge::finishReplay() +{ + ++replays_; + replayed_once_ = true; + replay_active_ = false; + + // Publish the running checksum into the shared pipeline state; the CPU driver + // snapshots it into the frame's actionChecksum at ACTUATE close (same slot the + // mini-GPU used), and ActionScorer diffs it against the golden log. + if (!stateKey_.empty()) { + PipelineStateBase* s = PipelineStateRegistry::getMutable(stateKey_); + if (!s) s = PipelineStateRegistry::getOrCreate(stateKey_); + s->watcherActionChecksum = checksum_; + s->watcherActionChecksumValid = true; + } + + if (verbose_) + out_->output("BalarRingBridge: replay %" PRIu64 " done, checksum=0x%" PRIx64 "\n", + replays_, checksum_); + + if (ringLink_) ringLink_->send(new HaliEvent(RingTag::Done, 0u)); + + // Drain ALL Cmds that arrived mid-replay. In Done-only mode each queued + // Cmd gets its own Done; in replay-each mode start the next replay, which + // drains the rest through its own finishReplay. + while (cmd_pending_ > 0) { + --cmd_pending_; + if (replayEachCmd_) { beginTrace(); break; } + if (ringLink_) ringLink_->send(new HaliEvent(RingTag::Done, 0u)); + } +} + +#endif // HAVE_BALAR_BRIDGE diff --git a/src/sst/elements/carcosa/components/balarRingBridge.h b/src/sst/elements/carcosa/components/balarRingBridge.h new file mode 100644 index 0000000000..f8ef60ae11 --- /dev/null +++ b/src/sst/elements/carcosa/components/balarRingBridge.h @@ -0,0 +1,212 @@ +// 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. +// +// Portions are copyright of other developers: +// See the file CONTRIBUTORS.TXT in the top level directory of the distribution. +// +// 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 CARCOSA_BALAR_RING_BRIDGE_H +#define CARCOSA_BALAR_RING_BRIDGE_H + +// Built only under SST_CARCOSA_HAVE_BALAR / HAVE_BALAR_BRIDGE (needs balar CUDA +// headers). Body is fully guarded so libcarcosa still builds with no balar — +// the bridge is simply absent. +#ifdef HAVE_BALAR_BRIDGE + +#ifndef __STDC_FORMAT_MACROS +#define __STDC_FORMAT_MACROS +#endif +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// balar CUDA-call packet ABI (pulls in builtin_types.h / driver_types.h). +#include +// CUDA driver API types (CUdeviceptr) used by the trace parser's device-pointer +// map -- balar's forked test CPU (balar/testcpu/) includes these two explicitly for the same +// reason; balar_packet.h alone does not pull in cuda.h. +#include "builtin_types.h" +#include "cuda.h" + +#include +#include +#include +#include + +namespace SST { +namespace Carcosa { + +/** + * Ring GPU substrate on balar; dedicated H2D weight stage for EccGuard confinement. + */ +class BalarRingBridge : public InterceptionAgentAPI +{ +public: + SST_ELI_REGISTER_SUBCOMPONENT( + BalarRingBridge, + "carcosa", + "BalarRingBridge", + SST_ELI_ELEMENT_VERSION(1, 0, 0), + "Hali-ring GPU bridge: replays a staged GEMM CUDA-call sequence onto a real " + "balar/GPGPU-Sim device, folds the D2H result into an action checksum, and " + "replies Done over the ring (execution-driven ECC confinement on the real substrate)", + SST::Carcosa::InterceptionAgentAPI + ) + + SST_ELI_DOCUMENT_PARAMS( + {"verbose", "Enable verbose output.", "false"}, + {"clock", "Clock for the StandardMem interfaces into balar.", "1GHz"}, + {"state_key", "PipelineStateRegistry key shared with the CPU driver / ActionScorer.", "cpu0_vla"}, + {"mmio_addr", "BalarMMIO base address (doorbell).", "0"}, + {"scratch_mem_addr", "Scratch region base for CUDA-call packets (control; NOT injected).", "0"}, + {"weight_stage_addr","Dedicated H2D weight-payload staging region (the injected weight buffer).", "0x20000000"}, + {"cache_line_size", "Cache line size for scratch/weight writes and flush.", "64"}, + {"trace_file", "GEMM CUDA-API trace replayed per ring Cmd.", "cuda_calls.trace"}, + {"cuda_executable", "CUDA binary for fatbin registration.", ""}, + {"replay_each_cmd", "If true, replay the full trace on every ring Cmd; if false, replay once then Done-only.", "false"} + ) + + SST_ELI_DOCUMENT_SUBCOMPONENT_SLOTS( + {"cache_link", "StandardMem for scratch/weight staging, flush, and return-packet reads (through EccGuard).", + "SST::Interfaces::StandardMem"}, + {"mmio_link", "StandardMem for the BalarMMIO doorbell and status reads.", + "SST::Interfaces::StandardMem"} + ) + + BalarRingBridge(ComponentId_t id, Params& params); + BalarRingBridge() : InterceptionAgentAPI() {} + // Out-of-line so the (forward-declared) CudaAPITraceParser / handler types are + // complete at the delete site. + ~BalarRingBridge() override; + + // Unused data-plane hook (this agent speaks the ring + its own StandardMem links). + bool handleInterceptedEvent(SST::MemHierarchy::MemEvent* ev, + SST::Link* highlink) override { + (void)ev; (void)highlink; return false; + } + + void setRingLink(SST::Link* leftLink) override { ringLink_ = leftLink; } + void handleRingEvent(SST::Carcosa::HaliEvent* ev) override; + + void agentInit(unsigned phase) override; + void agentSetup() override; + +private: + enum class IfacePath { CACHE, MMIO }; + + // StandardMem response demux. + void handleCacheEvent(SST::Interfaces::StandardMem::Request* req); + void handleMmioEvent(SST::Interfaces::StandardMem::Request* req); + + // Packet-issue state machine (mirrors balar's forked test CPU), driven by the ring. + void beginTrace(); // kick the first CUDA-call packet of a replay + void issueNextPacket(); // pull the next packet from the trace, or finish + void beginPacketIssue(const SST::BalarComponent::BalarCudaCallPacket_t& pack); + void sendNextStageChunk(); // walk the staging segments (packet + weights) + void sendNextFlush(); + void sendDoorbell(); + void sendStartCudaRetRead(); + void sendReadRetPacket(uint64_t ret_addr); + void sendNextD2HRead(); + + void onCacheWriteResp(SST::Interfaces::StandardMem::WriteResp* resp); + void onCacheFlushResp(SST::Interfaces::StandardMem::FlushResp* resp); + void onCacheReadResp(SST::Interfaces::StandardMem::ReadResp* resp); + void onMmioWriteResp(SST::Interfaces::StandardMem::WriteResp* resp); + void onMmioReadResp(SST::Interfaces::StandardMem::ReadResp* resp); + + // Returns false when an SST-memory D2H readback must finish asynchronously. + bool completeCudaCall(const SST::BalarComponent::BalarCudaCallReturnPacket_t* ret_pack); + void finishCudaCall(); + void releasePendingD2H(); + void finishReplay(); // publish checksum + send Done, arm for next Cmd + + static void uint64ToData(uint64_t num, std::vector* data); + static uint64_t dataToUInt64(std::vector* data); + + SST::Output* out_ = nullptr; + SST::Link* ringLink_ = nullptr; + SST::Interfaces::StandardMem* cache_link_ = nullptr; + SST::Interfaces::StandardMem* mmio_link_ = nullptr; + + std::string stateKey_; + uint64_t mmioAddr_ = 0; + uint64_t scratchMemAddr_ = 0; + uint64_t weightStageAddr_ = 0x20000000; + uint64_t cacheLineSize_ = 64; + std::string traceFile_; + std::string cudaExecutable_; + bool replayEachCmd_ = false; + bool verbose_ = false; + + // Per-request bookkeeping: tag + which link it went out on. + std::map> requests_; + + // Staging: one or more (addr, bytes) segments written before the doorbell. + // Segment 0 is always the encoded command packet at scratchMemAddr_; for an + // H2D memcpy a second segment holds the weight payload at weightStageAddr_. + struct StageSegment { uint64_t addr; std::vector bytes; }; + std::vector stage_segments_; + // Filled by the trace parser for an H2D memcpy: the weight payload to stage at + // weightStageAddr_ (a dedicated, separable region) rather than intermingled + // with the command packet. Consumed by beginPacketIssue. + std::vector pending_weight_payload_; + size_t seg_index_ = 0; // which segment we're writing + size_t seg_offset_ = 0; // byte offset within the current segment + size_t writes_outstanding_ = 0; // chunk writes still in flight (across segments) + size_t flushes_remaining_ = 0; + size_t flush_line_index_ = 0; + // Staged regions plus any D2H destination that must be invalidated pre-DMA. + std::vector> flush_ranges_; // (base, bytes) + bool packet_issue_active_ = false; + + // Replay lifecycle. + bool replay_active_ = false; // a trace replay is in flight for the current Cmd + bool replayed_once_ = false; // at least one full replay has completed + uint64_t cmd_pending_ = 0; // ring Cmds queued while a replay is active + + // Chained FNV-1a over D2H results (same construction as CriticalActionWatcher); + // published to watcherActionChecksum. Reset to the offset basis each replay. + uint64_t checksum_ = kFnv1a64OffsetBasis; + uint64_t replays_ = 0; + + // State for the single in-flight D2H memcpy. Small, non-SST copies return a + // directly populated host buffer. SST-memory copies instead require an + // asynchronous StandardMem readback after balar's DMA completion. + uint8_t* pending_d2h_host_buf_ = nullptr; + uint64_t pending_d2h_sst_addr_ = 0; + size_t pending_d2h_bytes_ = 0; + size_t pending_d2h_read_bytes_ = 0; + size_t pending_d2h_read_offset_ = 0; + size_t pending_d2h_read_chunk_ = 0; + bool pending_d2h_is_sst_ = false; + + class CudaAPITraceParser; + CudaAPITraceParser* trace_parser_ = nullptr; + + // Handler adapters for StandardMem's double-dispatch RequestHandler. + class CacheHandlers; + class MmioHandlers; + CacheHandlers* cache_handlers_ = nullptr; + MmioHandlers* mmio_handlers_ = nullptr; +}; + +} // namespace Carcosa +} // namespace SST + +#endif // HAVE_BALAR_BRIDGE +#endif // CARCOSA_BALAR_RING_BRIDGE_H diff --git a/src/sst/elements/carcosa/components/carcosaHash.h b/src/sst/elements/carcosa/components/carcosaHash.h new file mode 100644 index 0000000000..6ae9b29156 --- /dev/null +++ b/src/sst/elements/carcosa/components/carcosaHash.h @@ -0,0 +1,35 @@ +// 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. + +#ifndef SST_ELEMENTS_CARCOSA_CARCOSA_HASH_H +#define SST_ELEMENTS_CARCOSA_CARCOSA_HASH_H + +#include +#include + +namespace SST { +namespace Carcosa { + +// Shared FNV-1a 64-bit for all carcosa checksum producers (do not fork). +// Chaining is concatenation: fnv1a64(b, fnv1a64(a)) == fnv1a64(a || b). +constexpr uint64_t kFnv1a64OffsetBasis = 14695981039346656037ull; +constexpr uint64_t kFnv1a64Prime = 1099511628211ull; + +inline uint64_t fnv1a64(const uint8_t* data, size_t len, + uint64_t seed = kFnv1a64OffsetBasis) { + uint64_t h = seed; + for (size_t i = 0; i < len; ++i) { + h ^= static_cast(data[i]); + h *= kFnv1a64Prime; + } + return h; +} + +} // namespace Carcosa +} // namespace SST + +#endif /* SST_ELEMENTS_CARCOSA_CARCOSA_HASH_H */ diff --git a/src/sst/elements/carcosa/components/ringProtocol.h b/src/sst/elements/carcosa/components/ringProtocol.h new file mode 100644 index 0000000000..ef547d1ae4 --- /dev/null +++ b/src/sst/elements/carcosa/components/ringProtocol.h @@ -0,0 +1,39 @@ +// 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. +// +// Portions are copyright of other developers: +// See the file CONTRIBUTORS.TXT in the top level directory +// of the distribution for more information. +// +// 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 CARCOSA_RING_PROTOCOL_H +#define CARCOSA_RING_PROTOCOL_H + +#include + +namespace SST { +namespace Carcosa { + +// Hali ring accelerator handshake: hub SeqLen+Cmd (-> optional payload), +// partner Done, hub Exit. carcosa never parses Cmd payload; partners agree privately. +namespace RingTag { + static constexpr const char* Cmd = "cmd"; // dispatch a GPU-resident kernel + static constexpr const char* SeqLen = "seqlen"; // sequence-length hint before a Cmd + static constexpr const char* Done = "done"; // partner completed the dispatched work + static constexpr const char* Exit = "exit"; // end of run +} + +// Convenience predicates (avoid scattering string literals across agents). +inline bool ringTagIs(const std::string& s, const char* tag) { return s == tag; } + +} // namespace Carcosa +} // namespace SST + +#endif // CARCOSA_RING_PROTOCOL_H From 1b7576afca2fb7ce22fe19887885494ac974e9cb Mon Sep 17 00:00:00 2001 From: nab880 Date: Fri, 10 Jul 2026 16:03:14 -0700 Subject: [PATCH 4/6] carcosa: add in-tree frame-pipeline test double --- .../carcosa/components/framePipelineDriver.cc | 279 ++++++++++++++++++ .../carcosa/components/framePipelineDriver.h | 123 ++++++++ .../carcosa/tests/framePipelineCommon.py | 195 ++++++++++++ .../carcosa/tests/testFramePipelineClean.py | 10 + .../carcosa/tests/testFramePipelineCorrupt.py | 10 + .../tests/testFramePipelineFallback.py | 14 + .../tests/testFramePipelineMissingGolden.py | 11 + 7 files changed, 642 insertions(+) create mode 100644 src/sst/elements/carcosa/components/framePipelineDriver.cc create mode 100644 src/sst/elements/carcosa/components/framePipelineDriver.h create mode 100644 src/sst/elements/carcosa/tests/framePipelineCommon.py create mode 100644 src/sst/elements/carcosa/tests/testFramePipelineClean.py create mode 100644 src/sst/elements/carcosa/tests/testFramePipelineCorrupt.py create mode 100644 src/sst/elements/carcosa/tests/testFramePipelineFallback.py create mode 100644 src/sst/elements/carcosa/tests/testFramePipelineMissingGolden.py diff --git a/src/sst/elements/carcosa/components/framePipelineDriver.cc b/src/sst/elements/carcosa/components/framePipelineDriver.cc new file mode 100644 index 0000000000..61560bd8e1 --- /dev/null +++ b/src/sst/elements/carcosa/components/framePipelineDriver.cc @@ -0,0 +1,279 @@ +// 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 "sst_config.h" +#include "sst/elements/carcosa/components/framePipelineDriver.h" +#include +#include +#include + +using namespace SST; +using namespace SST::MemHierarchy; +using namespace SST::Carcosa; + +FramePipelineDriver::FramePipelineDriver(ComponentId_t id, Params& params) + : Component(id) { + out_ = new Output("", 1, 0, Output::STDOUT); + verbose_ = params.find("verbose", false); + state_key_ = params.find("state_key", ""); + region_name_ = params.find("region_name", "action_queue"); + region_base_ = params.find("region_base", 8192); + region_size_ = params.find("region_size", 64); + frames_ = params.find("frames", 3); + corrupt_frame_ = params.find("corrupt_frame", -1); + close_kernel_id_ = params.find("close_kernel_id", 1); + expect_corrupted_frames_ = params.find("expect_corrupted_frames", -1); + + auto parseU64Csv = [](const std::string& csv, std::vector& out) { + std::stringstream ss(csv); std::string tok; + while (std::getline(ss, tok, ',')) + if (!tok.empty()) out.push_back(std::stoull(tok, nullptr, 0)); + }; + auto parseIntCsv = [](const std::string& csv, std::vector& out) { + std::stringstream ss(csv); std::string tok; + while (std::getline(ss, tok, ',')) + if (!tok.empty()) out.push_back(std::stoi(tok, nullptr, 0)); + }; + parseU64Csv(params.find("expect_checksums", ""), expect_checksums_); + parseU64Csv(params.find("frame_tokens", ""), frame_tokens_); + parseIntCsv(params.find("dropped_frames", ""), dropped_frames_); + parseIntCsv(params.find("escape_frames", ""), escape_frames_); + + if (state_key_.empty()) { + out_->fatal(CALL_INFO, -1, + "FramePipelineDriver '%s': state_key is required.\n", getName().c_str()); + } + if (region_base_ < 16) { + out_->fatal(CALL_INFO, -1, + "FramePipelineDriver '%s': region_base must be >= 16 (straddling " + "read starts at region_base - 16).\n", getName().c_str()); + } + if (region_size_ < 48) { + out_->fatal(CALL_INFO, -1, + "FramePipelineDriver '%s': region_size must be >= 48 (the script " + "covers [0,48) and leaves the rest as an unread hole).\n", + getName().c_str()); + } + + cpu_side_ = configureLink("cpu_side", + new Event::Handler(this)); + mem_side_ = configureLink("mem_side", + new Event::Handler(this)); + if (!cpu_side_ || !mem_side_) { + out_->fatal(CALL_INFO, -1, + "FramePipelineDriver '%s': both cpu_side and mem_side must be " + "connected (through the watcher under test).\n", getName().c_str()); + } + + registerClock("1GHz", + new Clock::Handler(this)); + registerAsPrimaryComponent(); + primaryComponentDoNotEndSim(); + + // Create the registry entry here rather than in setup(): the watcher's + // setup() looks the key up and fatals on a miss, and setup() order + // across components is unspecified. Constructors all run first. + state_ptr_ = PipelineStateRegistry::getOrCreate(state_key_); + state_ptr_->actuationKernelName = "ACTUATE"; + state_ptr_->currentKernel = -1; + state_ptr_->currentKernelName = "IDLE"; + state_ptr_->pipelineCycle = 0; + state_ptr_->ensureRegionSlot(0); + state_ptr_->regions[0].base = region_base_; + state_ptr_->regions[0].size = region_size_; + state_ptr_->regions[0].valid = true; + state_ptr_->regions[0].id = 0; + state_ptr_->regions[0].name = region_name_; + + std::string extra = params.find("extra_region", ""); + if (!extra.empty()) { + std::stringstream es(extra); + std::string name, base_s, size_s; + if (!std::getline(es, name, ':') || !std::getline(es, base_s, ':') || + !std::getline(es, size_s, ':')) { + out_->fatal(CALL_INFO, -1, + "FramePipelineDriver '%s': extra_region must be " + "'name:base:size' (got '%s').\n", getName().c_str(), extra.c_str()); + } + state_ptr_->ensureRegionSlot(1); + state_ptr_->regions[1].base = std::stoull(base_s, nullptr, 0); + state_ptr_->regions[1].size = std::stoull(size_s, nullptr, 0); + state_ptr_->regions[1].valid = true; + state_ptr_->regions[1].id = 1; + state_ptr_->regions[1].name = name; + } + + buildScript(); +} + +FramePipelineDriver::~FramePipelineDriver() { + delete out_; +} + +void FramePipelineDriver::buildScript() { + const uint64_t base = region_base_; + for (int f = 0; f < frames_; ++f) { + // PREFILL: full-region read the watcher must ignore. If it wrongly + // merged, the [48,64) hole would carry 0xE0-pattern bytes instead of + // zeros and every expected checksum below would mismatch. + script_.push_back({Op::Kind::Publish, 0, "PREFILL", f}); + script_.push_back({Op::Kind::Read, -1, "", f, base, 64, 0xE0, false}); + // ACTUATE: in-region read [base+16, base+48) then a straddling read + // [base-16, base+16) whose upper half overlaps the region start. + script_.push_back({Op::Kind::Publish, 1, "ACTUATE", f}); + script_.push_back({Op::Kind::Read, -1, "", f, base + 16, 32, 0x10, + f == corrupt_frame_}); + script_.push_back({Op::Kind::Read, -1, "", f, base - 16, 32, 0x50, false}); + // Frame-closing status write happens before the kernel transition in + // the real pipeline; mirror that ordering here. + script_.push_back({Op::Kind::Stamp}); + script_.push_back({Op::Kind::Publish, 2, "POST", f}); + // The watcher only notices kernel transitions on observed events; + // this read makes it finalize (and classify) the ACTUATE frame. + script_.push_back({Op::Kind::Read, -1, "", f, base, 4, 0x00, false}); + } +} + +bool FramePipelineDriver::clockTick(Cycle_t) { + if (awaiting_ || done_) return done_; + while (pc_ < script_.size()) { + const Op& op = script_[pc_++]; + switch (op.kind) { + case Op::Kind::Publish: + state_ptr_->currentKernel = op.kernelId; + state_ptr_->currentKernelName = op.kernelName; + state_ptr_->pipelineCycle = op.cycle; + cur_frame_ = op.cycle; + if (verbose_) { + out_->output("FramePipelineDriver '%s': publish kernel=%d " + "('%s') cycle=%d\n", getName().c_str(), + op.kernelId, op.kernelName.c_str(), op.cycle); + } + break; + case Op::Kind::Stamp: + stampFrame(); + break; + case Op::Kind::Read: { + cur_seed_ = op.seed; + cur_corrupt_ = op.corrupt; + MemEvent* req = new MemEvent(getName(), op.addr, op.addr & ~63ull, + Command::GetS, op.size); + cpu_side_->send(req); + awaiting_ = true; + return false; + } + } + } + done_ = true; + primaryComponentOKToEndSim(); + return true; +} + +void FramePipelineDriver::stampFrame() { + PipelineStateBase::FrameRecord fr; + fr.pipelineCycle = cur_frame_; + fr.kernelAtClose = close_kernel_id_; + fr.kernelAtCloseName = "ACTUATE"; + fr.dropped = std::find(dropped_frames_.begin(), dropped_frames_.end(), + cur_frame_) != dropped_frames_.end(); + if (static_cast(cur_frame_) < frame_tokens_.size()) + fr.actionToken = frame_tokens_[cur_frame_]; + if (std::find(escape_frames_.begin(), escape_frames_.end(), cur_frame_) + != escape_frames_.end()) { + ++state_ptr_->eccCumulativeEscapes; + } + fr.cumulativeEscapes = state_ptr_->eccCumulativeEscapes; + fr.cumulativeFlips = state_ptr_->eccCumulativeFlips; + // Same consumption contract as VlaPipelineDriver: prefer the watcher's + // fold when it observed critical bytes this frame. The watcher retires + // the valid flag itself at frame close. + if (state_ptr_->watcherActionChecksumValid) { + fr.actionChecksum = state_ptr_->watcherActionChecksum; + } + fr.simTimePs = getCurrentSimTimeNano() * 1000; + state_ptr_->frames.push_back(fr); + if (verbose_) { + out_->output("FramePipelineDriver '%s': stamped frame cycle=%d " + "checksum=%" PRIu64 "\n", getName().c_str(), + cur_frame_, fr.actionChecksum); + } +} + +void FramePipelineDriver::handleMemSide(Event* ev) { + MemEvent* req = dynamic_cast(ev); + if (!req) { + out_->fatal(CALL_INFO, -1, + "FramePipelineDriver '%s': non-MemEvent on mem_side.\n", + getName().c_str()); + } + MemEvent* resp = req->makeResponse(); + std::vector payload(req->getSize()); + for (size_t i = 0; i < payload.size(); ++i) { + payload[i] = patternByte(cur_seed_, cur_frame_, req->getAddr() + i); + } + if (cur_corrupt_ && !payload.empty()) payload[0] ^= 0x80; + resp->setPayload(payload); + mem_side_->send(resp); + delete req; +} + +void FramePipelineDriver::handleCpuSide(Event* ev) { + awaiting_ = false; + delete ev; +} + +void FramePipelineDriver::finish() { + if (!state_ptr_) return; + bool ok = true; + + if (state_ptr_->frames.size() != static_cast(frames_)) { + out_->output("FramePipelineDriver '%s': FAIL expected %d frames, " + "recorded %zu.\n", getName().c_str(), frames_, + state_ptr_->frames.size()); + ok = false; + } + if (!expect_checksums_.empty()) { + if (expect_checksums_.size() != state_ptr_->frames.size()) { + out_->output("FramePipelineDriver '%s': FAIL expect_checksums has " + "%zu entries for %zu frames.\n", getName().c_str(), + expect_checksums_.size(), state_ptr_->frames.size()); + ok = false; + } else { + for (size_t f = 0; f < expect_checksums_.size(); ++f) { + uint64_t got = state_ptr_->frames[f].actionChecksum; + uint64_t want = expect_checksums_[f]; + if (got != want) { + out_->output("FramePipelineDriver '%s': FAIL frame %zu " + "checksum %" PRIu64 " != expected %" PRIu64 ".\n", + getName().c_str(), f, got, want); + ok = false; + } + } + } + } + if (expect_corrupted_frames_ >= 0 && + state_ptr_->framesCriticalRegionCorrupted != + static_cast(expect_corrupted_frames_)) { + out_->output("FramePipelineDriver '%s': FAIL framesCriticalRegionCorrupted=" + "%" PRIu64 " != expected %" PRId64 ".\n", getName().c_str(), + state_ptr_->framesCriticalRegionCorrupted, + expect_corrupted_frames_); + ok = false; + } + + if (!ok) { + out_->fatal(CALL_INFO, -1, + "FramePipelineDriver '%s': expectations not met (see FAIL lines).\n", + getName().c_str()); + } + out_->output("FramePipelineDriver '%s': PASS %zu frames verified.\n", + getName().c_str(), state_ptr_->frames.size()); +} diff --git a/src/sst/elements/carcosa/components/framePipelineDriver.h b/src/sst/elements/carcosa/components/framePipelineDriver.h new file mode 100644 index 0000000000..33fe2b46e5 --- /dev/null +++ b/src/sst/elements/carcosa/components/framePipelineDriver.h @@ -0,0 +1,123 @@ +// 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 SST_ELEMENTS_CARCOSA_FRAME_PIPELINE_DRIVER_H +#define SST_ELEMENTS_CARCOSA_FRAME_PIPELINE_DRIVER_H + +#include "sst/elements/carcosa/components/pipelineStateRegistry.h" +#include "sst/elements/memHierarchy/memEvent.h" +#include +#include +#include +#include +#include +#include + +namespace SST { +namespace Carcosa { + +/** + * VLA pipeline test double through CriticalActionWatcher; finish() fatals on mismatch. + */ +class FramePipelineDriver : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT( + FramePipelineDriver, + "carcosa", + "FramePipelineDriver", + SST_ELI_ELEMENT_VERSION(1, 0, 0), + "In-tree frame-pipeline test double: publishes kernel transitions and " + "regions, drives scripted reads through a CriticalActionWatcher, and " + "pushes FrameRecords into PipelineStateBase::frames for ActionScorer.", + COMPONENT_CATEGORY_UNCATEGORIZED) + + SST_ELI_DOCUMENT_PARAMS( + {"state_key", "PipelineStateRegistry key to publish under (required).", ""}, + {"region_name", "Name of the published critical region.", "action_queue"}, + {"region_base", "Base address of the critical region (must be >= 16 so the straddling read has room below).", "8192"}, + {"region_size", "Size in bytes of the critical region.", "64"}, + {"frames", "Number of pipeline frames to run.", "3"}, + {"corrupt_frame", "Frame index whose in-region ACTUATE read payload gets one bit flipped (-1 = none).", "-1"}, + {"close_kernel_id", "Kernel id stamped into FrameRecord::kernelAtClose (set differently from the golden log's kernel_at_close to exercise the cycle-only fallback).", "1"}, + {"extra_region", "Optional second published region, 'name:base:size'. Used by the PortModuleStateGate tests: a gate on a published region no traffic touches must never fire.", ""}, + {"expect_checksums","Optional CSV of per-frame expected FrameRecord::actionChecksum values; finish() fatals on any mismatch.", ""}, + {"frame_tokens", "Optional CSV of per-frame action tokens (0 means unavailable).", ""}, + {"dropped_frames", "Optional CSV of frame indices to mark dropped.", ""}, + {"escape_frames", "Optional CSV of frame indices that add one cumulative ECC escape.", ""}, + {"expect_corrupted_frames", "Expected PipelineStateBase::framesCriticalRegionCorrupted at finish (-1 = don't check).", "-1"}, + {"verbose", "Enable verbose output.", "false"}) + + SST_ELI_DOCUMENT_PORTS( + {"cpu_side", "Connect to the watcher's highlink (requests out, responses in).", {"memHierarchy.MemEventBase"}}, + {"mem_side", "Connect to the watcher's lowlink (requests in, responses out).", {"memHierarchy.MemEventBase"}}) + + FramePipelineDriver(SST::ComponentId_t id, SST::Params& params); + ~FramePipelineDriver() override; + + void finish() override; + +private: + struct Op { + enum class Kind { Publish, Read, Stamp } kind; + int kernelId = -1; // Publish + std::string kernelName; // Publish + int cycle = 0; // Publish + uint64_t addr = 0; // Read + uint32_t size = 0; // Read + uint8_t seed = 0; // Read + bool corrupt = false;// Read: XOR 0x80 into payload[0] + }; + + static uint8_t patternByte(uint8_t seed, int frame, uint64_t addr) { + // Mirrored by tests/framePipelineCommon.py: keep in sync. + return static_cast((seed + 29 * frame + (addr & 0xFF)) & 0xFF); + } + + void buildScript(); + bool clockTick(SST::Cycle_t cycle); + void handleCpuSide(SST::Event* ev); + void handleMemSide(SST::Event* ev); + void stampFrame(); + + SST::Output* out_ = nullptr; + bool verbose_ = false; + + std::string state_key_; + std::string region_name_; + uint64_t region_base_ = 8192; + uint64_t region_size_ = 64; + int frames_ = 3; + int corrupt_frame_ = -1; + int close_kernel_id_ = 1; + std::vector expect_checksums_; + std::vector frame_tokens_; + std::vector dropped_frames_; + std::vector escape_frames_; + int64_t expect_corrupted_frames_ = -1; + + PipelineStateBase* state_ptr_ = nullptr; + + std::vector script_; + size_t pc_ = 0; + bool awaiting_ = false; + bool done_ = false; + int cur_frame_ = 0; + uint8_t cur_seed_ = 0; + bool cur_corrupt_ = false; + + SST::Link* cpu_side_ = nullptr; + SST::Link* mem_side_ = nullptr; +}; + +} // namespace Carcosa +} // namespace SST + +#endif /* SST_ELEMENTS_CARCOSA_FRAME_PIPELINE_DRIVER_H */ diff --git a/src/sst/elements/carcosa/tests/framePipelineCommon.py b/src/sst/elements/carcosa/tests/framePipelineCommon.py new file mode 100644 index 0000000000..cfc3ec3548 --- /dev/null +++ b/src/sst/elements/carcosa/tests/framePipelineCommon.py @@ -0,0 +1,195 @@ +"""Shared topology + oracle for the testFramePipeline*.py configs. + +Builds carcosa.FramePipelineDriver <-> carcosa.CriticalActionWatcher wiring +(driver.cpu_side <-> watcher.highlink, watcher.lowlink <-> driver.mem_side) +plus a carcosa.ActionScorer, and computes the expected per-frame watcher +checksums in pure Python — an oracle independent of the C++ FNV/merge code +under test. Golden CSVs are written to a tempfile at config time. + +Per frame f the driver produces (region base B, size 64): + PREFILL : read [B, B+64) -- must NOT be merged by the watcher + ACTUATE : read [B+16, B+48) -- fully in-region (corruptible) + read [B-16, B+16) -- straddles the region's lower edge + stamp : FrameRecord pushed with the watcher checksum + POST : read [B, B+4) -- transition event, watcher finalizes + +Expected snapshot: [0,16) straddle bytes, [16,48) in-region read bytes, +[48,64) never read during ACTUATE and must stay zero (this is what catches +a watcher that wrongly merges the PREFILL read). + +The byte pattern mirrors FramePipelineDriver::patternByte — keep in sync. +""" + +import os +import tempfile + +import sst + +# Force libmemHierarchy.so to load before libcarcosa.so (macOS flat-namespace +# RTTI lookup); the import fails but the dlopen side effect is what matters. +try: + import sst.memHierarchy # noqa: F401 +except ModuleNotFoundError: + pass + +REGION_BASE = 0x2000 +REGION_SIZE = 64 +STATE_KEY = "frame_pipeline_test" + +_FNV_OFFSET = 14695981039346656037 +_FNV_PRIME = 1099511628211 +_MASK64 = (1 << 64) - 1 + +# Read seeds, matching FramePipelineDriver::buildScript. +_SEED_ACTUATE_IN = 0x10 +_SEED_STRADDLE = 0x50 + + +def fnv1a64(data): + h = _FNV_OFFSET + for b in data: + h ^= b + h = (h * _FNV_PRIME) & _MASK64 + return h + + +def pattern_byte(seed, frame, addr): + return (seed + 29 * frame + (addr & 0xFF)) & 0xFF + + +def expected_checksum(frame, corrupt=False): + snap = bytearray(REGION_SIZE) + for i in range(16): + snap[i] = pattern_byte(_SEED_STRADDLE, frame, REGION_BASE + i) + for j in range(32): + snap[16 + j] = pattern_byte(_SEED_ACTUATE_IN, frame, REGION_BASE + 16 + j) + if corrupt: + snap[16] ^= 0x80 # first byte of the in-region ACTUATE read + return fnv1a64(bytes(snap)) + + +def _write_golden(rows): + fd, path = tempfile.mkstemp(prefix="framePipelineGolden.", suffix=".csv") + with os.fdopen(fd, "w") as f: + has_tokens = bool(rows and len(rows[0]) == 4) + f.write("pipeline_cycle,kernel_at_close,action_checksum%s\n" % + (",action_token" if has_tokens else "")) + for row in rows: + f.write(",".join(str(v) for v in row) + "\n") + return path + + +def build(frames=3, corrupt_frame=-1, close_kernel_id=1, golden_kernel_id=None, + drop_golden_frames=(), expect_argmax_diff=0, expect_unsafe=0, + expect_corrupted=0, verbose=False, extra_region=None, + mem_side_gate=None, check_exact_checksums=True): + """Wire up driver/watcher/scorer for one scenario. + + golden_kernel_id: kernel_at_close written into the golden CSV. Defaults + to close_kernel_id; set differently to force the exact (cycle, kernel) + lookup to miss so the cycle-only fallback path must fire in both the + watcher and the scorer. + drop_golden_frames: cycles omitted from the golden CSV (with + golden_required=true the watcher must fatal -> mark the config + # EXPECT_FAIL). + extra_region: 'name:base:size' second region published by the driver + (a decoy for the gate tests). + mem_side_gate: params dict for a carcosa.PortModuleStateGate installed + on the driver's mem_side port (Send direction corrupts responses before + the watcher sees them). + check_exact_checksums: pass expect_checksums to the driver. Disable for + gate-flip runs where the corrupted values are RNG-dependent and only + the classification counts are deterministic. + """ + gk = close_kernel_id if golden_kernel_id is None else golden_kernel_id + golden_rows = [(f, gk, expected_checksum(f)) + for f in range(frames) if f not in drop_golden_frames] + golden_path = _write_golden(golden_rows) + + # What the run should actually record (corruption is deterministic). + actual = [expected_checksum(f, corrupt=(f == corrupt_frame)) + for f in range(frames)] + + driver = sst.Component("driver", "carcosa.FramePipelineDriver") + driver.addParams({ + "state_key": STATE_KEY, + "region_name": "action_queue", + "region_base": REGION_BASE, + "region_size": REGION_SIZE, + "frames": frames, + "corrupt_frame": corrupt_frame, + "close_kernel_id": close_kernel_id, + "expect_corrupted_frames": expect_corrupted, + "verbose": "true" if verbose else "false", + }) + if check_exact_checksums: + driver.addParams({"expect_checksums": ",".join(str(c) for c in actual)}) + if extra_region: + driver.addParams({"extra_region": extra_region}) + if mem_side_gate: + driver.addPortModule("mem_side", "carcosa.PortModuleStateGate", + mem_side_gate) + + watcher = sst.Component("watcher", "carcosa.CriticalActionWatcher") + watcher.addParams({ + "state_key": STATE_KEY, + "critical_region": "action_queue", + "critical_len": REGION_SIZE, + "golden_log": golden_path, + "golden_required": "true", + "verbose": "true" if verbose else "false", + }) + + scorer = sst.Component("scorer", "carcosa.ActionScorer") + scorer.addParams({ + "state_key": STATE_KEY, + "golden_log": golden_path, + "golden_required": "true", + "expect_frames_total": frames, + "expect_frames_dropped": 0, + "expect_frames_argmax_diff": expect_argmax_diff, + "expect_frames_unsafe": expect_unsafe, + }) + + sst.Link("driver_watcher_high").connect( + (driver, "cpu_side", "1ns"), (watcher, "highlink", "1ns")) + sst.Link("watcher_driver_low").connect( + (watcher, "lowlink", "1ns"), (driver, "mem_side", "1ns")) + + return driver, watcher, scorer + + +def build_scorer_case(frame_tokens, golden_tokens, dropped_frames=(), + escape_frames=(), corrupt_frame=-1, **expected): + """Focused ActionScorer taxonomy/token fixture.""" + frames = len(frame_tokens) + golden_path = _write_golden([ + (f, 1, expected_checksum(f), golden_tokens[f]) for f in range(frames) + ]) + actual = [expected_checksum(f, corrupt=(f == corrupt_frame)) + for f in range(frames)] + + driver = sst.Component("driver", "carcosa.FramePipelineDriver") + driver.addParams({ + "state_key": STATE_KEY, "frames": frames, "region_name": "action_queue", + "region_base": REGION_BASE, "region_size": REGION_SIZE, + "corrupt_frame": corrupt_frame, "close_kernel_id": 1, + "expect_checksums": ",".join(str(v) for v in actual), + "frame_tokens": ",".join(str(v) for v in frame_tokens), + "dropped_frames": ",".join(str(v) for v in dropped_frames), + "escape_frames": ",".join(str(v) for v in escape_frames), + }) + watcher = sst.Component("watcher", "carcosa.CriticalActionWatcher") + watcher.addParams({"state_key": STATE_KEY, "critical_region": "action_queue", + "critical_len": REGION_SIZE, "golden_log": golden_path, + "golden_required": "true"}) + scorer = sst.Component("scorer", "carcosa.ActionScorer") + params = {"state_key": STATE_KEY, "golden_log": golden_path, + "golden_required": "true", "expect_frames_total": frames} + params.update(expected) + scorer.addParams(params) + sst.Link("driver_watcher_high").connect( + (driver, "cpu_side", "1ns"), (watcher, "highlink", "1ns")) + sst.Link("watcher_driver_low").connect( + (watcher, "lowlink", "1ns"), (driver, "mem_side", "1ns")) + return driver, watcher, scorer diff --git a/src/sst/elements/carcosa/tests/testFramePipelineClean.py b/src/sst/elements/carcosa/tests/testFramePipelineClean.py new file mode 100644 index 0000000000..1c2c2f6e8e --- /dev/null +++ b/src/sst/elements/carcosa/tests/testFramePipelineClean.py @@ -0,0 +1,10 @@ +"""Fault-free frame pipeline: watcher checksums must match the Python-computed +golden exactly (verifies lowlink response observation, the straddling +partial-overlap merge, and that PREFILL traffic is not snapshotted), no frame +may classify corrupted, and the scorer must report zero divergence. + +Run: sst testFramePipelineClean.py +""" +import framePipelineCommon as common + +common.build() diff --git a/src/sst/elements/carcosa/tests/testFramePipelineCorrupt.py b/src/sst/elements/carcosa/tests/testFramePipelineCorrupt.py new file mode 100644 index 0000000000..b56829622b --- /dev/null +++ b/src/sst/elements/carcosa/tests/testFramePipelineCorrupt.py @@ -0,0 +1,10 @@ +"""One bit flipped in frame 1's in-region ACTUATE payload: the watcher must +classify exactly that frame corrupted against its golden log, and the scorer +must flag exactly one argmax divergence / unsafe frame. + +Run: sst testFramePipelineCorrupt.py +""" +import framePipelineCommon as common + +common.build(corrupt_frame=1, expect_argmax_diff=1, expect_unsafe=1, + expect_corrupted=1) diff --git a/src/sst/elements/carcosa/tests/testFramePipelineFallback.py b/src/sst/elements/carcosa/tests/testFramePipelineFallback.py new file mode 100644 index 0000000000..6d3ad087b4 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testFramePipelineFallback.py @@ -0,0 +1,14 @@ +"""Cycle-only golden fallback: the golden CSV's kernel_at_close (9) never +matches the recorded frames (close_kernel_id=1), so the exact +(cycle, kernel) lookup misses for every frame and both the watcher and the +scorer must fall back to the unambiguous cycle-only entry. With +golden_required=true, a broken fallback fatals (unmatched frames); the +corrupt frame additionally checks the fallback comparison still flags +divergence. + +Run: sst testFramePipelineFallback.py +""" +import framePipelineCommon as common + +common.build(corrupt_frame=1, golden_kernel_id=9, expect_argmax_diff=1, + expect_unsafe=1, expect_corrupted=1) diff --git a/src/sst/elements/carcosa/tests/testFramePipelineMissingGolden.py b/src/sst/elements/carcosa/tests/testFramePipelineMissingGolden.py new file mode 100644 index 0000000000..137a66221e --- /dev/null +++ b/src/sst/elements/carcosa/tests/testFramePipelineMissingGolden.py @@ -0,0 +1,11 @@ +"""Golden CSV lacking frame 2's row with golden_required=true: the watcher +must fatal when it finalizes the uncovered frame instead of silently +skipping classification. + +# EXPECT_FAIL — the run must exit nonzero. + +Run: sst testFramePipelineMissingGolden.py +""" +import framePipelineCommon as common + +common.build(drop_golden_frames=(2,)) From 8e6b569fb1f520a5afdffb4da14279ea55ea6f59 Mon Sep 17 00:00:00 2001 From: nab880 Date: Fri, 10 Jul 2026 16:03:14 -0700 Subject: [PATCH 5/6] carcosa: add dedicated behavioral regression suite --- .../components/regressionTestComponents.cc | 218 ++++++++++++++++++ .../components/regressionTestComponents.h | 98 ++++++++ .../carcosa/tests/eccRuntimeCommon.py | 22 ++ .../elements/carcosa/tests/haliEdgeCommon.py | 18 ++ .../tests/testActionScorerDropDivergence.py | 9 + .../carcosa/tests/testActionScorerTaxonomy.py | 12 + .../tests/testActionScorerTokenFallback.py | 10 + .../carcosa/tests/testEccCampaignReentry.py | 10 + .../carcosa/tests/testEccJedecDistribution.py | 7 + .../tests/testEccModelDeterministic.py | 6 + .../tests/testEccPoissonDistribution.py | 6 + .../tests/testEccResidentDistribution.py | 9 + .../tests/testEccRuntimeCorrectable.py | 7 + .../carcosa/tests/testEccRuntimeDueDrop.py | 8 + .../carcosa/tests/testEccRuntimeEscape.py | 7 + .../carcosa/tests/testHaliDeferredComplete.py | 2 + .../carcosa/tests/testHaliDoubleDeferred.py | 3 + .../carcosa/tests/testHaliPayloadGetX.py | 2 + .../carcosa/tests/testHaliPayloadlessGetX.py | 2 + 19 files changed, 456 insertions(+) create mode 100644 src/sst/elements/carcosa/components/regressionTestComponents.cc create mode 100644 src/sst/elements/carcosa/components/regressionTestComponents.h create mode 100644 src/sst/elements/carcosa/tests/eccRuntimeCommon.py create mode 100644 src/sst/elements/carcosa/tests/haliEdgeCommon.py create mode 100644 src/sst/elements/carcosa/tests/testActionScorerDropDivergence.py create mode 100644 src/sst/elements/carcosa/tests/testActionScorerTaxonomy.py create mode 100644 src/sst/elements/carcosa/tests/testActionScorerTokenFallback.py create mode 100644 src/sst/elements/carcosa/tests/testEccCampaignReentry.py create mode 100644 src/sst/elements/carcosa/tests/testEccJedecDistribution.py create mode 100644 src/sst/elements/carcosa/tests/testEccModelDeterministic.py create mode 100644 src/sst/elements/carcosa/tests/testEccPoissonDistribution.py create mode 100644 src/sst/elements/carcosa/tests/testEccResidentDistribution.py create mode 100644 src/sst/elements/carcosa/tests/testEccRuntimeCorrectable.py create mode 100644 src/sst/elements/carcosa/tests/testEccRuntimeDueDrop.py create mode 100644 src/sst/elements/carcosa/tests/testEccRuntimeEscape.py create mode 100644 src/sst/elements/carcosa/tests/testHaliDeferredComplete.py create mode 100644 src/sst/elements/carcosa/tests/testHaliDoubleDeferred.py create mode 100644 src/sst/elements/carcosa/tests/testHaliPayloadGetX.py create mode 100644 src/sst/elements/carcosa/tests/testHaliPayloadlessGetX.py diff --git a/src/sst/elements/carcosa/components/regressionTestComponents.cc b/src/sst/elements/carcosa/components/regressionTestComponents.cc new file mode 100644 index 0000000000..858a0d91e3 --- /dev/null +++ b/src/sst/elements/carcosa/components/regressionTestComponents.cc @@ -0,0 +1,218 @@ +#include "sst_config.h" +#include "sst/elements/carcosa/components/regressionTestComponents.h" +#include "sst/elements/carcosa/components/eccModelMath.h" +#include +#include + +using namespace SST; +using namespace SST::Carcosa; +using namespace SST::MemHierarchy; + +EccModelTest::EccModelTest(ComponentId_t id, Params&) : Component(id) { + Output out("", 1, 0, Output::STDOUT); + auto require = [&](bool ok, const char* what) { + if (!ok) out.fatal(CALL_INFO, -1, "EccModelTest: FAIL %s\n", what); + }; + // 100 FIT/Mbit/h * 1024 MiB * 8 Mbit/MiB * 100 ns/event / 3.6e12 ns/h. + const double want_fit = 100.0 * 1e-9 * 1024.0 * 8.0 * 100.0 / 3.6e12; + require(std::fabs(EccModelMath::fitEventRate(100, 1024, 100) - want_fit) < 1e-30, + "FIT conversion (including byte-to-bit x8)"); + require(EccModelMath::wordCount(10, EccScheme::SECDED_64) == 2 && + EccModelMath::wordBits(10, EccScheme::SECDED_64, 0) == 64 && + EccModelMath::wordBits(10, EccScheme::SECDED_64, 1) == 16, + "partial SECDED word sizing"); + require(EccModelMath::jedecEventRate(0.25, 0.9, 64) == 0.25, + "explicit fault_event_rate precedence"); + require(EccModelMath::jedecEventRate(0.0, 0.001, 10) == 0.08, + "BER payload-bit fallback"); + std::string prior = "TARGET"; uint64_t count = 2; + require(!EccModelMath::resetCampaignEntry("TARGET", prior, count) && count == 2, + "same campaign entry preserves count"); + require(EccModelMath::resetCampaignEntry("OTHER", prior, count) && count == 0, + "off-target transition resets count"); + count = 1; + require(EccModelMath::resetCampaignEntry("TARGET", prior, count) && count == 0, + "target re-entry resets count"); + out.output("EccModelTest: PASS deterministic ECC math and campaign reset.\n"); +} + +HaliTestAgent::HaliTestAgent(ComponentId_t id, Params& params) + : InterceptionAgentAPI(id, params) { + out_ = new Output("", 1, 0, Output::STDOUT); + mode_ = params.find("mode", "payloadless_getx"); + if (mode_ == "deferred_complete") { + self_ = configureSelfLink("complete", "1ns", + new Event::Handler(this)); + } +} + +HaliTestAgent::~HaliTestAgent() { delete out_; } + +bool HaliTestAgent::handleInterceptedEvent(MemEvent*, Link*) { + out_->fatal(CALL_INFO, -1, "HaliTestAgent: payload-less GetX reached legacy API.\n"); + return true; +} + +ControlResult HaliTestAgent::handleControlAccess(ControlAccess& acc) { + if (mode_ == "payloadless_getx") + out_->fatal(CALL_INFO, -1, "HaliTestAgent: payload-less GetX reached neutral API.\n"); + if (mode_ == "payload_getx") { + if (!acc.isWrite || acc.value != 0x12345678u) + out_->fatal(CALL_INFO, -1, "HaliTestAgent: payload-bearing GetX decoded incorrectly.\n"); + return ControlResult::Handled; + } + if (mode_ == "deferred_complete") { + if (acc.isWrite) return ControlResult::Ignored; + self_->send(new Event()); + return ControlResult::Deferred; + } + return acc.isWrite ? ControlResult::Ignored : ControlResult::Deferred; +} + +void HaliTestAgent::complete(Event* ev) { + delete ev; + if (!channel_) out_->fatal(CALL_INFO, -1, "HaliTestAgent: missing ControlChannel.\n"); + channel_->completePendingRead(0xAABBCCDDu); +} + +HaliTestDriver::HaliTestDriver(ComponentId_t id, Params& params) : Component(id) { + out_ = new Output("", 1, 0, Output::STDOUT); + base_ = params.find("base", 0xBEEF0000); + mode_ = params.find("mode", "payloadless_getx"); + defer_ = mode_ == "double_defer"; + cpu_ = configureLink("cpu_side", new Event::Handler(this)); + mem_ = configureLink("mem_side", new Event::Handler(this)); + registerClock("1GHz", new Clock::Handler(this)); + registerAsPrimaryComponent(); primaryComponentDoNotEndSim(); +} + +bool HaliTestDriver::tick(Cycle_t) { + if (sent_) return false; + sent_ = true; + if (defer_) { + cpu_->send(new MemEvent(getName(), base_, base_ & ~63ull, Command::GetS, 4)); + cpu_->send(new MemEvent(getName(), base_ + 4, base_ & ~63ull, Command::GetS, 4)); + } else if (mode_ == "deferred_complete") { + cpu_->send(new MemEvent(getName(), base_, base_ & ~63ull, Command::GetS, 4)); + } else if (mode_ == "payload_getx") { + std::vector payload = {0x78, 0x56, 0x34, 0x12}; + cpu_->send(new MemEvent(getName(), base_, base_ & ~63ull, + Command::GetX, payload)); + } else { + MemEvent* req = new MemEvent(getName(), base_, base_ & ~63ull, + Command::GetX, 64); + req->getPayload().clear(); + cpu_->send(req); + } + return false; +} + +void HaliTestDriver::memEvent(Event* ev) { + auto* req = dynamic_cast(ev); + downstream_seen_ = true; + if (!req || mode_ != "payloadless_getx" || + req->getCmd() != Command::GetX || req->getPayloadSize() != 0) + out_->fatal(CALL_INFO, -1, "HaliTestDriver: forwarded request was not payload-less GetX.\n"); + MemEvent* resp = req->makeResponse(); + std::vector payload(64, 0); + resp->setPayload(payload); + mem_->send(resp); delete req; +} + +void HaliTestDriver::cpuEvent(Event* ev) { + auto* resp = dynamic_cast(ev); + if (!resp) out_->fatal(CALL_INFO, -1, "HaliTestDriver: non-MemEvent response.\n"); + if (mode_ == "deferred_complete") { + auto& p = resp->getPayload(); + if (p.size() < 4 || p[0] != 0xDD || p[1] != 0xCC || + p[2] != 0xBB || p[3] != 0xAA) + out_->fatal(CALL_INFO, -1, "HaliTestDriver: deferred read returned wrong value.\n"); + } + delete resp; passed_ = true; primaryComponentOKToEndSim(); +} + +void HaliTestDriver::finish() { + if (!defer_ && !passed_) + out_->fatal(CALL_INFO, -1, "HaliTestDriver: mode %s did not complete.\n", mode_.c_str()); + if ((mode_ == "payload_getx" || mode_ == "deferred_complete") && downstream_seen_) + out_->fatal(CALL_INFO, -1, "HaliTestDriver: handled control access leaked downstream.\n"); + if (!defer_) out_->output("HaliTestDriver: PASS mode=%s.\n", mode_.c_str()); + delete out_; out_ = nullptr; +} + +EccRuntimeTestDriver::EccRuntimeTestDriver(ComponentId_t id, Params& params) + : Component(id) { + out_ = new Output("", 1, 0, Output::STDOUT); + state_key_ = params.find("state_key", "ecc_runtime_test"); + requests_ = params.find("requests", 1); + payload_size_ = params.find("payload_size", 8); + expect_mutated_ = params.find("expect_mutated", -1); + expect_abort_ = params.find("expect_abort", -1); + expect_escapes_ = params.find("expect_escapes", -1); + std::stringstream ss(params.find("kernel_sequence", "")); + std::string tok; + while (std::getline(ss, tok, ',')) kernels_.push_back(tok); + state_ = PipelineStateRegistry::getOrCreate(state_key_); + state_->currentKernelName = "TEST"; + cpu_ = configureLink("cpu_side", new Event::Handler(this)); + mem_ = configureLink("mem_side", new Event::Handler(this)); + registerClock("1GHz", new Clock::Handler(this)); + registerAsPrimaryComponent(); primaryComponentDoNotEndSim(); +} + +bool EccRuntimeTestDriver::tick(Cycle_t) { + if (!started_) { started_ = true; issue(); } + return false; +} + +void EccRuntimeTestDriver::issue() { + if (issued_ >= requests_) return; + if (issued_ < static_cast(kernels_.size())) + state_->currentKernelName = kernels_[issued_]; + const uint64_t addr = 0x4000 + static_cast(issued_ % 64) * 64; + cpu_->send(new MemEvent(getName(), addr, addr & ~63ull, + Command::GetS, payload_size_)); + ++issued_; +} + +void EccRuntimeTestDriver::memEvent(Event* ev) { + auto* req = dynamic_cast(ev); + if (!req) out_->fatal(CALL_INFO, -1, "EccRuntimeTestDriver: non-MemEvent request.\n"); + MemEvent* resp = req->makeResponse(); + std::vector payload(payload_size_, 0xA5); + resp->setPayload(payload); + mem_->send(resp); delete req; +} + +void EccRuntimeTestDriver::cpuEvent(Event* ev) { + auto* resp = dynamic_cast(ev); + if (!resp) out_->fatal(CALL_INFO, -1, "EccRuntimeTestDriver: non-MemEvent response.\n"); + bool changed = false; + for (uint8_t b : resp->getPayload()) if (b != 0xA5) { changed = true; break; } + if (changed) ++mutated_; + delete resp; + ++completed_; + if (completed_ == requests_) primaryComponentOKToEndSim(); + else issue(); +} + +void EccRuntimeTestDriver::finish() { + bool ok = completed_ == requests_; + if (expect_mutated_ >= 0 && mutated_ != expect_mutated_) ok = false; + if (expect_abort_ >= 0 && state_->frameAbortRequested != (expect_abort_ != 0)) ok = false; + if (expect_escapes_ >= 0 && state_->eccCumulativeEscapes != + static_cast(expect_escapes_)) ok = false; + if (!ok) { + out_->fatal(CALL_INFO, -1, + "EccRuntimeTestDriver: FAIL completed=%d/%d mutated=%d abort=%d escapes=%" PRIu64 "\n", + completed_, requests_, mutated_, state_->frameAbortRequested ? 1 : 0, + state_->eccCumulativeEscapes); + } + out_->output("EccRuntimeTestDriver: PASS completed=%d mutated=%d abort=%d escapes=%" PRIu64 "\n", + completed_, mutated_, state_->frameAbortRequested ? 1 : 0, + state_->eccCumulativeEscapes); + delete out_; out_ = nullptr; +} diff --git a/src/sst/elements/carcosa/components/regressionTestComponents.h b/src/sst/elements/carcosa/components/regressionTestComponents.h new file mode 100644 index 0000000000..f3250f8657 --- /dev/null +++ b/src/sst/elements/carcosa/components/regressionTestComponents.h @@ -0,0 +1,98 @@ +#ifndef SST_ELEMENTS_CARCOSA_REGRESSION_TEST_COMPONENTS_H +#define SST_ELEMENTS_CARCOSA_REGRESSION_TEST_COMPONENTS_H + +#include "sst/elements/carcosa/components/interceptionAgentAPI.h" +#include "sst/elements/carcosa/components/pipelineStateRegistry.h" +#include +#include +#include +#include + +namespace SST { namespace Carcosa { + +class EccModelTest : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT(EccModelTest, "carcosa", "EccModelTest", + SST_ELI_ELEMENT_VERSION(1,0,0), "Self-asserting ECC model math test.", + COMPONENT_CATEGORY_UNCATEGORIZED) + EccModelTest(ComponentId_t id, Params& params); +}; + +class HaliTestAgent : public InterceptionAgentAPI { +public: + SST_ELI_REGISTER_SUBCOMPONENT(HaliTestAgent, "carcosa", "HaliTestAgent", + SST_ELI_ELEMENT_VERSION(1,0,0), "Self-asserting Hali control test agent.", + SST::Carcosa::InterceptionAgentAPI) + HaliTestAgent(ComponentId_t id, Params& params); + HaliTestAgent() : InterceptionAgentAPI() {} + ~HaliTestAgent() override; + bool handleInterceptedEvent(SST::MemHierarchy::MemEvent*, SST::Link*) override; + ControlResult handleControlAccess(ControlAccess&) override; + void setControlChannel(ControlChannel* ch) override { channel_ = ch; } +private: + void complete(Event* ev); + SST::Output* out_ = nullptr; + std::string mode_; + ControlChannel* channel_ = nullptr; + Link* self_ = nullptr; +}; + +class HaliTestDriver : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT(HaliTestDriver, "carcosa", "HaliTestDriver", + SST_ELI_ELEMENT_VERSION(1,0,0), "Self-asserting Hali data-plane driver.", + COMPONENT_CATEGORY_UNCATEGORIZED) + SST_ELI_DOCUMENT_PARAMS({"mode", "payloadless_getx or double_defer", "payloadless_getx"}, + {"base", "Intercept range base.", "3203334144"}) + SST_ELI_DOCUMENT_PORTS( + {"cpu_side", "Connect to Hali highlink.", {"memHierarchy.MemEventBase"}}, + {"mem_side", "Connect to Hali lowlink.", {"memHierarchy.MemEventBase"}}) + HaliTestDriver(ComponentId_t id, Params& params); + void finish() override; +private: + bool tick(Cycle_t); + void cpuEvent(Event*); + void memEvent(Event*); + Output* out_ = nullptr; + Link *cpu_ = nullptr, *mem_ = nullptr; + uint64_t base_ = 0; + std::string mode_; + bool defer_ = false, sent_ = false, passed_ = false, downstream_seen_ = false; +}; + +class EccRuntimeTestDriver : public SST::Component { +public: + SST_ELI_REGISTER_COMPONENT(EccRuntimeTestDriver, "carcosa", "EccRuntimeTestDriver", + SST_ELI_ELEMENT_VERSION(1,0,0), "Self-asserting EccGuard runtime driver.", + COMPONENT_CATEGORY_UNCATEGORIZED) + SST_ELI_DOCUMENT_PARAMS( + {"state_key", "Pipeline registry key.", "ecc_runtime_test"}, + {"requests", "Number of serialized reads.", "1"}, + {"payload_size", "Response payload bytes.", "8"}, + {"kernel_sequence", "Optional CSV kernel name per request.", ""}, + {"expect_mutated", "Expected mutated responses (-1 disables).", "-1"}, + {"expect_abort", "Expected frameAbortRequested (-1 disables).", "-1"}, + {"expect_escapes", "Expected cumulative escapes (-1 disables).", "-1"}) + SST_ELI_DOCUMENT_PORTS( + {"cpu_side", "Connect to EccGuard highlink.", {"memHierarchy.MemEventBase"}}, + {"mem_side", "Connect to EccGuard lowlink.", {"memHierarchy.MemEventBase"}}) + EccRuntimeTestDriver(ComponentId_t id, Params& params); + void finish() override; +private: + bool tick(Cycle_t); + void cpuEvent(Event*); + void memEvent(Event*); + void issue(); + Output* out_ = nullptr; + Link *cpu_ = nullptr, *mem_ = nullptr; + PipelineStateBase* state_ = nullptr; + std::string state_key_; + std::vector kernels_; + int requests_ = 1, payload_size_ = 8, issued_ = 0, completed_ = 0; + int mutated_ = 0, expect_mutated_ = -1, expect_abort_ = -1; + int64_t expect_escapes_ = -1; + bool started_ = false; +}; + +}} // namespace SST::Carcosa +#endif diff --git a/src/sst/elements/carcosa/tests/eccRuntimeCommon.py b/src/sst/elements/carcosa/tests/eccRuntimeCommon.py new file mode 100644 index 0000000000..6011c6454e --- /dev/null +++ b/src/sst/elements/carcosa/tests/eccRuntimeCommon.py @@ -0,0 +1,22 @@ +import sst +try: + import sst.memHierarchy # noqa: F401 +except ModuleNotFoundError: + pass + +def build(guard_params, driver_params=None): + state_key = "ecc_runtime_test" + dp = {"state_key": state_key} + if driver_params: + dp.update(driver_params) + driver = sst.Component("driver", "carcosa.EccRuntimeTestDriver") + driver.addParams(dp) + guard = sst.Component("guard", "carcosa.EccGuard") + gp = {"state_key": state_key, "apply_on_responses_only": "true", "seed": 12345} + gp.update(guard_params) + guard.addParams(gp) + sst.Link("cpu_guard").connect((driver, "cpu_side", "1ns"), + (guard, "highlink", "1ns")) + sst.Link("guard_mem").connect((guard, "lowlink", "1ns"), + (driver, "mem_side", "1ns")) + diff --git a/src/sst/elements/carcosa/tests/haliEdgeCommon.py b/src/sst/elements/carcosa/tests/haliEdgeCommon.py new file mode 100644 index 0000000000..b50002f97a --- /dev/null +++ b/src/sst/elements/carcosa/tests/haliEdgeCommon.py @@ -0,0 +1,18 @@ +import sst +try: + import sst.memHierarchy # noqa: F401 +except ModuleNotFoundError: + pass + +def build(mode): + base = 0xBEEF0000 + driver = sst.Component("driver", "carcosa.HaliTestDriver") + driver.addParams({"mode": mode, "base": base}) + hali = sst.Component("hali", "carcosa.Hali") + hali.addParams({"intercept_ranges": "0x%x,4096" % base}) + agent = hali.setSubComponent("interceptionAgent", "carcosa.HaliTestAgent") + agent.addParams({"mode": mode}) + sst.Link("cpu_hali").connect((driver, "cpu_side", "1ns"), + (hali, "highlink", "1ns")) + sst.Link("hali_mem").connect((hali, "lowlink", "1ns"), + (driver, "mem_side", "1ns")) diff --git a/src/sst/elements/carcosa/tests/testActionScorerDropDivergence.py b/src/sst/elements/carcosa/tests/testActionScorerDropDivergence.py new file mode 100644 index 0000000000..b060bb8827 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testActionScorerDropDivergence.py @@ -0,0 +1,9 @@ +from framePipelineCommon import build_scorer_case + +# Divergence takes O3 precedence even when the same frame is also dropped. +build_scorer_case( + frame_tokens=[999], golden_tokens=[100], dropped_frames=[0], + expect_frames_dropped=1, expect_frames_argmax_diff=0, + expect_frames_action_diff=1, expect_frames_unsafe=1, + expect_frames_o1=0, expect_frames_o2=0, + expect_frames_o3=1, expect_frames_o4=0) diff --git a/src/sst/elements/carcosa/tests/testActionScorerTaxonomy.py b/src/sst/elements/carcosa/tests/testActionScorerTaxonomy.py new file mode 100644 index 0000000000..8fba658ab9 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testActionScorerTaxonomy.py @@ -0,0 +1,12 @@ +from framePipelineCommon import build_scorer_case + +# O1 clean; O2 dropped/correct; O3 token mismatch; O4 escape with matching +# token. Frame 3's checksum is corrupt on purpose: token presence must make +# the matching decoded action authoritative. +build_scorer_case( + frame_tokens=[101, 102, 999, 104], golden_tokens=[101, 102, 103, 104], + dropped_frames=[1], escape_frames=[3], corrupt_frame=3, + expect_frames_dropped=1, expect_frames_argmax_diff=1, + expect_frames_action_diff=1, expect_frames_unsafe=1, + expect_frames_o1=1, expect_frames_o2=1, + expect_frames_o3=1, expect_frames_o4=1) diff --git a/src/sst/elements/carcosa/tests/testActionScorerTokenFallback.py b/src/sst/elements/carcosa/tests/testActionScorerTokenFallback.py new file mode 100644 index 0000000000..3b5617a431 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testActionScorerTokenFallback.py @@ -0,0 +1,10 @@ +from framePipelineCommon import build_scorer_case + +# Token 0 means absent. Despite a token in the golden row, scoring must fall +# back to the mismatching checksum and classify the frame O3/unsafe. +build_scorer_case( + frame_tokens=[0], golden_tokens=[77], corrupt_frame=0, + expect_frames_dropped=0, expect_frames_argmax_diff=1, + expect_frames_action_diff=0, expect_frames_unsafe=1, + expect_frames_o1=0, expect_frames_o2=0, + expect_frames_o3=1, expect_frames_o4=0) diff --git a/src/sst/elements/carcosa/tests/testEccCampaignReentry.py b/src/sst/elements/carcosa/tests/testEccCampaignReentry.py new file mode 100644 index 0000000000..7c790408c2 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccCampaignReentry.py @@ -0,0 +1,10 @@ +from eccRuntimeCommon import build +build({"fault_model": "campaign", "ecc_scheme": "secded", + "campaign_target_kernel": "TARGET", "campaign_event_budget": 2, + "campaign_event_rate": 1, "campaign_max_events_per_kernel_entry": 1, + "campaign_mode": "cell", "campaign_errors_fixed": 3, + "test_total_min": 4, "test_total_max": 4, + "test_clean_min": 2, "test_clean_max": 2, + "test_escape_min": 2, "test_escape_max": 2}, + {"requests": 4, "kernel_sequence": "TARGET,TARGET,OTHER,TARGET", + "expect_mutated": 2, "expect_abort": 0, "expect_escapes": 2}) diff --git a/src/sst/elements/carcosa/tests/testEccJedecDistribution.py b/src/sst/elements/carcosa/tests/testEccJedecDistribution.py new file mode 100644 index 0000000000..138babb9b0 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccJedecDistribution.py @@ -0,0 +1,7 @@ +from eccRuntimeCommon import build +# N=5000, explicit event probability .2 => mean 1000, sigma ~28. +build({"fault_model": "jedec_mix", "ecc_scheme": "none", "ber": 0.9, + "fault_event_rate": 0.2, + "test_total_min": 5000, "test_total_max": 5000, + "test_escape_min": 900, "test_escape_max": 1100}, + {"requests": 5000, "expect_abort": 0}) diff --git a/src/sst/elements/carcosa/tests/testEccModelDeterministic.py b/src/sst/elements/carcosa/tests/testEccModelDeterministic.py new file mode 100644 index 0000000000..f52ff161d5 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccModelDeterministic.py @@ -0,0 +1,6 @@ +import sst +try: + import sst.memHierarchy # noqa: F401 +except ModuleNotFoundError: + pass +sst.Component("ecc_model_test", "carcosa.EccModelTest") diff --git a/src/sst/elements/carcosa/tests/testEccPoissonDistribution.py b/src/sst/elements/carcosa/tests/testEccPoissonDistribution.py new file mode 100644 index 0000000000..34ecfda092 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccPoissonDistribution.py @@ -0,0 +1,6 @@ +from eccRuntimeCommon import build +# N=5000, lambda=64*0.01=.64, P(any)=1-exp(-.64)=.473. +build({"fault_model": "poisson", "ecc_scheme": "none", "ber": 0.01, + "test_total_min": 5000, "test_total_max": 5000, + "test_escape_min": 2200, "test_escape_max": 2500}, + {"requests": 5000, "expect_abort": 0}) diff --git a/src/sst/elements/carcosa/tests/testEccResidentDistribution.py b/src/sst/elements/carcosa/tests/testEccResidentDistribution.py new file mode 100644 index 0000000000..ec1d611e55 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccResidentDistribution.py @@ -0,0 +1,9 @@ +from eccRuntimeCommon import build +# Roughly 20us of serialized traffic at 5000 births/ms => mean near 100. +build({"fault_model": "resident", "ecc_scheme": "none", + "resident_addr_start": 0x4000, "resident_addr_len": 4096, + "resident_fault_rate_per_ms": 5000, + "resident_permanent_fraction": 1, "resident_mode": "cell", + "test_total_min": 5000, "test_total_max": 5000, + "test_resident_born_min": 70, "test_resident_born_max": 130}, + {"requests": 5000, "expect_abort": 0}) diff --git a/src/sst/elements/carcosa/tests/testEccRuntimeCorrectable.py b/src/sst/elements/carcosa/tests/testEccRuntimeCorrectable.py new file mode 100644 index 0000000000..72fa40ad72 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccRuntimeCorrectable.py @@ -0,0 +1,7 @@ +from eccRuntimeCommon import build +build({"fault_model": "campaign", "ecc_scheme": "secded", + "campaign_event_budget": 1, "campaign_event_rate": 1, + "campaign_mode": "cell", "campaign_errors_fixed": 1, + "test_total_min": 1, "test_total_max": 1, + "test_correctable_min": 1, "test_correctable_max": 1}, + {"expect_mutated": 0, "expect_abort": 0, "expect_escapes": 0}) diff --git a/src/sst/elements/carcosa/tests/testEccRuntimeDueDrop.py b/src/sst/elements/carcosa/tests/testEccRuntimeDueDrop.py new file mode 100644 index 0000000000..ab89ff9346 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccRuntimeDueDrop.py @@ -0,0 +1,8 @@ +from eccRuntimeCommon import build +build({"fault_model": "campaign", "ecc_scheme": "secded", + "campaign_event_budget": 1, "campaign_event_rate": 1, + "campaign_mode": "cell", "campaign_errors_fixed": 2, + "due_action": "drop_frame", + "test_total_min": 1, "test_total_max": 1, + "test_due_min": 1, "test_due_max": 1}, + {"expect_mutated": 0, "expect_abort": 1, "expect_escapes": 0}) diff --git a/src/sst/elements/carcosa/tests/testEccRuntimeEscape.py b/src/sst/elements/carcosa/tests/testEccRuntimeEscape.py new file mode 100644 index 0000000000..9a3a177abf --- /dev/null +++ b/src/sst/elements/carcosa/tests/testEccRuntimeEscape.py @@ -0,0 +1,7 @@ +from eccRuntimeCommon import build +build({"fault_model": "campaign", "ecc_scheme": "secded", + "campaign_event_budget": 1, "campaign_event_rate": 1, + "campaign_mode": "cell", "campaign_errors_fixed": 3, + "test_total_min": 1, "test_total_max": 1, + "test_escape_min": 1, "test_escape_max": 1}, + {"expect_mutated": 1, "expect_abort": 0, "expect_escapes": 1}) diff --git a/src/sst/elements/carcosa/tests/testHaliDeferredComplete.py b/src/sst/elements/carcosa/tests/testHaliDeferredComplete.py new file mode 100644 index 0000000000..a12ba69125 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testHaliDeferredComplete.py @@ -0,0 +1,2 @@ +from haliEdgeCommon import build +build("deferred_complete") diff --git a/src/sst/elements/carcosa/tests/testHaliDoubleDeferred.py b/src/sst/elements/carcosa/tests/testHaliDoubleDeferred.py new file mode 100644 index 0000000000..7bbb2fcff0 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testHaliDoubleDeferred.py @@ -0,0 +1,3 @@ +# EXPECT_FAIL: Hali must reject a second Deferred control read. +from haliEdgeCommon import build +build("double_defer") diff --git a/src/sst/elements/carcosa/tests/testHaliPayloadGetX.py b/src/sst/elements/carcosa/tests/testHaliPayloadGetX.py new file mode 100644 index 0000000000..43c89d7a15 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testHaliPayloadGetX.py @@ -0,0 +1,2 @@ +from haliEdgeCommon import build +build("payload_getx") diff --git a/src/sst/elements/carcosa/tests/testHaliPayloadlessGetX.py b/src/sst/elements/carcosa/tests/testHaliPayloadlessGetX.py new file mode 100644 index 0000000000..8dd8e2ed38 --- /dev/null +++ b/src/sst/elements/carcosa/tests/testHaliPayloadlessGetX.py @@ -0,0 +1,2 @@ +from haliEdgeCommon import build +build("payloadless_getx") From 1807bf35de48e7fa8c7c2d34880158503c7bab47 Mon Sep 17 00:00:00 2001 From: nab880 Date: Sun, 12 Jul 2026 12:52:43 -0700 Subject: [PATCH 6/6] Refactor Carcosa replay and fault handling Clarify ownership, configuration, and shared state APIs. --- src/sst/elements/carcosa/Makefile.am | 11 +- .../carcosa/components/actionScorer.cc | 48 +- .../carcosa/components/actionScorer.h | 13 +- .../carcosa/components/balarRingBridge.cc | 383 +++----------- .../carcosa/components/balarRingBridge.h | 39 +- .../carcosa/components/balarTraceParser.cc | 365 ++++++++++++++ .../carcosa/components/balarTraceParser.h | 61 +++ .../carcosa/components/componentTestBounds.h | 64 +++ .../elements/carcosa/components/configParse.h | 51 ++ .../components/criticalActionWatcher.cc | 12 +- .../components/criticalActionWatcher.h | 2 +- .../elements/carcosa/components/eccGuard.cc | 476 +++++++----------- .../elements/carcosa/components/eccGuard.h | 32 +- .../carcosa/components/eccPayloadCorruptor.cc | 120 +++++ .../carcosa/components/eccPayloadCorruptor.h | 45 ++ .../elements/carcosa/components/eccPolicy.h | 36 +- .../carcosa/components/fourStateAgent.cc | 53 +- .../carcosa/components/fourStateAgent.h | 2 +- .../carcosa/components/framePipelineDriver.cc | 27 +- .../components/pipelineStateRegistry.h | 50 ++ .../elements/carcosa/components/vlaRegions.h | 37 +- .../SimplePipeline/simplePipelineExample.cc | 26 +- .../SimplePipeline/simplePipelineExample.h | 4 +- .../carcosa/injectors/portModuleStateGate.cc | 116 +++-- .../carcosa/injectors/portModuleStateGate.h | 38 +- 25 files changed, 1221 insertions(+), 890 deletions(-) create mode 100644 src/sst/elements/carcosa/components/balarTraceParser.cc create mode 100644 src/sst/elements/carcosa/components/balarTraceParser.h create mode 100644 src/sst/elements/carcosa/components/componentTestBounds.h create mode 100644 src/sst/elements/carcosa/components/configParse.h create mode 100644 src/sst/elements/carcosa/components/eccPayloadCorruptor.cc create mode 100644 src/sst/elements/carcosa/components/eccPayloadCorruptor.h diff --git a/src/sst/elements/carcosa/Makefile.am b/src/sst/elements/carcosa/Makefile.am index 3dac048bf8..902b8ed225 100644 --- a/src/sst/elements/carcosa/Makefile.am +++ b/src/sst/elements/carcosa/Makefile.am @@ -13,11 +13,15 @@ libcarcosa_la_SOURCES = \ components/hali.h \ components/eccGuard.cc \ components/eccGuard.h \ + components/eccPayloadCorruptor.cc \ + components/eccPayloadCorruptor.h \ components/eccModelMath.h \ components/criticalActionWatcher.cc \ components/criticalActionWatcher.h \ components/eccScheme.h \ components/eccPolicy.h \ + components/componentTestBounds.h \ + components/configParse.h \ components/actionScorer.cc \ components/actionScorer.h \ components/faultInjManager.cc \ @@ -93,7 +97,9 @@ libcarcosa_la_SOURCES = \ if SST_CARCOSA_HAVE_BALAR libcarcosa_la_SOURCES += \ components/balarRingBridge.cc \ - components/balarRingBridge.h + components/balarRingBridge.h \ + components/balarTraceParser.cc \ + components/balarTraceParser.h # HAVE_BALAR_BRIDGE is passed on the command line (like balar's own -DHAVE_CUDA=1 # in CUDA_CPPFLAGS) rather than via sst_element_config.h, which element sources do # not include. CUDA_CPPFLAGS carries the CUDA include path for balar's packet headers. @@ -171,9 +177,12 @@ nobase_sst_HEADERS = \ faultlogic/randomFlipFault.h \ faultlogic/randomFlipMemHFault.h \ components/pmDataRegistry.h \ + components/componentTestBounds.h \ + components/configParse.h \ components/eccScheme.h \ components/eccPolicy.h \ components/eccGuard.h \ + components/eccPayloadCorruptor.h \ components/eccModelMath.h \ components/criticalActionWatcher.h \ components/actionScorer.h \ diff --git a/src/sst/elements/carcosa/components/actionScorer.cc b/src/sst/elements/carcosa/components/actionScorer.cc index a95933d8b7..1279d0e9e8 100644 --- a/src/sst/elements/carcosa/components/actionScorer.cc +++ b/src/sst/elements/carcosa/components/actionScorer.cc @@ -30,15 +30,15 @@ ActionScorer::ActionScorer(ComponentId_t id, Params& params) emit_golden_ = params.find("emit_golden", false); golden_required_ = params.find("golden_required", true); verbose_ = params.find("verbose", false); - expect_frames_total_ = params.find("expect_frames_total", -1); - expect_frames_dropped_ = params.find("expect_frames_dropped", -1); - expect_frames_argmax_diff_ = params.find("expect_frames_argmax_diff", -1); - expect_frames_action_diff_ = params.find("expect_frames_action_diff", -1); - expect_frames_unsafe_ = params.find("expect_frames_unsafe", -1); - expect_frames_o1_ = params.find("expect_frames_o1", -1); - expect_frames_o2_ = params.find("expect_frames_o2", -1); - expect_frames_o3_ = params.find("expect_frames_o3", -1); - expect_frames_o4_ = params.find("expect_frames_o4", -1); + test_bounds_.addExact("frames_total", params.find("expect_frames_total", -1)); + test_bounds_.addExact("frames_dropped", params.find("expect_frames_dropped", -1)); + test_bounds_.addExact("frames_argmax_diff", params.find("expect_frames_argmax_diff", -1)); + test_bounds_.addExact("frames_action_diff", params.find("expect_frames_action_diff", -1)); + test_bounds_.addExact("frames_unsafe", params.find("expect_frames_unsafe", -1)); + test_bounds_.addExact("frames_outcome_O1", params.find("expect_frames_o1", -1)); + test_bounds_.addExact("frames_outcome_O2", params.find("expect_frames_o2", -1)); + test_bounds_.addExact("frames_outcome_O3", params.find("expect_frames_o3", -1)); + test_bounds_.addExact("frames_outcome_O4", params.find("expect_frames_o4", -1)); if (state_key_.empty()) { out_->fatal(CALL_INFO, -1, "ActionScorer '%s': state_key is required.\n", getName().c_str()); @@ -341,26 +341,16 @@ void ActionScorer::finish() { getName().c_str(), unmatched, total); } - // Test hooks: turn the summary counters into pass/fail so in-tree - // configs (tests/testFramePipeline*.py) can assert scorer behavior via - // the sst exit code alone. - bool expect_ok = true; - auto check = [&](const char* name, int64_t want, uint64_t got) { - if (want < 0 || static_cast(want) == got) return; - out_->output("ActionScorer '%s': FAIL %s=%" PRIu64 " != expected %" PRId64 ".\n", - getName().c_str(), name, got, want); - expect_ok = false; - }; - check("frames_total", expect_frames_total_, total); - check("frames_dropped", expect_frames_dropped_, dropped); - check("frames_argmax_diff", expect_frames_argmax_diff_, argmax_diff); - check("frames_action_diff", expect_frames_action_diff_, action_diff); - check("frames_unsafe", expect_frames_unsafe_, unsafe); - check("frames_outcome_O1", expect_frames_o1_, o1); - check("frames_outcome_O2", expect_frames_o2_, o2); - check("frames_outcome_O3", expect_frames_o3_, o3); - check("frames_outcome_O4", expect_frames_o4_, o4); - if (!expect_ok) { + test_bounds_.set("frames_total", total); + test_bounds_.set("frames_dropped", dropped); + test_bounds_.set("frames_argmax_diff", argmax_diff); + test_bounds_.set("frames_action_diff", action_diff); + test_bounds_.set("frames_unsafe", unsafe); + test_bounds_.set("frames_outcome_O1", o1); + test_bounds_.set("frames_outcome_O2", o2); + test_bounds_.set("frames_outcome_O3", o3); + test_bounds_.set("frames_outcome_O4", o4); + if (!test_bounds_.check(*out_, getName())) { out_->fatal(CALL_INFO, -1, "ActionScorer '%s': expect_* checks failed (see FAIL lines).\n", getName().c_str()); diff --git a/src/sst/elements/carcosa/components/actionScorer.h b/src/sst/elements/carcosa/components/actionScorer.h index 537a1d4a64..51c8b8e78a 100644 --- a/src/sst/elements/carcosa/components/actionScorer.h +++ b/src/sst/elements/carcosa/components/actionScorer.h @@ -12,9 +12,7 @@ #ifndef SST_ELEMENTS_CARCOSA_ACTION_SCORER_H #define SST_ELEMENTS_CARCOSA_ACTION_SCORER_H -// ActionScorer: walks PipelineStateBase::frames on finish() into a CSV. -// Prefer actionToken over raw checksum for divergence (O3 vs O4); missing -// golden_log fatals unless golden_required=false (empty log => no divergence). +#include "sst/elements/carcosa/components/componentTestBounds.h" #include #include @@ -90,14 +88,7 @@ class ActionScorer : public SST::Component { std::vector golden_; bool golden_loaded_ = false; - // Test hooks (-1 = unchecked); see the expect_* params. - int64_t expect_frames_total_ = -1; - int64_t expect_frames_dropped_ = -1; - int64_t expect_frames_argmax_diff_ = -1; - int64_t expect_frames_action_diff_ = -1; - int64_t expect_frames_unsafe_ = -1; - int64_t expect_frames_o1_ = -1, expect_frames_o2_ = -1; - int64_t expect_frames_o3_ = -1, expect_frames_o4_ = -1; + ComponentTestBounds test_bounds_; SST::Output* out_ = nullptr; diff --git a/src/sst/elements/carcosa/components/balarRingBridge.cc b/src/sst/elements/carcosa/components/balarRingBridge.cc index 9f8756dd49..a63e75b61c 100644 --- a/src/sst/elements/carcosa/components/balarRingBridge.cc +++ b/src/sst/elements/carcosa/components/balarRingBridge.cc @@ -16,6 +16,7 @@ #ifdef HAVE_BALAR_BRIDGE #include "sst/elements/carcosa/components/balarRingBridge.h" +#include "sst/elements/carcosa/components/balarTraceParser.h" #include "sst/elements/carcosa/components/pipelineStateRegistry.h" // balar packet encode/decode helpers (templates). @@ -23,72 +24,13 @@ #include #include -#include #include -#include -#include -#include -#include using namespace SST; using namespace SST::Interfaces; using namespace SST::Carcosa; -// Bring in balar's CUDA-call ABI: the BalarCudaCall*_t packets, the encode/decode -// templates, CudaAPIEnumToString, and the CudaAPI_t enum constants (CUDA_MALLOC, -// CUDA_MEMCPY, ...) which live in this namespace (mirrors balar's forked test CPU.cc). using namespace SST::BalarComponent; -// Chained FNV-1a (carcosaHash.h) over D2H chunks; serialized packet SM makes -// chunk order deterministic so this equals one FNV over concatenated bytes — -// same construction as CriticalActionWatcher (do not fork the FNV constant). - -namespace { - -std::string brbTrim(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 brbSplit(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 brbMapFromVec(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[brbTrim(p.substr(0, pos))] = brbTrim(p.substr(pos + delim.size())); - } - return m; -} - -std::string brbLookupParam(const std::map& params, const std::string& key, SST::Output* out) -{ - auto it = params.find(key); - if (it != params.end()) return brbTrim(it->second); - for (const auto& param : params) - if (brbTrim(param.first) == key) return brbTrim(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 ""; -} - -} // namespace - // --------------------------------------------------------------------------- // StandardMem double-dispatch handler adapters. // --------------------------------------------------------------------------- @@ -113,243 +55,24 @@ class BalarRingBridge::MmioHandlers : public StandardMem::RequestHandler { BalarRingBridge* b_; }; -// CUDA-API trace parser (from balar/testcpu/). H2D weight payloads stage into -// weightStageAddr_ (not scratch after the command packet) so EccGuard can -// confine injection to weights only. -class BalarRingBridge::CudaAPITraceParser { -public: - CudaAPITraceParser(BalarRingBridge* b, SST::Output* out, - const std::string& trace_file, const std::string& cuda_executable) - : b_(b), out_(out), cuda_executable_(cuda_executable), fat_cubin_handle_(0), has_peeked_packet_(false) - { - trace_file_ = trace_file; - size_t sep = trace_file.rfind("/"); - trace_base_path_ = (sep == std::string::npos) ? "./" : trace_file.substr(0, sep + 1); - rewind(); - } - - // Reopen the trace from the top and re-queue the fatbin registration. Called - // per replay so each ring Cmd re-runs the same staged GEMM sequence. - void rewind() - { - if (trace_stream_.is_open()) trace_stream_.close(); - trace_stream_.open(trace_file_, std::ifstream::in); - if (!trace_stream_.is_open()) - out_->fatal(CALL_INFO, -1, "BalarRingBridge: trace file '%s' does not exist\n", trace_file_.c_str()); - has_peeked_packet_ = false; - std::queue empty; - std::swap(init_packets_, empty); - // Register the fatbin only on the first replay; balar keeps the handle. - if (!registered_fatbin_) { - 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); - } - } - - 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; } - if (trace_stream_.eof()) return false; - - std::string line; - std::getline(trace_stream_, line); - if (line.empty()) return false; - 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 = brbTrim(rest); } - auto params_map = brbMapFromVec(brbSplit(line, ","), ":"); - - if (cuda_call_type.find("memalloc") != std::string::npos) { - pack.cuda_call_id = CUDA_MALLOC; - std::string dptr_name = brbLookupParam(params_map, "dptr", out_); - size_t size = 0; - std::stringstream(brbLookupParam(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 = brbLookupParam(params_map, "device_ptr", out_); - size_t size = 0; - std::stringstream(brbLookupParam(params_map, "size", out_)) >> size; - std::string data_path = trace_base_path_ + brbLookupParam(params_map, "data_file", out_); - std::ifstream data_stream(data_path, std::ios::binary); - if (!data_stream.is_open()) - out_->fatal(CALL_INFO, -1, "BalarRingBridge: 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); - memcpy(real_data, file_data.data(), size); - auto it = dptr_map_.find(dptr_name); - if (it == dptr_map_.end()) - out_->fatal(CALL_INFO, -1, "BalarRingBridge: 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; - // Separable staging: the H2D source is the dedicated weight region, - // NOT contiguous with the command packet. - pack.cuda_memcpy.src = b_->weightStageAddr_; - b_->pending_weight_payload_ = std::move(file_data); - } else { - pack.cuda_memcpy.kind = cudaMemcpyDeviceToHost; - pack.cuda_memcpy.src = (uint64_t)*it->second; - pack.cuda_memcpy.count = size; - pack.cuda_memcpy.payload = (uint64_t)real_data; - if (size >= b_->cacheLineSize_) { - pack.isSSTmem = true; - pack.cuda_memcpy.dst = b_->scratchMemAddr_ + sizeof(BalarCudaCallPacket_t); - pack.cuda_memcpy.dst_buf = nullptr; - b_->pending_d2h_is_sst_ = true; - b_->pending_d2h_sst_addr_ = pack.cuda_memcpy.dst; - } else { - uint8_t* buf = size ? (uint8_t*)malloc(size) : nullptr; - pack.cuda_memcpy.dst = (uint64_t)buf; - b_->pending_d2h_host_buf_ = buf; - } - b_->pending_d2h_bytes_ = size; - } - return true; - } - if (cuda_call_type.find("kernel launch") != std::string::npos) { - std::string func_name = brbLookupParam(params_map, "name", out_); - std::string ptx_name = brbLookupParam(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 = std::stoul(brbLookupParam(params_map, "gdx", out_)); - config.configure_call.gdy = std::stoul(brbLookupParam(params_map, "gdy", out_)); - config.configure_call.gdz = std::stoul(brbLookupParam(params_map, "gdz", out_)); - config.configure_call.bdx = std::stoul(brbLookupParam(params_map, "bdx", out_)); - config.configure_call.bdy = std::stoul(brbLookupParam(params_map, "bdy", out_)); - config.configure_call.bdz = std::stoul(brbLookupParam(params_map, "bdz", out_)); - config.configure_call.sharedMem = std::stoul(brbLookupParam(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 = brbLookupParam(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 = 0; - std::stringstream(arg_size_str) >> 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) { - set_arg.setup_argument.arg = (uint64_t)*dptr_map_.at(arg_val); - } else if (arg_val.find(".") != std::string::npos) { - double val = std::stod(arg_val); - 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 { - int val = std::stoi(arg_val); - set_arg.setup_argument.arg = 0; - 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 = brbLookupParam(params_map, "dptr", out_); - pack.cuda_free.devPtr = (void*)*dptr_map_.at(dptr_name); - return true; - } - 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; registered_fatbin_ = true; } - -private: - BalarRingBridge* b_; - SST::Output* out_; - std::string cuda_executable_; - std::string trace_file_; - std::string trace_base_path_; - std::ifstream trace_stream_; - std::queue init_packets_; - std::map dptr_map_; - std::map func_map_; - uint64_t fat_cubin_handle_; - bool registered_fatbin_ = false; - bool has_peeked_packet_; - BalarCudaCallPacket_t peeked_packet_; -}; - -// --------------------------------------------------------------------------- -// Construction / lifecycle -// --------------------------------------------------------------------------- BalarRingBridge::BalarRingBridge(ComponentId_t id, Params& params) : InterceptionAgentAPI(id, params) { out_ = new Output("BalarRingBridge[@p:@l] ", 1, 0, Output::STDOUT); verbose_ = params.find("verbose", false); - stateKey_ = params.find("state_key", "cpu0_vla"); - mmioAddr_ = params.find("mmio_addr", 0); - scratchMemAddr_ = params.find("scratch_mem_addr", 0); - weightStageAddr_ = params.find("weight_stage_addr", 0x20000000); - cacheLineSize_ = params.find("cache_line_size", 64); - traceFile_ = params.find("trace_file", "cuda_calls.trace"); - replayEachCmd_ = params.find("replay_each_cmd", false); + state_key_ = params.find("state_key", "cpu0_vla"); + mmio_addr_ = params.find("mmio_addr", 0); + scratch_mem_addr_ = params.find("scratch_mem_addr", 0); + weight_stage_addr_ = params.find("weight_stage_addr", 0x20000000); + cache_line_size_ = params.find("cache_line_size", 64); + trace_file_ = params.find("trace_file", "cuda_calls.trace"); + replay_each_cmd_ = params.find("replay_each_cmd", false); bool found = false; - cudaExecutable_ = params.find("cuda_executable", found); + cuda_executable_ = params.find("cuda_executable", found); if (!found) out_->fatal(CALL_INFO, -1, "BalarRingBridge: 'cuda_executable' is required (fatbin registration)\n"); - if (cacheLineSize_ == 0) + if (cache_line_size_ == 0) out_->fatal(CALL_INFO, -1, "BalarRingBridge: cache_line_size must be > 0\n"); TimeConverter tc = getTimeConverter(params.find("clock", "1GHz")); @@ -370,10 +93,10 @@ BalarRingBridge::BalarRingBridge(ComponentId_t id, Params& params) BalarRingBridge::~BalarRingBridge() { releasePendingD2H(); - delete out_; delete trace_parser_; delete cache_handlers_; delete mmio_handlers_; + delete out_; } void BalarRingBridge::agentInit(unsigned phase) @@ -386,11 +109,11 @@ void BalarRingBridge::agentSetup() { if (cache_link_) cache_link_->setup(); if (mmio_link_) mmio_link_->setup(); - trace_parser_ = new CudaAPITraceParser(this, out_, traceFile_, cudaExecutable_); + trace_parser_ = new BalarTraceParser(out_, trace_file_, cuda_executable_); if (verbose_) out_->output("BalarRingBridge: setup, scratch=0x%" PRIx64 " weights=0x%" PRIx64 " mmio=0x%" PRIx64 " trace=%s\n", - scratchMemAddr_, weightStageAddr_, mmioAddr_, traceFile_.c_str()); + scratch_mem_addr_, weight_stage_addr_, mmio_addr_, trace_file_.c_str()); } void BalarRingBridge::handleCacheEvent(StandardMem::Request* req) { req->handle(cache_handlers_); } @@ -423,8 +146,8 @@ void BalarRingBridge::handleRingEvent(HaliEvent* ev) // If we already replayed once and are not replaying per-Cmd, just release the // barrier: the resident-weight result is stable, so re-launching is redundant. - if (replayed_once_ && !replayEachCmd_) { - if (ringLink_) ringLink_->send(new HaliEvent(RingTag::Done, 0u)); + if (replayed_once_ && !replay_each_cmd_) { + if (ring_link_) ring_link_->send(new HaliEvent(RingTag::Done, 0u)); return; } if (replay_active_) { ++cmd_pending_; return; } // serialize; drain on finish @@ -443,17 +166,35 @@ void BalarRingBridge::beginTrace() void BalarRingBridge::issueNextPacket() { - BalarCudaCallPacket_t pack{}; - if (trace_parser_ && trace_parser_->getNextPacket(pack)) { + BalarTracePacket trace_packet; + if (trace_parser_ && trace_parser_->next(trace_packet)) { + BalarCudaCallPacket_t& pack = trace_packet.packet; + if (trace_packet.is_h2d) { + pack.isSSTmem = true; + pack.cuda_memcpy.src = weight_stage_addr_; + pending_weight_payload_ = std::move(trace_packet.h2d_payload); + } + if (trace_packet.is_d2h) { + releasePendingD2H(); + pending_d2h_bytes_ = trace_packet.d2h_bytes; + if (trace_packet.d2h_bytes >= cache_line_size_) { + pack.isSSTmem = true; + pack.cuda_memcpy.dst = scratch_mem_addr_ + sizeof(BalarCudaCallPacket_t); + pack.cuda_memcpy.dst_buf = nullptr; + pending_d2h_is_sst_ = true; + pending_d2h_sst_addr_ = pack.cuda_memcpy.dst; + } else { + pending_d2h_host_buf_.assign(trace_packet.d2h_bytes, 0); + pack.cuda_memcpy.dst = pending_d2h_host_buf_.empty() + ? 0 : reinterpret_cast(pending_d2h_host_buf_.data()); + } + } beginPacketIssue(pack); } else { finishReplay(); } } -// --------------------------------------------------------------------------- -// Packet-issue state machine (mirrors balar's forked test CPU, ring-driven) -// --------------------------------------------------------------------------- void BalarRingBridge::beginPacketIssue(const BalarCudaCallPacket_t& pack) { BalarCudaCallPacket_t pack_copy = pack; @@ -463,12 +204,12 @@ void BalarRingBridge::beginPacketIssue(const BalarCudaCallPacket_t& pack) flush_ranges_.clear(); // Segment 0: the encoded command packet, in the control scratch region. - stage_segments_.push_back({scratchMemAddr_, std::vector(encoded->begin(), encoded->end())}); + stage_segments_.push_back({scratch_mem_addr_, std::vector(encoded->begin(), encoded->end())}); delete encoded; // Segment 1 (H2D only): the weight payload, in the DEDICATED separable region. if (!pending_weight_payload_.empty()) { - stage_segments_.push_back({weightStageAddr_, std::move(pending_weight_payload_)}); + stage_segments_.push_back({weight_stage_addr_, std::move(pending_weight_payload_)}); pending_weight_payload_.clear(); } @@ -479,7 +220,7 @@ void BalarRingBridge::beginPacketIssue(const BalarCudaCallPacket_t& pack) if (verbose_) out_->output("BalarRingBridge: issue %s (%zu segments) scratch=0x%" PRIx64 "\n", - CudaAPIEnumToString(pack.cuda_call_id), stage_segments_.size(), scratchMemAddr_); + CudaAPIEnumToString(pack.cuda_call_id), stage_segments_.size(), scratch_mem_addr_); sendNextStageChunk(); } @@ -491,7 +232,7 @@ void BalarRingBridge::sendNextStageChunk() while (seg_index_ < stage_segments_.size()) { StageSegment& seg = stage_segments_[seg_index_]; if (seg_offset_ >= seg.bytes.size()) { ++seg_index_; seg_offset_ = 0; continue; } - size_t chunk = std::min(seg.bytes.size() - seg_offset_, cacheLineSize_); + size_t chunk = std::min(seg.bytes.size() - seg_offset_, cache_line_size_); std::vector payload(seg.bytes.begin() + seg_offset_, seg.bytes.begin() + seg_offset_ + chunk); auto* req = new StandardMem::Write(seg.addr + seg_offset_, chunk, payload, false); @@ -523,14 +264,14 @@ void BalarRingBridge::onCacheWriteResp(StandardMem::WriteResp* resp) // lines before balar's DMA writes beneath this interface so the // completion readback cannot observe stale cache data. if (pending_d2h_is_sst_ && pending_d2h_bytes_ > 0) { - uint64_t first_line = pending_d2h_sst_addr_ - (pending_d2h_sst_addr_ % cacheLineSize_); + uint64_t first_line = pending_d2h_sst_addr_ - (pending_d2h_sst_addr_ % cache_line_size_); uint64_t last_addr = pending_d2h_sst_addr_ + pending_d2h_bytes_ - 1; - uint64_t last_line = last_addr - (last_addr % cacheLineSize_); - flush_ranges_.push_back({first_line, (size_t)(last_line - first_line + cacheLineSize_)}); + uint64_t last_line = last_addr - (last_addr % cache_line_size_); + flush_ranges_.push_back({first_line, (size_t)(last_line - first_line + cache_line_size_)}); } size_t total_lines = 0; for (auto& r : flush_ranges_) - total_lines += (r.second + cacheLineSize_ - 1) / cacheLineSize_; + total_lines += (r.second + cache_line_size_ - 1) / cache_line_size_; flushes_remaining_ = total_lines; flush_line_index_ = 0; sendNextFlush(); @@ -549,11 +290,11 @@ void BalarRingBridge::sendNextFlush() size_t idx = flush_line_index_; StandardMem::Addr line_addr = 0; for (auto& r : flush_ranges_) { - size_t lines = (r.second + cacheLineSize_ - 1) / cacheLineSize_; - if (idx < lines) { line_addr = r.first + idx * cacheLineSize_; break; } + size_t lines = (r.second + cache_line_size_ - 1) / cache_line_size_; + if (idx < lines) { line_addr = r.first + idx * cache_line_size_; break; } idx -= lines; } - auto* req = new StandardMem::FlushAddr(line_addr, cacheLineSize_, true, 1); + auto* req = new StandardMem::FlushAddr(line_addr, cache_line_size_, true, 1); requests_[req->getID()] = std::make_pair("StageFlush", IfacePath::CACHE); cache_link_->send(req); ++flush_line_index_; @@ -573,8 +314,8 @@ void BalarRingBridge::onCacheFlushResp(StandardMem::FlushResp* resp) void BalarRingBridge::sendDoorbell() { std::vector payload; - uint64ToData(scratchMemAddr_, &payload); - auto* req = new StandardMem::Write(mmioAddr_, payload.size(), payload, false); + uint64ToData(scratch_mem_addr_, &payload); + auto* req = new StandardMem::Write(mmio_addr_, payload.size(), payload, false); requests_[req->getID()] = std::make_pair("Doorbell", IfacePath::MMIO); mmio_link_->send(req); } @@ -591,7 +332,7 @@ void BalarRingBridge::onMmioWriteResp(StandardMem::WriteResp* resp) void BalarRingBridge::sendStartCudaRetRead() { - auto* req = new StandardMem::Read(mmioAddr_, sizeof(uint64_t)); + auto* req = new StandardMem::Read(mmio_addr_, sizeof(uint64_t)); requests_[req->getID()] = std::make_pair("Start_CUDA_ret", IfacePath::MMIO); mmio_link_->send(req); } @@ -621,7 +362,7 @@ void BalarRingBridge::sendNextD2HRead() { size_t remaining = pending_d2h_read_bytes_ - pending_d2h_read_offset_; uint64_t addr = pending_d2h_sst_addr_ + pending_d2h_read_offset_; - size_t line_remaining = cacheLineSize_ - (addr % cacheLineSize_); + size_t line_remaining = cache_line_size_ - (addr % cache_line_size_); pending_d2h_read_chunk_ = std::min(remaining, line_remaining); auto* req = new StandardMem::Read(addr, pending_d2h_read_chunk_); @@ -718,8 +459,7 @@ void BalarRingBridge::finishCudaCall() void BalarRingBridge::releasePendingD2H() { - free(pending_d2h_host_buf_); - pending_d2h_host_buf_ = nullptr; + pending_d2h_host_buf_.clear(); pending_d2h_sst_addr_ = 0; pending_d2h_bytes_ = 0; pending_d2h_read_bytes_ = 0; @@ -737,26 +477,25 @@ void BalarRingBridge::finishReplay() // Publish the running checksum into the shared pipeline state; the CPU driver // snapshots it into the frame's actionChecksum at ACTUATE close (same slot the // mini-GPU used), and ActionScorer diffs it against the golden log. - if (!stateKey_.empty()) { - PipelineStateBase* s = PipelineStateRegistry::getMutable(stateKey_); - if (!s) s = PipelineStateRegistry::getOrCreate(stateKey_); - s->watcherActionChecksum = checksum_; - s->watcherActionChecksumValid = true; + if (!state_key_.empty()) { + PipelineStateBase* s = PipelineStateRegistry::getMutable(state_key_); + if (!s) s = PipelineStateRegistry::getOrCreate(state_key_); + s->publishWatcherChecksum(checksum_); } if (verbose_) out_->output("BalarRingBridge: replay %" PRIu64 " done, checksum=0x%" PRIx64 "\n", replays_, checksum_); - if (ringLink_) ringLink_->send(new HaliEvent(RingTag::Done, 0u)); + if (ring_link_) ring_link_->send(new HaliEvent(RingTag::Done, 0u)); // Drain ALL Cmds that arrived mid-replay. In Done-only mode each queued // Cmd gets its own Done; in replay-each mode start the next replay, which // drains the rest through its own finishReplay. while (cmd_pending_ > 0) { --cmd_pending_; - if (replayEachCmd_) { beginTrace(); break; } - if (ringLink_) ringLink_->send(new HaliEvent(RingTag::Done, 0u)); + if (replay_each_cmd_) { beginTrace(); break; } + if (ring_link_) ring_link_->send(new HaliEvent(RingTag::Done, 0u)); } } diff --git a/src/sst/elements/carcosa/components/balarRingBridge.h b/src/sst/elements/carcosa/components/balarRingBridge.h index f8ef60ae11..29662b31d9 100644 --- a/src/sst/elements/carcosa/components/balarRingBridge.h +++ b/src/sst/elements/carcosa/components/balarRingBridge.h @@ -49,9 +49,8 @@ namespace SST { namespace Carcosa { -/** - * Ring GPU substrate on balar; dedicated H2D weight stage for EccGuard confinement. - */ +class BalarTraceParser; + class BalarRingBridge : public InterceptionAgentAPI { public: @@ -88,8 +87,6 @@ class BalarRingBridge : public InterceptionAgentAPI BalarRingBridge(ComponentId_t id, Params& params); BalarRingBridge() : InterceptionAgentAPI() {} - // Out-of-line so the (forward-declared) CudaAPITraceParser / handler types are - // complete at the delete site. ~BalarRingBridge() override; // Unused data-plane hook (this agent speaks the ring + its own StandardMem links). @@ -98,7 +95,7 @@ class BalarRingBridge : public InterceptionAgentAPI (void)ev; (void)highlink; return false; } - void setRingLink(SST::Link* leftLink) override { ringLink_ = leftLink; } + void setRingLink(SST::Link* leftLink) override { ring_link_ = leftLink; } void handleRingEvent(SST::Carcosa::HaliEvent* ev) override; void agentInit(unsigned phase) override; @@ -138,18 +135,18 @@ class BalarRingBridge : public InterceptionAgentAPI static uint64_t dataToUInt64(std::vector* data); SST::Output* out_ = nullptr; - SST::Link* ringLink_ = nullptr; + SST::Link* ring_link_ = nullptr; SST::Interfaces::StandardMem* cache_link_ = nullptr; SST::Interfaces::StandardMem* mmio_link_ = nullptr; - std::string stateKey_; - uint64_t mmioAddr_ = 0; - uint64_t scratchMemAddr_ = 0; - uint64_t weightStageAddr_ = 0x20000000; - uint64_t cacheLineSize_ = 64; - std::string traceFile_; - std::string cudaExecutable_; - bool replayEachCmd_ = false; + std::string state_key_; + uint64_t mmio_addr_ = 0; + uint64_t scratch_mem_addr_ = 0; + uint64_t weight_stage_addr_ = 0x20000000; + uint64_t cache_line_size_ = 64; + std::string trace_file_; + std::string cuda_executable_; + bool replay_each_cmd_ = false; bool verbose_ = false; // Per-request bookkeeping: tag + which link it went out on. @@ -157,13 +154,8 @@ class BalarRingBridge : public InterceptionAgentAPI std::pair> requests_; // Staging: one or more (addr, bytes) segments written before the doorbell. - // Segment 0 is always the encoded command packet at scratchMemAddr_; for an - // H2D memcpy a second segment holds the weight payload at weightStageAddr_. struct StageSegment { uint64_t addr; std::vector bytes; }; std::vector stage_segments_; - // Filled by the trace parser for an H2D memcpy: the weight payload to stage at - // weightStageAddr_ (a dedicated, separable region) rather than intermingled - // with the command packet. Consumed by beginPacketIssue. std::vector pending_weight_payload_; size_t seg_index_ = 0; // which segment we're writing size_t seg_offset_ = 0; // byte offset within the current segment @@ -187,7 +179,9 @@ class BalarRingBridge : public InterceptionAgentAPI // State for the single in-flight D2H memcpy. Small, non-SST copies return a // directly populated host buffer. SST-memory copies instead require an // asynchronous StandardMem readback after balar's DMA completion. - uint8_t* pending_d2h_host_buf_ = nullptr; + // Backing storage for small, non-SST D2H copies. The packet carries a + // pointer into this vector, which remains stable until the call completes. + std::vector pending_d2h_host_buf_; uint64_t pending_d2h_sst_addr_ = 0; size_t pending_d2h_bytes_ = 0; size_t pending_d2h_read_bytes_ = 0; @@ -195,8 +189,7 @@ class BalarRingBridge : public InterceptionAgentAPI size_t pending_d2h_read_chunk_ = 0; bool pending_d2h_is_sst_ = false; - class CudaAPITraceParser; - CudaAPITraceParser* trace_parser_ = nullptr; + BalarTraceParser* trace_parser_ = nullptr; // Handler adapters for StandardMem's double-dispatch RequestHandler. class CacheHandlers; diff --git a/src/sst/elements/carcosa/components/balarTraceParser.cc b/src/sst/elements/carcosa/components/balarTraceParser.cc new file mode 100644 index 0000000000..6c7a357d19 --- /dev/null +++ b/src/sst/elements/carcosa/components/balarTraceParser.cc @@ -0,0 +1,365 @@ +// Copyright 2009-2026 NTESS. Under the terms +// of Contract DE-NA0003525 with NTESS, the U.S. +// Government retains certain rights in this software. + +#include "sst_config.h" + +#ifdef HAVE_BALAR_BRIDGE + +#include "sst/elements/carcosa/components/balarTraceParser.h" +#include "sst/elements/carcosa/components/configParse.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace SST; +using namespace SST::BalarComponent; +using namespace SST::Carcosa; + +namespace { + +std::string trim(const std::string& value) +{ + size_t first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) return {}; + size_t last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); +} + +std::vector split(const std::string& value, char delimiter) +{ + std::vector result; + std::stringstream stream(value); + std::string part; + while (std::getline(stream, part, delimiter)) result.push_back(trim(part)); + return result; +} + +std::map parseParams(const std::string& value) +{ + std::map result; + for (const auto& part : split(value, ',')) { + size_t delimiter = part.find(':'); + if (delimiter != std::string::npos) { + result[trim(part.substr(0, delimiter))] = trim(part.substr(delimiter + 1)); + } + } + return result; +} + +const std::string& requireParam(const std::map& params, + const std::string& key, Output* out, + size_t line_number, bool allow_empty = false) +{ + auto found = params.find(key); + if (found == params.end() || (!allow_empty && found->second.empty())) { + out->fatal(CALL_INFO, -1, + "Balar trace line %zu: missing parameter '%s'\n", + line_number, key.c_str()); + } + return found->second; +} + +template +T parseInteger(const std::string& value, const char* field, Output* out, + size_t line_number) +{ + try { + if (value.empty() || value.front() == '-') throw std::invalid_argument(value); + size_t parsed = 0; + unsigned long long raw = std::stoull(value, &parsed, 0); + if (parsed != value.size() || + raw > static_cast(std::numeric_limits::max())) { + out->fatal(CALL_INFO, -1, + "Balar trace line %zu: invalid %s '%s'\n", + line_number, field, value.c_str()); + } + return static_cast(raw); + } catch (...) { + out->fatal(CALL_INFO, -1, + "Balar trace line %zu: invalid %s '%s'\n", + line_number, field, value.c_str()); + } + return {}; +} + +int parseSignedInteger(const std::string& value, const char* field, Output* out, + size_t line_number) +{ + try { + size_t parsed = 0; + long long raw = std::stoll(value, &parsed, 0); + if (parsed == value.size() && raw >= INT_MIN && raw <= INT_MAX) { + return static_cast(raw); + } + } catch (...) { + } + out->fatal(CALL_INFO, -1, "Balar trace line %zu: invalid %s '%s'\n", + line_number, field, value.c_str()); + return 0; +} + +} // namespace + +BalarTraceParser::BalarTraceParser(Output* out, std::string trace_file, + std::string cuda_executable) + : out_(out), trace_file_(std::move(trace_file)), + cuda_executable_(std::move(cuda_executable)) +{ + size_t separator = trace_file_.find_last_of('/'); + trace_base_path_ = separator == std::string::npos + ? "./" : trace_file_.substr(0, separator + 1); + rewind(); +} + +void BalarTraceParser::rewind() +{ + if (trace_stream_.is_open()) trace_stream_.close(); + trace_stream_.clear(); + trace_stream_.open(trace_file_); + if (!trace_stream_.is_open()) { + out_->fatal(CALL_INFO, -1, "Balar trace file '%s' does not exist\n", + trace_file_.c_str()); + } + line_number_ = 0; + std::queue empty; + queued_packets_.swap(empty); + if (!fatbin_registered_) { + BalarCudaCallPacket_t packet{}; + packet.cuda_call_id = CUDA_REG_FAT_BINARY; + packet.isSSTmem = false; + strncpy(packet.register_fatbin.file_name, cuda_executable_.c_str(), + BALAR_CUDA_MAX_FILE_NAME - 1); + packet.register_fatbin.file_name[BALAR_CUDA_MAX_FILE_NAME - 1] = '\0'; + queued_packets_.push(packet); + } +} + +bool BalarTraceParser::next(BalarTracePacket& result) +{ + result = {}; + if (!queued_packets_.empty()) { + result.packet = queued_packets_.front(); + queued_packets_.pop(); + return true; + } + + std::string line; + while (std::getline(trace_stream_, line)) { + ++line_number_; + line = trim(line); + if (line.empty() || line.front() == '#') continue; + if (parseLine(line, result)) return true; + } + if (!trace_stream_.eof()) { + out_->fatal(CALL_INFO, -1, "Balar trace read failed near line %zu\n", + line_number_ + 1); + } + return false; +} + +bool BalarTraceParser::parseLine(const std::string& line, + BalarTracePacket& result) +{ + size_t delimiter = line.find(':'); + if (delimiter == std::string::npos) { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: expected ': '\n", + line_number_); + } + + std::string call = trim(line.substr(0, delimiter)); + auto params = parseParams(line.substr(delimiter + 1)); + auto& packet = result.packet; + packet = {}; + packet.isSSTmem = false; + + if (call.find("memalloc") != std::string::npos) { + packet.cuda_call_id = CUDA_MALLOC; + const auto& name = requireParam(params, "dptr", out_, line_number_); + auto slot = device_ptrs_.emplace(name, CUdeviceptr{}).first; + packet.cuda_malloc.devPtr = reinterpret_cast(&slot->second); + packet.cuda_malloc.size = parseInteger( + requireParam(params, "size", out_, line_number_), "size", out_, line_number_); + return true; + } + + bool h2d = call.find("memcpyH2D") != std::string::npos; + bool d2h = call.find("memcpyD2H") != std::string::npos; + if (h2d || d2h) { + packet.cuda_call_id = CUDA_MEMCPY; + const auto& name = requireParam(params, "device_ptr", out_, line_number_); + auto device = device_ptrs_.find(name); + if (device == device_ptrs_.end()) { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: unknown device pointer '%s'\n", + line_number_, name.c_str()); + } + size_t size = parseInteger( + requireParam(params, "size", out_, line_number_), "size", out_, line_number_); + packet.cuda_memcpy.count = size; + packet.cuda_memcpy.payload = 0; + if (h2d) { + result.is_h2d = true; + packet.cuda_memcpy.kind = cudaMemcpyHostToDevice; + packet.cuda_memcpy.dst = device->second; + std::string path = trace_base_path_ + + requireParam(params, "data_file", out_, line_number_); + std::ifstream data(path, std::ios::binary); + if (!data.is_open()) { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: data file '%s' does not exist\n", + line_number_, path.c_str()); + } + result.h2d_payload.resize(size); + if (size > 0) { + data.read(reinterpret_cast(result.h2d_payload.data()), size); + if (data.gcount() != static_cast(size)) { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: data file '%s' is shorter than %zu bytes\n", + line_number_, path.c_str(), size); + } + } + } else { + packet.cuda_memcpy.kind = cudaMemcpyDeviceToHost; + packet.cuda_memcpy.src = device->second; + result.is_d2h = true; + result.d2h_bytes = size; + } + return true; + } + + if (call.find("kernel launch") != std::string::npos) { + const auto& function_name = requireParam(params, "name", out_, line_number_); + const auto& ptx_name = requireParam(params, "ptx_name", out_, line_number_); + + BalarCudaCallPacket_t configure{}, argument{}, launch{}, registration{}; + configure.cuda_call_id = CUDA_CONFIG_CALL; + argument.cuda_call_id = CUDA_SET_ARG; + launch.cuda_call_id = CUDA_LAUNCH; + configure.configure_call.gdx = parseInteger(requireParam(params, "gdx", out_, line_number_), "gdx", out_, line_number_); + configure.configure_call.gdy = parseInteger(requireParam(params, "gdy", out_, line_number_), "gdy", out_, line_number_); + configure.configure_call.gdz = parseInteger(requireParam(params, "gdz", out_, line_number_), "gdz", out_, line_number_); + configure.configure_call.bdx = parseInteger(requireParam(params, "bdx", out_, line_number_), "bdx", out_, line_number_); + configure.configure_call.bdy = parseInteger(requireParam(params, "bdy", out_, line_number_), "bdy", out_, line_number_); + configure.configure_call.bdz = parseInteger(requireParam(params, "bdz", out_, line_number_), "bdz", out_, line_number_); + configure.configure_call.sharedMem = parseInteger(requireParam(params, "sharedBytes", out_, line_number_), "sharedBytes", out_, line_number_); + configure.configure_call.stream = nullptr; + queued_packets_.push(configure); + + auto function = functions_.find(function_name); + if (function == functions_.end()) { + uint64_t id = functions_.size(); + function = functions_.emplace(function_name, id).first; + registration.cuda_call_id = CUDA_REG_FUNCTION; + registration.register_function.fatCubinHandle = fatbin_handle_; + registration.register_function.hostFun = id; + strncpy(registration.register_function.deviceFun, ptx_name.c_str(), + BALAR_CUDA_MAX_KERNEL_NAME - 1); + registration.register_function.deviceFun[BALAR_CUDA_MAX_KERNEL_NAME - 1] = '\0'; + packet = registration; + } else { + packet = queued_packets_.front(); + queued_packets_.pop(); + } + + size_t offset = 0; + std::string arguments = requireParam(params, "args", out_, line_number_, true); + while (!arguments.empty()) { + size_t value_end = arguments.find('/'); + if (value_end == std::string::npos) { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: malformed kernel arguments\n", + line_number_); + } + std::string value = arguments.substr(0, value_end); + arguments.erase(0, value_end + 1); + size_t size_end = arguments.find('/'); + if (size_end == std::string::npos) { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: malformed kernel argument size\n", + line_number_); + } + size_t argument_size = parseInteger( + arguments.substr(0, size_end), "argument size", out_, line_number_); + arguments.erase(0, size_end + 1); + if (argument_size == 0 || argument_size > sizeof(argument.setup_argument.value)) { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: unsupported argument size %zu\n", + line_number_, argument_size); + } + offset = (offset + argument_size - 1) / argument_size * argument_size; + argument.setup_argument.size = argument_size; + argument.setup_argument.offset = offset; + argument.setup_argument.arg = 0; + memset(argument.setup_argument.value, 0, + sizeof(argument.setup_argument.value)); + offset += argument_size; + + if (value.find("dptr") != std::string::npos) { + auto device = device_ptrs_.find(value); + if (device == device_ptrs_.end()) { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: unknown argument pointer '%s'\n", + line_number_, value.c_str()); + } + argument.setup_argument.arg = device->second; + } else if (value.find('.') != std::string::npos) { + double parsed = 0.0; + if (!ConfigParse::parseDouble(value, parsed)) { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: invalid floating argument '%s'\n", + line_number_, value.c_str()); + } + if (argument_size == sizeof(double)) { + memcpy(argument.setup_argument.value, &parsed, argument_size); + } else if (argument_size == sizeof(float)) { + float narrowed = static_cast(parsed); + memcpy(argument.setup_argument.value, &narrowed, argument_size); + } else { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: unsupported floating argument size %zu\n", + line_number_, argument_size); + } + } else { + int parsed = parseSignedInteger(value, "integer argument", out_, line_number_); + memcpy(argument.setup_argument.value, &parsed, + std::min(argument_size, sizeof(parsed))); + } + queued_packets_.push(argument); + } + launch.cuda_launch.func = function->second; + queued_packets_.push(launch); + return true; + } + + if (call.find("free") != std::string::npos) { + packet.cuda_call_id = CUDA_FREE; + const auto& name = requireParam(params, "dptr", out_, line_number_); + auto device = device_ptrs_.find(name); + if (device == device_ptrs_.end()) { + out_->fatal(CALL_INFO, -1, + "Balar trace line %zu: unknown device pointer '%s'\n", + line_number_, name.c_str()); + } + packet.cuda_free.devPtr = reinterpret_cast(device->second); + return true; + } + + out_->fatal(CALL_INFO, -1, "Balar trace line %zu: unsupported call '%s'\n", + line_number_, call.c_str()); + return false; +} + +void BalarTraceParser::setFatbinHandle(uint64_t handle) +{ + fatbin_handle_ = handle; + fatbin_registered_ = true; +} + +#endif diff --git a/src/sst/elements/carcosa/components/balarTraceParser.h b/src/sst/elements/carcosa/components/balarTraceParser.h new file mode 100644 index 0000000000..5eb4882a1b --- /dev/null +++ b/src/sst/elements/carcosa/components/balarTraceParser.h @@ -0,0 +1,61 @@ +// Copyright 2009-2026 NTESS. Under the terms +// of Contract DE-NA0003525 with NTESS, the U.S. +// Government retains certain rights in this software. + +#ifndef CARCOSA_BALAR_TRACE_PARSER_H +#define CARCOSA_BALAR_TRACE_PARSER_H + +#ifdef HAVE_BALAR_BRIDGE + +#include +#include + +#include "cuda.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace SST::Carcosa { + +struct BalarTracePacket { + SST::BalarComponent::BalarCudaCallPacket_t packet{}; + std::vector h2d_payload; + size_t d2h_bytes = 0; + bool is_h2d = false; + bool is_d2h = false; +}; + +class BalarTraceParser { +public: + BalarTraceParser(SST::Output* out, std::string trace_file, + std::string cuda_executable); + + void rewind(); + bool next(BalarTracePacket& result); + void setFatbinHandle(uint64_t handle); + +private: + bool parseLine(const std::string& line, BalarTracePacket& result); + + SST::Output* out_; + std::string trace_file_; + std::string trace_base_path_; + std::string cuda_executable_; + std::ifstream trace_stream_; + std::queue queued_packets_; + std::map device_ptrs_; + std::map functions_; + uint64_t fatbin_handle_ = 0; + size_t line_number_ = 0; + bool fatbin_registered_ = false; +}; + +} // namespace SST::Carcosa + +#endif +#endif diff --git a/src/sst/elements/carcosa/components/componentTestBounds.h b/src/sst/elements/carcosa/components/componentTestBounds.h new file mode 100644 index 0000000000..0b0fa46958 --- /dev/null +++ b/src/sst/elements/carcosa/components/componentTestBounds.h @@ -0,0 +1,64 @@ +// Copyright 2009-2026 NTESS. Under the terms +// of Contract DE-NA0003525 with NTESS, the U.S. +// Government retains certain rights in this software. + +#ifndef SST_ELEMENTS_CARCOSA_COMPONENT_TEST_BOUNDS_H +#define SST_ELEMENTS_CARCOSA_COMPONENT_TEST_BOUNDS_H + +#include + +#include +#include +#include +#include +#include + +namespace SST::Carcosa { + +class ComponentTestBounds { +public: + void add(std::string name, int64_t minimum, int64_t maximum) { + bounds_.push_back({std::move(name), minimum, maximum, 0}); + } + + void addExact(std::string name, int64_t expected) { + add(std::move(name), expected, expected); + } + + void set(const std::string& name, uint64_t value) { + for (auto& bound : bounds_) { + if (bound.name == name) { + bound.value = value; + return; + } + } + } + + bool check(SST::Output& out, const std::string& component) const { + bool valid = true; + for (const auto& bound : bounds_) { + if ((bound.minimum >= 0 && bound.value < static_cast(bound.minimum)) || + (bound.maximum >= 0 && bound.value > static_cast(bound.maximum))) { + out.output("%s: FAIL %s=%" PRIu64 " outside [%" PRId64 ",%" PRId64 "].\n", + component.c_str(), bound.name.c_str(), bound.value, + bound.minimum, bound.maximum); + valid = false; + } + } + return valid; + } + +private: + struct Bound { + std::string name; + int64_t minimum; + int64_t maximum; + uint64_t value; + }; + + std::vector bounds_; +}; + +} // namespace SST::Carcosa + +#endif diff --git a/src/sst/elements/carcosa/components/configParse.h b/src/sst/elements/carcosa/components/configParse.h new file mode 100644 index 0000000000..7d1e08c53a --- /dev/null +++ b/src/sst/elements/carcosa/components/configParse.h @@ -0,0 +1,51 @@ +// Copyright 2009-2026 NTESS. Under the terms +// of Contract DE-NA0003525 with NTESS, the U.S. +// Government retains certain rights in this software. + +#ifndef SST_ELEMENTS_CARCOSA_CONFIG_PARSE_H +#define SST_ELEMENTS_CARCOSA_CONFIG_PARSE_H + +#include +#include +#include + +namespace SST::Carcosa::ConfigParse { + +inline bool parseDouble(const std::string& text, double& value) { + try { + size_t parsed = 0; + value = std::stod(text, &parsed); + return parsed == text.size() && std::isfinite(value); + } catch (...) { + return false; + } +} + +inline bool parseUint64(const std::string& text, uint64_t& value) { + if (text.empty() || text.front() == '-') return false; + try { + size_t parsed = 0; + value = std::stoull(text, &parsed, 0); + return parsed == text.size(); + } catch (...) { + return false; + } +} + +inline bool parseInt(const std::string& text, int& value) { + try { + size_t parsed = 0; + value = std::stoi(text, &parsed, 0); + return parsed == text.size(); + } catch (...) { + return false; + } +} + +inline bool isProbability(double value) { + return value >= 0.0 && value <= 1.0; +} + +} // namespace SST::Carcosa::ConfigParse + +#endif diff --git a/src/sst/elements/carcosa/components/criticalActionWatcher.cc b/src/sst/elements/carcosa/components/criticalActionWatcher.cc index 0820293d6d..d8301aaf03 100644 --- a/src/sst/elements/carcosa/components/criticalActionWatcher.cc +++ b/src/sst/elements/carcosa/components/criticalActionWatcher.cc @@ -24,7 +24,7 @@ CriticalActionWatcher::CriticalActionWatcher(ComponentId_t id, Params& params) state_key_ = params.find("state_key", ""); critical_region_ = params.find("critical_region", "action_queue"); critical_len_ = params.find("critical_len", 64); - applyOnResponsesOnly_ = + apply_on_responses_only_ = params.find("apply_on_responses_only", true); actuation_kernel_name_ = params.find("actuation_kernel", ""); golden_path_ = params.find("golden_log", ""); @@ -180,8 +180,7 @@ void CriticalActionWatcher::mergePayloadIntoSnapshot(uint64_t rel_off, // the pipeline driver closes its FrameRecord on the ACTUATE status write, // before this watcher necessarily sees traffic from the next kernel. if (observed_this_frame_ && state_ptr_) { - state_ptr_->watcherActionChecksum = hashSnapshot(); - state_ptr_->watcherActionChecksumValid = true; + state_ptr_->publishWatcherChecksum(hashSnapshot()); } } @@ -321,16 +320,15 @@ void CriticalActionWatcher::finalizeActuateFrame() { corrupted = checksum != golden; } } - state_ptr_->watcherCriticalCorrupted = corrupted; + state_ptr_->recordWatcherCorruption(corrupted); if (corrupted) { - ++state_ptr_->framesCriticalRegionCorrupted; if (stat_frames_critical_corrupted_) stat_frames_critical_corrupted_->addData(1); } // Frame-close status write (no intervening critical traffic) already // consumed this fold; retire the flag so the next frame cannot inherit a // stale checksum if nothing downstream reads it. - state_ptr_->watcherActionChecksumValid = false; + state_ptr_->retireWatcherChecksum(); snapshot_.assign(crit_len_, 0); observed_this_frame_ = false; } @@ -353,7 +351,7 @@ void CriticalActionWatcher::observeEvent(MemEvent* mev) { last_kernel_id_ = state_ptr_->currentKernel; saw_kernel_ = true; - if ((!applyOnResponsesOnly_ || isResponseCmd(mev)) + if ((!apply_on_responses_only_ || isResponseCmd(mev)) && k == actuation_kernel_name_) { uint64_t rel = 0, poff = 0, olen = 0; if (eventOverlapsCritical(mev, rel, poff, olen)) { diff --git a/src/sst/elements/carcosa/components/criticalActionWatcher.h b/src/sst/elements/carcosa/components/criticalActionWatcher.h index 2f802e10f1..276591b6d0 100644 --- a/src/sst/elements/carcosa/components/criticalActionWatcher.h +++ b/src/sst/elements/carcosa/components/criticalActionWatcher.h @@ -85,7 +85,7 @@ class CriticalActionWatcher : public SST::Component { std::string critical_region_; uint64_t critical_len_ = 64; - bool applyOnResponsesOnly_ = true; + bool apply_on_responses_only_ = true; PipelineStateBase* state_ptr_ = nullptr; std::string actuation_kernel_name_; diff --git a/src/sst/elements/carcosa/components/eccGuard.cc b/src/sst/elements/carcosa/components/eccGuard.cc index 06f1e52854..473853c9df 100644 --- a/src/sst/elements/carcosa/components/eccGuard.cc +++ b/src/sst/elements/carcosa/components/eccGuard.cc @@ -57,33 +57,32 @@ bool parseModeWeightsCsv(const std::string& csv, double out[kModeCount]) { int idx = 0; while (std::getline(ss, tok, ':')) { if (idx >= kModeCount) return false; - try { - out[idx++] = std::stod(tok); - } catch (...) { - return false; - } + double value = 0.0; + if (!ConfigParse::parseDouble(tok, value) || value < 0.0) return false; + out[idx++] = value; } return idx == kModeCount; } -EccGuard::FaultModel parseFaultModel(const std::string& s) { - if (s == "jedec_mix" || s == "jedec" || s == "JEDEC_MIX") return EccGuard::FaultModel::JedecMix; - if (s == "campaign" || s == "CAMPAIGN") return EccGuard::FaultModel::Campaign; - if (s == "resident" || s == "RESIDENT") return EccGuard::FaultModel::Resident; - return EccGuard::FaultModel::Poisson; +bool parseFaultModel(const std::string& s, EccGuard::FaultModel& model) { + if (s == "poisson" || s == "POISSON") model = EccGuard::FaultModel::Poisson; + else if (s == "jedec_mix" || s == "jedec" || s == "JEDEC_MIX") model = EccGuard::FaultModel::JedecMix; + else if (s == "campaign" || s == "CAMPAIGN") model = EccGuard::FaultModel::Campaign; + else if (s == "resident" || s == "RESIDENT") model = EccGuard::FaultModel::Resident; + else return false; + return true; } -// Parse campaign mode name into FaultMode; unknown -> SingleRow (same -// defaulting as jedec_mix). Crash only on empty/garbage upstream. -EccGuard::FaultMode parseCampaignMode(const std::string& s) { - if (s == "cell" || s == "single_cell" || s == "SingleCell") return EccGuard::FaultMode::SingleCell; - if (s == "word" || s == "single_word" || s == "SingleWord") return EccGuard::FaultMode::SingleWord; - if (s == "row" || s == "single_row" || s == "SingleRow") return EccGuard::FaultMode::SingleRow; - if (s == "column" || s == "single_column" || s == "SingleColumn") return EccGuard::FaultMode::SingleColumn; - if (s == "bank" || s == "single_bank" || s == "SingleBank") return EccGuard::FaultMode::SingleBank; - if (s == "device" || s == "single_device" || s == "SingleDevice") return EccGuard::FaultMode::SingleDevice; - if (s == "multi_chip" || s == "MULTI_CHIP") return EccGuard::FaultMode::SingleWord; - return EccGuard::FaultMode::SingleRow; +bool parseFaultMode(const std::string& s, EccGuard::FaultMode& mode) { + if (s == "cell" || s == "single_cell" || s == "SingleCell") mode = EccGuard::FaultMode::SingleCell; + else if (s == "word" || s == "single_word" || s == "SingleWord") mode = EccGuard::FaultMode::SingleWord; + else if (s == "row" || s == "single_row" || s == "SingleRow") mode = EccGuard::FaultMode::SingleRow; + else if (s == "column" || s == "single_column" || s == "SingleColumn") mode = EccGuard::FaultMode::SingleColumn; + else if (s == "bank" || s == "single_bank" || s == "SingleBank") mode = EccGuard::FaultMode::SingleBank; + else if (s == "device" || s == "single_device" || s == "SingleDevice") mode = EccGuard::FaultMode::SingleDevice; + else if (s == "multi_chip" || s == "MULTI_CHIP") mode = EccGuard::FaultMode::SingleWord; + else return false; + return true; } bool isMultiChipCampaignAlias(const std::string& s) { @@ -100,62 +99,11 @@ std::string resolveCampaignKernel(const std::string& raw) { return raw; } -EccGuard::PayloadDtype parseDtype(const std::string& s) { - if (s == "bf16" || s == "BF16") return EccGuard::PayloadDtype::Bf16; - if (s == "fp8" || s == "FP8") return EccGuard::PayloadDtype::Fp8; - if (s == "int8" || s == "INT8") return EccGuard::PayloadDtype::Int8; - return EccGuard::PayloadDtype::Bytes; -} - -EccGuard::DueAction parseDueAction(const std::string& s) { - if (s == "drop_frame" || s == "drop" || s == "DROP_FRAME") return EccGuard::DueAction::DropFrame; - return EccGuard::DueAction::LatencyOnly; -} - -const char* dtypeName(EccGuard::PayloadDtype d) { - switch (d) { - case EccGuard::PayloadDtype::Bytes: return "bytes"; - case EccGuard::PayloadDtype::Bf16: return "bf16"; - case EccGuard::PayloadDtype::Fp8: return "fp8"; - case EccGuard::PayloadDtype::Int8: return "int8"; - } - return "unknown"; -} - -// Returns true if the given bit (0-indexed inside its element) is "high blast" -// for the dtype (sign bit or top exponent bit). Bit 0 is LSB of the element. -bool isHighBlastBit(EccGuard::PayloadDtype dtype, unsigned bit_in_element) { - switch (dtype) { - case EccGuard::PayloadDtype::Bf16: { - // bf16: [15] sign, [14:7] exponent, [6:0] mantissa. - if (bit_in_element == 15) return true; // sign - if (bit_in_element >= 13 && bit_in_element <= 14) return true; // top 2 exp bits - return false; - } - case EccGuard::PayloadDtype::Fp8: { - // E4M3-style fp8: [7] sign, [6:3] exponent, [2:0] mantissa. - if (bit_in_element == 7) return true; - if (bit_in_element == 6) return true; - return false; - } - case EccGuard::PayloadDtype::Int8: { - // Two's-complement int8: bit 7 sign. - return bit_in_element == 7; - } - case EccGuard::PayloadDtype::Bytes: - default: - return false; - } -} - -unsigned dtypeBytes(EccGuard::PayloadDtype d) { - switch (d) { - case EccGuard::PayloadDtype::Bf16: return 2; - case EccGuard::PayloadDtype::Fp8: - case EccGuard::PayloadDtype::Int8: return 1; - case EccGuard::PayloadDtype::Bytes: - default: return 1; - } +bool parseDueAction(const std::string& s, EccGuard::DueAction& action) { + if (s == "latency_only" || s == "LATENCY_ONLY") action = EccGuard::DueAction::LatencyOnly; + else if (s == "drop_frame" || s == "drop" || s == "DROP_FRAME") action = EccGuard::DueAction::DropFrame; + else return false; + return true; } } // namespace @@ -165,21 +113,22 @@ EccGuard::EccGuard(ComponentId_t id, Params& params) : Component(id) { out_ = new Output("", 1, 0, Output::STDOUT); verbose_ = params.find("verbose", false); - test_total_min_ = params.find("test_total_min", -1); - test_total_max_ = params.find("test_total_max", -1); - test_clean_min_ = params.find("test_clean_min", -1); - test_clean_max_ = params.find("test_clean_max", -1); - test_correctable_min_ = params.find("test_correctable_min", -1); - test_correctable_max_ = params.find("test_correctable_max", -1); - test_due_min_ = params.find("test_due_min", -1); - test_due_max_ = params.find("test_due_max", -1); - test_escape_min_ = params.find("test_escape_min", -1); - test_escape_max_ = params.find("test_escape_max", -1); - test_resident_born_min_ = params.find("test_resident_born_min", -1); - test_resident_born_max_ = params.find("test_resident_born_max", -1); + test_bounds_.add("total", params.find("test_total_min", -1), + params.find("test_total_max", -1)); + test_bounds_.add("clean", params.find("test_clean_min", -1), + params.find("test_clean_max", -1)); + test_bounds_.add("correctable", params.find("test_correctable_min", -1), + params.find("test_correctable_max", -1)); + test_bounds_.add("due", params.find("test_due_min", -1), + params.find("test_due_max", -1)); + test_bounds_.add("escape", params.find("test_escape_min", -1), + params.find("test_escape_max", -1)); + test_bounds_.add("resident_born", + params.find("test_resident_born_min", -1), + params.find("test_resident_born_max", -1)); state_key_ = params.find("state_key", ""); - applyOnResponsesOnly_ = params.find("apply_on_responses_only", true); + apply_on_responses_only_ = params.find("apply_on_responses_only", true); EccPolicyEntry uniform; uniform.inherits_uniform = false; @@ -204,15 +153,29 @@ EccGuard::EccGuard(ComponentId_t id, Params& params) : Component(id) { std::vector errors; int parsed = policy_.parseCsv(ks_csv, errors); for (auto& e : errors) out_->output("EccGuard: %s\n", e.c_str()); + if (!errors.empty()) { + out_->fatal(CALL_INFO, -1, "EccGuard: invalid kernel_policy.\n"); + } if (verbose_) { out_->output("EccGuard: parsed %d kernel/region policy override(s).\n", parsed); } } - // Phase 2: fault model + dtype-aware flips + DUE action. - fault_model_ = parseFaultModel(params.find("fault_model", "poisson")); - payload_dtype_ = parseDtype (params.find("payload_dtype", "bytes")); - due_action_ = parseDueAction (params.find("due_action", "latency_only")); + std::string fault_model = params.find("fault_model", "poisson"); + std::string payload_dtype = params.find("payload_dtype", "bytes"); + std::string due_action = params.find("due_action", "latency_only"); + if (!parseFaultModel(fault_model, fault_model_)) { + out_->fatal(CALL_INFO, -1, "EccGuard: unknown fault_model '%s'.\n", + fault_model.c_str()); + } + if (!EccPayloadCorruptor::parseDtype(payload_dtype, payload_dtype_)) { + out_->fatal(CALL_INFO, -1, "EccGuard: unknown payload_dtype '%s'.\n", + payload_dtype.c_str()); + } + if (!parseDueAction(due_action, due_action_)) { + out_->fatal(CALL_INFO, -1, "EccGuard: unknown due_action '%s'.\n", + due_action.c_str()); + } std::string mw_csv = params.find("fault_mode_weights", ""); if (!mw_csv.empty()) { @@ -228,7 +191,6 @@ EccGuard::EccGuard(ComponentId_t id, Params& params) : Component(id) { { double sum = 0.0; for (int i = 0; i < kModeCount; ++i) { - if (mode_weights_[i] < 0.0) mode_weights_[i] = 0.0; sum += mode_weights_[i]; } if (sum <= 0.0) { @@ -239,6 +201,11 @@ EccGuard::EccGuard(ComponentId_t id, Params& params) : Component(id) { } fault_event_rate_ = params.find("fault_event_rate", 0.0); + if (!ConfigParse::isProbability(fault_event_rate_)) { + out_->fatal(CALL_INFO, -1, + "EccGuard: fault_event_rate=%g must be in [0,1].\n", + fault_event_rate_); + } // Campaign-mode parameters. These are inert unless fault_model_ == Campaign. { @@ -253,6 +220,11 @@ EccGuard::EccGuard(ComponentId_t id, Params& params) : Component(id) { } campaign_event_budget_ = params.find("campaign_event_budget", 0); campaign_event_rate_ = params.find ("campaign_event_rate", 0.0); + if (!ConfigParse::isProbability(campaign_event_rate_)) { + out_->fatal(CALL_INFO, -1, + "EccGuard: campaign_event_rate=%g must be in [0,1].\n", + campaign_event_rate_); + } campaign_max_per_kernel_entry_ = params.find("campaign_max_events_per_kernel_entry", 0); campaign_errors_fixed_ = params.find("campaign_errors_fixed", 0); @@ -260,7 +232,10 @@ EccGuard::EccGuard(ComponentId_t id, Params& params) : Component(id) { campaign_force_multi_chip_ = params.find("campaign_force_multi_chip", false) || isMultiChipCampaignAlias(raw_cmode); - campaign_mode_ = parseCampaignMode(raw_cmode); + if (!parseFaultMode(raw_cmode, campaign_mode_)) { + out_->fatal(CALL_INFO, -1, "EccGuard: unknown campaign_mode '%s'.\n", + raw_cmode.c_str()); + } addr_filter_region_ = params.find("addr_filter_region", ""); addr_filter_len_ = params.find("addr_filter_len", 0); inject_addr_start_ = params.find("inject_addr_start", 0); @@ -283,6 +258,10 @@ EccGuard::EccGuard(ComponentId_t id, Params& params) : Component(id) { double fit_rate = params.find("fit_per_mbit_per_hour", 0.0); double dram_mb = params.find("dram_capacity_mb", 1024.0); double per_event_ns = params.find("sim_time_per_event_ns", 100.0); + if (fit_rate < 0.0 || dram_mb <= 0.0 || per_event_ns <= 0.0) { + out_->fatal(CALL_INFO, -1, + "EccGuard: FIT must be nonnegative; capacity and event time must be positive.\n"); + } if (fit_rate > 0.0 && fault_event_rate_ <= 0.0) { // FIT = failures per 1e9 device-hours, per Mbit. Convert to per-event prob. // dram_capacity_mb is megaBYTES; FIT is per megaBIT, hence the x8. @@ -293,6 +272,11 @@ EccGuard::EccGuard(ComponentId_t id, Params& params) : Component(id) { fit_rate, dram_mb, per_event_ns, fault_event_rate_); } } + if (!ConfigParse::isProbability(fault_event_rate_)) { + out_->fatal(CALL_INFO, -1, + "EccGuard: derived fault_event_rate=%g exceeds [0,1].\n", + fault_event_rate_); + } // Resident fault-map parameters (fault_model='resident'). Parsed // unconditionally so sst-info documents them; inert unless the model is @@ -304,16 +288,30 @@ EccGuard::EccGuard(ComponentId_t id, Params& params) : Component(id) { double rate_per_ms = params.find("resident_fault_rate_per_ms", 0.0); resident_time_accel_ = params.find("resident_time_acceleration", 1.0); double scrub_us = params.find("resident_scrub_interval_us", 0.0); + if (rate_per_ms < 0.0 || resident_time_accel_ < 0.0 || scrub_us < 0.0) { + out_->fatal(CALL_INFO, -1, + "EccGuard: resident rates, acceleration, and scrub interval must be nonnegative.\n"); + } resident_scrub_interval_ns_ = static_cast(scrub_us * 1e3); resident_permanent_fraction_ = params.find("resident_permanent_fraction", 0.3); + if (!ConfigParse::isProbability(resident_permanent_fraction_)) { + out_->fatal(CALL_INFO, -1, + "EccGuard: resident_permanent_fraction=%g must be in [0,1].\n", + resident_permanent_fraction_); + } std::string rmode = params.find("resident_mode", "mix"); resident_mode_mix_ = (rmode.empty() || rmode == "mix" || rmode == "MIX"); - if (!resident_mode_mix_) resident_mode_fixed_ = parseCampaignMode(rmode); + if (!resident_mode_mix_ && !parseFaultMode(rmode, resident_mode_fixed_)) { + out_->fatal(CALL_INFO, -1, "EccGuard: unknown resident_mode '%s'.\n", + rmode.c_str()); + } resident_row_bytes_ = params.find("resident_row_bytes", 8192); resident_bank_rows_ = params.find("resident_bank_rows", 8); - if (resident_row_bytes_ < 64) resident_row_bytes_ = 64; - if (resident_bank_rows_ == 0) resident_bank_rows_ = 1; + if (resident_row_bytes_ < 64 || resident_bank_rows_ == 0) { + out_->fatal(CALL_INFO, -1, + "EccGuard: resident_row_bytes must be >=64 and resident_bank_rows must be >0.\n"); + } if (rate_per_ms > 0.0) { resident_rate_per_ns_ = rate_per_ms / 1e6; @@ -457,7 +455,7 @@ void EccGuard::setup() { ? "jedec_mix" : (fault_model_ == FaultModel::Campaign ? "campaign" : "poisson")), - dtypeName(payload_dtype_), + EccPayloadCorruptor::dtypeName(payload_dtype_), due_action_ == DueAction::DropFrame ? "drop_frame" : "latency_only", fault_event_rate_); } @@ -528,7 +526,7 @@ void EccGuard::finish() { out_->output("escape_high_blast,escape_low_blast,frames_aborted,payload_dtype,due_poisoned_bits\n"); out_->output("%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%s,%" PRIu64 "\n", escape_high_blast_total_, escape_low_blast_total_, - frames_aborted_total_, dtypeName(payload_dtype_), + frames_aborted_total_, EccPayloadCorruptor::dtypeName(payload_dtype_), due_poison_flips_total_); out_->output("=== End EccGuard %s Escape/Abort Summary ===\n\n", getName().c_str()); } @@ -550,25 +548,15 @@ void EccGuard::finish() { } const uint64_t total = totals.clean + totals.correctable + totals.due + totals.escape; - bool ok = true; - auto bound = [&](const char* name, uint64_t got, int64_t lo, int64_t hi) { - if ((lo >= 0 && got < static_cast(lo)) || - (hi >= 0 && got > static_cast(hi))) { - out_->output("EccGuard '%s': FAIL %s=%" PRIu64 - " outside [%" PRId64 ",%" PRId64 "].\n", - getName().c_str(), name, got, lo, hi); - ok = false; - } - }; - bound("total", total, test_total_min_, test_total_max_); - bound("clean", totals.clean, test_clean_min_, test_clean_max_); - bound("correctable", totals.correctable, - test_correctable_min_, test_correctable_max_); - bound("due", totals.due, test_due_min_, test_due_max_); - bound("escape", totals.escape, test_escape_min_, test_escape_max_); - bound("resident_born", resident_faults_born_total_, - test_resident_born_min_, test_resident_born_max_); - if (!ok) out_->fatal(CALL_INFO, -1, "EccGuard test expectations failed.\n"); + test_bounds_.set("total", total); + test_bounds_.set("clean", totals.clean); + test_bounds_.set("correctable", totals.correctable); + test_bounds_.set("due", totals.due); + test_bounds_.set("escape", totals.escape); + test_bounds_.set("resident_born", resident_faults_born_total_); + if (!test_bounds_.check(*out_, getName())) { + out_->fatal(CALL_INFO, -1, "EccGuard test expectations failed.\n"); + } } void EccGuard::resolveStateLazy() { @@ -620,7 +608,7 @@ bool EccGuard::resolveAddrFilterBounds(uint64_t& base_out, uint64_t& len_out) co bool EccGuard::shouldApplyPolicy(MemEvent* mev) { if (!mev) return false; resolveStateLazy(); - if (!applyOnResponsesOnly_) return true; + if (!apply_on_responses_only_) return true; if (mev->isResponse()) return true; if (fault_model_ != FaultModel::Campaign || addr_filter_region_.empty()) return false; @@ -656,7 +644,7 @@ void EccGuard::requestFrameAbort() { PipelineStateBase* s = PipelineStateRegistry::getMutable(state_key_); if (!s) return; - s->frameAbortRequested = true; + if (!s->requestFrameAbort()) return; ++frames_aborted_total_; if (stat_frames_aborted_) stat_frames_aborted_->addData(1); } @@ -670,8 +658,7 @@ void publishCumulative(const std::string& state_key, uint64_t escapes_inc, PipelineStateBase* s = PipelineStateRegistry::getMutable(state_key); if (!s) return; - s->eccCumulativeEscapes += escapes_inc; - s->eccCumulativeFlips += flips_inc; + s->addEccCounts(escapes_inc, flips_inc); } // Bump per-frame per-kernel escape counts (argmaxed at frame close). @@ -681,7 +668,7 @@ void publishPerFrameEscape(const std::string& state_key, PipelineStateBase* s = PipelineStateRegistry::getMutable(state_key); if (!s) return; - s->eccPerFrameEscapesByKernel[kernel_name] += 1; + s->addKernelEscape(kernel_name); } } // namespace @@ -811,6 +798,53 @@ void EccGuard::distributeErrorsToChips( } } +void EccGuard::placeFaultErrors(FaultDraw& draw, uint32_t payload_bytes, + EccScheme scheme) +{ + uint32_t word_count = numWords(payload_bytes, scheme); + draw.per_word_errors.assign(word_count, 0u); + bool chip_aware = scheme == EccScheme::CHIPKILL_x4; + if (chip_aware) draw.per_word_chip_errors.resize(word_count); + if (word_count == 0 || draw.num_errors == 0) return; + + auto setWord = [&](uint32_t word, unsigned errors) { + draw.per_word_errors[word] = errors; + if (chip_aware) { + distributeErrorsToChips(draw.per_word_chip_errors[word], errors, + scheme, draw.mode); + } + }; + + if (isCorrelatedMode(draw.mode)) { + std::uniform_int_distribution pick(0, word_count - 1); + uint32_t word = pick(stdRng_); + unsigned remaining = draw.num_errors; + unsigned capacity = bitsPerWord(payload_bytes, scheme); + for (uint32_t offset = 0; remaining > 0 && offset < word_count; ++offset) { + uint32_t target = (word + offset) % word_count; + unsigned placed = capacity == 0 + ? remaining : std::min(capacity, remaining); + setWord(target, placed); + remaining -= placed; + } + return; + } + + std::uniform_int_distribution pick(0, word_count - 1); + for (unsigned i = 0; i < draw.num_errors; ++i) { + ++draw.per_word_errors[pick(stdRng_)]; + } + if (chip_aware) { + for (uint32_t word = 0; word < word_count; ++word) { + if (draw.per_word_errors[word] > 0) { + distributeErrorsToChips(draw.per_word_chip_errors[word], + draw.per_word_errors[word], scheme, + draw.mode); + } + } + } +} + EccGuard::FaultDraw EccGuard::drawFaultPoisson(uint32_t payload_bytes, double ber, EccScheme scheme) { @@ -847,8 +881,6 @@ EccGuard::FaultDraw EccGuard::drawFaultJedecMix(uint32_t payload_bytes, FaultDraw d; if (payload_bytes == 0) return d; - uint32_t nwords = numWords(payload_bytes, scheme); - d.per_word_errors.assign(nwords, 0u); if (event_rate <= 0.0) return d; std::bernoulli_distribution gate(std::min(event_rate, 1.0)); if (!gate(stdRng_)) return d; @@ -874,51 +906,7 @@ EccGuard::FaultDraw EccGuard::drawFaultJedecMix(uint32_t payload_bytes, if (cap > 0 && errs > cap) errs = cap; d.num_errors = errs; - // Correlated/single-word modes deposit into one random word (physical - // clustering); SingleCell scatters across words bit-by-bit. - bool need_chips = (scheme == EccScheme::CHIPKILL_x4); - if (need_chips) d.per_word_chip_errors.resize(nwords); - if (nwords > 0) { - if (isCorrelatedMode(d.mode)) { - std::uniform_int_distribution wpick(0, nwords - 1); - uint32_t w = wpick(stdRng_); - unsigned word_cap = bitsPerWord(payload_bytes, scheme); - if (word_cap > 0 && errs > word_cap) { - d.per_word_errors[w] = word_cap; - if (need_chips) - distributeErrorsToChips(d.per_word_chip_errors[w], word_cap, scheme, d.mode); - unsigned remaining = errs - word_cap; - uint32_t offset = 1; - while (remaining > 0 && offset < nwords) { - uint32_t wn = (w + offset) % nwords; - unsigned put = std::min(word_cap, remaining); - d.per_word_errors[wn] = put; - if (need_chips) - distributeErrorsToChips(d.per_word_chip_errors[wn], put, scheme, d.mode); - remaining -= put; - ++offset; - } - } else { - d.per_word_errors[w] = errs; - if (need_chips) - distributeErrorsToChips(d.per_word_chip_errors[w], errs, scheme, d.mode); - } - } else { - // Uncorrelated (SingleCell, default): scatter bit-by-bit. - std::uniform_int_distribution wpick(0, nwords - 1); - for (unsigned i = 0; i < errs; ++i) { - uint32_t w = wpick(stdRng_); - d.per_word_errors[w] += 1; - } - if (need_chips) { - for (uint32_t w = 0; w < nwords; ++w) { - if (d.per_word_errors[w] > 0) - distributeErrorsToChips(d.per_word_chip_errors[w], - d.per_word_errors[w], scheme, d.mode); - } - } - } - } + placeFaultErrors(d, payload_bytes, scheme); ++per_mode_draws_[chosen]; if (chosen == static_cast(FaultMode::SingleRow) && stat_correlated_row_) stat_correlated_row_->addData(1); @@ -936,9 +924,6 @@ EccGuard::FaultDraw EccGuard::drawFaultCampaign(uint32_t payload_bytes, FaultDraw d; if (payload_bytes == 0) return d; - uint32_t nwords = numWords(payload_bytes, scheme); - d.per_word_errors.assign(nwords, 0u); - if (campaign_event_budget_ == 0) return d; if (campaign_events_fired_ >= campaign_event_budget_) return d; // Addr-filtered campaign: action_queue traffic is the temporal proxy. @@ -984,47 +969,7 @@ EccGuard::FaultDraw EccGuard::drawFaultCampaign(uint32_t payload_bytes, if (cap > 0 && errs > cap) errs = cap; d.num_errors = errs; - bool need_chips = (scheme == EccScheme::CHIPKILL_x4); - if (need_chips) d.per_word_chip_errors.resize(nwords); - if (nwords > 0) { - if (isCorrelatedMode(d.mode)) { - std::uniform_int_distribution wpick(0, nwords - 1); - uint32_t w = wpick(stdRng_); - unsigned word_cap = bitsPerWord(payload_bytes, scheme); - if (word_cap > 0 && errs > word_cap) { - d.per_word_errors[w] = word_cap; - if (need_chips) - distributeErrorsToChips(d.per_word_chip_errors[w], word_cap, scheme, d.mode); - unsigned remaining = errs - word_cap; - uint32_t offset = 1; - while (remaining > 0 && offset < nwords) { - uint32_t wn = (w + offset) % nwords; - unsigned put = std::min(word_cap, remaining); - d.per_word_errors[wn] = put; - if (need_chips) - distributeErrorsToChips(d.per_word_chip_errors[wn], put, scheme, d.mode); - remaining -= put; - ++offset; - } - } else { - d.per_word_errors[w] = errs; - if (need_chips) - distributeErrorsToChips(d.per_word_chip_errors[w], errs, scheme, d.mode); - } - } else { - std::uniform_int_distribution wpick(0, nwords - 1); - for (unsigned i = 0; i < errs; ++i) { - d.per_word_errors[wpick(stdRng_)] += 1; - } - if (need_chips) { - for (uint32_t w = 0; w < nwords; ++w) { - if (d.per_word_errors[w] > 0) - distributeErrorsToChips(d.per_word_chip_errors[w], - d.per_word_errors[w], scheme, d.mode); - } - } - } - } + placeFaultErrors(d, payload_bytes, scheme); ++campaign_events_fired_; ++campaign_events_this_entry_; @@ -1372,35 +1317,6 @@ EccGuard::FaultDraw EccGuard::drawFaultResident(MemEvent* mev, return d; } -unsigned EccGuard::flipExactBitsInWord(MemEvent* mev, uint32_t word_index, - EccScheme scheme, - const std::vector& exact_bits, - unsigned& high_flips, unsigned& low_flips) { - auto& payload = mev->getPayload(); - if (payload.empty() || exact_bits.empty()) return 0; - const uint32_t total_bits = static_cast(payload.size()) * 8; - const uint32_t wb = eccWordBytes(scheme); - const uint32_t start_bit = (wb == 0) ? 0 : word_index * wb * 8; - const uint32_t end_bit = (wb == 0) ? total_bits - : std::min(total_bits, start_bit + wb * 8); - unsigned elem_bytes = dtypeBytes(payload_dtype_); - if (elem_bytes == 0) elem_bytes = 1; - - unsigned flipped = 0; - for (uint32_t b : exact_bits) { - if (b < start_bit || b >= end_bit) continue; - const uint32_t byte = b / 8u, bit = b % 8u; - payload[byte] ^= static_cast(1u << bit); - bool hi = false; - if (payload_dtype_ != PayloadDtype::Bytes) { - hi = isHighBlastBit(payload_dtype_, (byte % elem_bytes) * 8u + bit); - } - if (hi) ++high_flips; else ++low_flips; - ++flipped; - } - return flipped; -} - void EccGuard::warnIfBerExceedsTightBound(double ber, const char* origin) { if (ber <= kEccBerTightUpperBound) return; // Memoize so repeated BER values don't spam the log. @@ -1508,13 +1424,15 @@ uint64_t EccGuard::applyPolicy(MemEvent* mev) { requestFrameAbort(); return; } - unsigned hi = 0, lo = 0, flips = 0; + unsigned flips = 0; for (uint32_t w : line.due_words) { - flips += draw.exact_bits.empty() - ? flipBitsInWord(mev, w, entry.scheme, - draw.per_word_errors[w], hi, lo) - : flipExactBitsInWord(mev, w, entry.scheme, - draw.exact_bits, hi, lo); + EccPayloadFlipCount count = draw.exact_bits.empty() + ? EccPayloadCorruptor::flipRandom( + *mev, w, entry.scheme, draw.per_word_errors[w], + payload_dtype_, rng_) + : EccPayloadCorruptor::flipExact( + *mev, w, entry.scheme, draw.exact_bits, payload_dtype_); + flips += count.total; } due_poison_flips_total_ += flips; if (stat_due_poisoned_) stat_due_poisoned_->addData(flips); @@ -1540,11 +1458,15 @@ uint64_t EccGuard::applyPolicy(MemEvent* mev) { // Correctable words leak nothing; DUE words get the DUE response below. unsigned hi = 0, lo = 0, flips = 0; for (uint32_t w : line.escape_words) { - flips += draw.exact_bits.empty() - ? flipBitsInWord(mev, w, entry.scheme, - draw.per_word_errors[w], hi, lo) - : flipExactBitsInWord(mev, w, entry.scheme, - draw.exact_bits, hi, lo); + EccPayloadFlipCount count = draw.exact_bits.empty() + ? EccPayloadCorruptor::flipRandom( + *mev, w, entry.scheme, draw.per_word_errors[w], + payload_dtype_, rng_) + : EccPayloadCorruptor::flipExact( + *mev, w, entry.scheme, draw.exact_bits, payload_dtype_); + flips += count.total; + hi += count.high; + lo += count.low; } escape_high_blast_total_ += hi; escape_low_blast_total_ += lo; @@ -1605,45 +1527,3 @@ uint64_t EccGuard::applyPolicy(MemEvent* mev) { return latency_ps; } - -unsigned EccGuard::flipBitsInWord(MemEvent* mev, uint32_t word_index, - EccScheme scheme, unsigned nbits, - unsigned& high_flips, unsigned& low_flips) { - auto& payload = mev->getPayload(); - if (payload.empty() || nbits == 0) return 0; - uint32_t total_bytes = static_cast(payload.size()); - uint32_t wb = eccWordBytes(scheme); - uint32_t start = (wb == 0) ? 0 : word_index * wb; - uint32_t end = (wb == 0) ? total_bytes - : std::min(total_bytes, start + wb); - if (start >= end) return 0; - uint32_t span_bits = (end - start) * 8; - if (nbits > span_bits) nbits = span_bits; - - unsigned elem_bytes = dtypeBytes(payload_dtype_); - if (elem_bytes == 0) elem_bytes = 1; - - // Sample distinct bit positions inside the word span so repeated flips - // cannot XOR-cancel. nbits << span_bits in practice, so rejection - // sampling terminates quickly; the cap above guarantees termination. - std::set used; - unsigned flipped = 0; - while (flipped < nbits) { - uint32_t bit = rng_.generateNextUInt32() % span_bits; - if (!used.insert(bit).second) continue; - uint32_t global_byte = start + bit / 8u; - uint32_t bit_in_byte = bit % 8u; - payload[global_byte] ^= static_cast(1u << bit_in_byte); - bool hi = false; - if (payload_dtype_ != PayloadDtype::Bytes) { - // Elements are little-endian and aligned to the payload start; - // bit 0 of an element is its LSB (byte 0 of bf16 = mantissa low, - // byte 1 = sign + exponent high). - uint32_t bit_in_elem = (global_byte % elem_bytes) * 8u + bit_in_byte; - hi = isHighBlastBit(payload_dtype_, bit_in_elem); - } - if (hi) ++high_flips; else ++low_flips; - ++flipped; - } - return flipped; -} diff --git a/src/sst/elements/carcosa/components/eccGuard.h b/src/sst/elements/carcosa/components/eccGuard.h index 5c987f38cf..ae1535a517 100644 --- a/src/sst/elements/carcosa/components/eccGuard.h +++ b/src/sst/elements/carcosa/components/eccGuard.h @@ -17,6 +17,8 @@ // Schroeder SIGMETRICS'09). due_action=drop_frame sets frameAbortRequested. #include "sst/elements/carcosa/components/eccPolicy.h" +#include "sst/elements/carcosa/components/eccPayloadCorruptor.h" +#include "sst/elements/carcosa/components/componentTestBounds.h" #include "sst/elements/carcosa/components/eccScheme.h" #include "sst/elements/carcosa/components/pipelineStateRegistry.h" #include "sst/elements/memHierarchy/memEvent.h" @@ -162,7 +164,7 @@ class EccGuard : public SST::Component { void finish() override; enum class FaultModel : uint8_t { Poisson, JedecMix, Campaign, Resident }; - enum class PayloadDtype : uint8_t { Bytes, Bf16, Fp8, Int8 }; + using PayloadDtype = EccPayloadDtype; enum class DueAction : uint8_t { LatencyOnly, DropFrame }; enum class FaultMode : uint8_t { @@ -197,19 +199,12 @@ class EccGuard : public SST::Component { void handleSelf(SST::Event* ev); uint64_t applyPolicy(SST::MemHierarchy::MemEvent* mev); - // Flip nbits distinct random bits in ECC word word_index (no XOR-cancel). - // Classifies high/low blast for payload_dtype_. Returns bits actually flipped. - unsigned flipBitsInWord(SST::MemHierarchy::MemEvent* mev, - uint32_t word_index, EccScheme scheme, - unsigned nbits, - unsigned& high_flips, unsigned& low_flips); FaultDraw drawFaultPoisson(uint32_t payload_bytes, double ber, EccScheme scheme); FaultDraw drawFaultJedecMix(uint32_t payload_bytes, double event_rate, EccScheme scheme); void distributeErrorsToChips(std::vector& chip_counts, unsigned errs, EccScheme scheme, FaultMode mode); - // Campaign injection: deterministic budget gated on (current kernel == - // campaign_target_kernel_) and per-access probability - // campaign_event_rate_; see eccGuard.h docs. + void placeFaultErrors(FaultDraw& draw, uint32_t payload_bytes, + EccScheme scheme); FaultDraw drawFaultCampaign(uint32_t payload_bytes, EccScheme scheme, const std::string& kernel_name); @@ -238,13 +233,6 @@ class EccGuard : public SST::Component { unsigned bit_in_line); FaultDraw drawFaultResident(SST::MemHierarchy::MemEvent* mev, uint32_t payload_bytes, EccScheme scheme); - // Flip the subset of draw.exact_bits that falls inside ECC word - // `word_index` (payload-relative), classifying blast per bit. - unsigned flipExactBitsInWord(SST::MemHierarchy::MemEvent* mev, - uint32_t word_index, EccScheme scheme, - const std::vector& exact_bits, - unsigned& high_flips, unsigned& low_flips); - // Emit a one-shot warning whenever a policy entry's BER exceeds the // documented tight-approximation bound (see kEccBerTightUpperBound in // eccScheme.h). Tracks already-warned BER values to avoid log spam. @@ -273,7 +261,7 @@ class EccGuard : public SST::Component { SST::Link* selfLink_ = nullptr; EccPolicyTable policy_; - bool applyOnResponsesOnly_ = true; + bool apply_on_responses_only_ = true; FaultModel fault_model_ = FaultModel::Poisson; PayloadDtype payload_dtype_ = PayloadDtype::Bytes; @@ -373,13 +361,7 @@ class EccGuard : public SST::Component { // "" means "no FSM publisher yet". std::map, OutcomeCounters> per_kernel_region_; - // Dedicated-branch test oracles. A negative value disables each bound. - int64_t test_total_min_ = -1, test_total_max_ = -1; - int64_t test_clean_min_ = -1, test_clean_max_ = -1; - int64_t test_correctable_min_ = -1, test_correctable_max_ = -1; - int64_t test_due_min_ = -1, test_due_max_ = -1; - int64_t test_escape_min_ = -1, test_escape_max_ = -1; - int64_t test_resident_born_min_ = -1, test_resident_born_max_ = -1; + ComponentTestBounds test_bounds_; // Fault-mode draw counters; written every time fault_model_=JedecMix fires. uint64_t per_mode_draws_[static_cast(FaultMode::Count)] = {}; diff --git a/src/sst/elements/carcosa/components/eccPayloadCorruptor.cc b/src/sst/elements/carcosa/components/eccPayloadCorruptor.cc new file mode 100644 index 0000000000..1587b662e6 --- /dev/null +++ b/src/sst/elements/carcosa/components/eccPayloadCorruptor.cc @@ -0,0 +1,120 @@ +// Copyright 2009-2026 NTESS. Under the terms +// of Contract DE-NA0003525 with NTESS, the U.S. +// Government retains certain rights in this software. + +#include "sst_config.h" +#include "sst/elements/carcosa/components/eccPayloadCorruptor.h" + +#include +#include + +using namespace SST::Carcosa; + +namespace { + +unsigned dtypeBytes(EccPayloadDtype dtype) +{ + return dtype == EccPayloadDtype::Bf16 ? 2 : 1; +} + +bool isHighBlastBit(EccPayloadDtype dtype, unsigned bit) +{ + switch (dtype) { + case EccPayloadDtype::Bf16: + return bit == 15 || bit == 13 || bit == 14; + case EccPayloadDtype::Fp8: + return bit == 7 || bit == 6; + case EccPayloadDtype::Int8: + return bit == 7; + case EccPayloadDtype::Bytes: + return false; + } + return false; +} + +void countBlast(EccPayloadFlipCount& result, EccPayloadDtype dtype, + uint32_t byte, uint32_t bit) +{ + bool high = dtype != EccPayloadDtype::Bytes && + isHighBlastBit(dtype, (byte % dtypeBytes(dtype)) * 8u + bit); + if (high) ++result.high; + else ++result.low; + ++result.total; +} + +} // namespace + +bool EccPayloadCorruptor::parseDtype(const std::string& value, + EccPayloadDtype& dtype) +{ + if (value == "bytes" || value == "BYTES") dtype = EccPayloadDtype::Bytes; + else if (value == "bf16" || value == "BF16") dtype = EccPayloadDtype::Bf16; + else if (value == "fp8" || value == "FP8") dtype = EccPayloadDtype::Fp8; + else if (value == "int8" || value == "INT8") dtype = EccPayloadDtype::Int8; + else return false; + return true; +} + +const char* EccPayloadCorruptor::dtypeName(EccPayloadDtype dtype) +{ + switch (dtype) { + case EccPayloadDtype::Bytes: return "bytes"; + case EccPayloadDtype::Bf16: return "bf16"; + case EccPayloadDtype::Fp8: return "fp8"; + case EccPayloadDtype::Int8: return "int8"; + } + return "unknown"; +} + +EccPayloadFlipCount EccPayloadCorruptor::flipRandom( + SST::MemHierarchy::MemEvent& event, uint32_t word_index, + EccScheme scheme, unsigned count, EccPayloadDtype dtype, + SST::RNG::MersenneRNG& rng) +{ + EccPayloadFlipCount result; + auto& payload = event.getPayload(); + if (payload.empty() || count == 0) return result; + + uint32_t word_bytes = eccWordBytes(scheme); + uint32_t start = word_bytes == 0 ? 0 : word_index * word_bytes; + uint32_t end = word_bytes == 0 + ? payload.size() : std::min(payload.size(), start + word_bytes); + if (start >= end) return result; + + uint32_t span_bits = (end - start) * 8; + count = std::min(count, span_bits); + std::set used; + while (result.total < count) { + uint32_t selected = rng.generateNextUInt32() % span_bits; + if (!used.insert(selected).second) continue; + uint32_t byte = start + selected / 8u; + uint32_t bit = selected % 8u; + payload[byte] ^= static_cast(1u << bit); + countBlast(result, dtype, byte, bit); + } + return result; +} + +EccPayloadFlipCount EccPayloadCorruptor::flipExact( + SST::MemHierarchy::MemEvent& event, uint32_t word_index, + EccScheme scheme, const std::vector& bits, + EccPayloadDtype dtype) +{ + EccPayloadFlipCount result; + auto& payload = event.getPayload(); + if (payload.empty() || bits.empty()) return result; + + uint32_t total_bits = payload.size() * 8; + uint32_t word_bytes = eccWordBytes(scheme); + uint32_t start = word_bytes == 0 ? 0 : word_index * word_bytes * 8; + uint32_t end = word_bytes == 0 + ? total_bits : std::min(total_bits, start + word_bytes * 8); + for (uint32_t selected : bits) { + if (selected < start || selected >= end) continue; + uint32_t byte = selected / 8u; + uint32_t bit = selected % 8u; + payload[byte] ^= static_cast(1u << bit); + countBlast(result, dtype, byte, bit); + } + return result; +} diff --git a/src/sst/elements/carcosa/components/eccPayloadCorruptor.h b/src/sst/elements/carcosa/components/eccPayloadCorruptor.h new file mode 100644 index 0000000000..12b6cef5f9 --- /dev/null +++ b/src/sst/elements/carcosa/components/eccPayloadCorruptor.h @@ -0,0 +1,45 @@ +// Copyright 2009-2026 NTESS. Under the terms +// of Contract DE-NA0003525 with NTESS, the U.S. +// Government retains certain rights in this software. + +#ifndef SST_ELEMENTS_CARCOSA_ECC_PAYLOAD_CORRUPTOR_H +#define SST_ELEMENTS_CARCOSA_ECC_PAYLOAD_CORRUPTOR_H + +#include "sst/elements/carcosa/components/eccScheme.h" + +#include +#include + +#include +#include +#include + +namespace SST::Carcosa { + +enum class EccPayloadDtype : uint8_t { Bytes, Bf16, Fp8, Int8 }; + +struct EccPayloadFlipCount { + unsigned total = 0; + unsigned high = 0; + unsigned low = 0; +}; + +class EccPayloadCorruptor { +public: + static bool parseDtype(const std::string& value, EccPayloadDtype& dtype); + static const char* dtypeName(EccPayloadDtype dtype); + + static EccPayloadFlipCount flipRandom( + SST::MemHierarchy::MemEvent& event, uint32_t word_index, + EccScheme scheme, unsigned count, EccPayloadDtype dtype, + SST::RNG::MersenneRNG& rng); + + static EccPayloadFlipCount flipExact( + SST::MemHierarchy::MemEvent& event, uint32_t word_index, + EccScheme scheme, const std::vector& bits, + EccPayloadDtype dtype); +}; + +} // namespace SST::Carcosa + +#endif diff --git a/src/sst/elements/carcosa/components/eccPolicy.h b/src/sst/elements/carcosa/components/eccPolicy.h index 74889ffe83..afec9d5131 100644 --- a/src/sst/elements/carcosa/components/eccPolicy.h +++ b/src/sst/elements/carcosa/components/eccPolicy.h @@ -12,6 +12,7 @@ #ifndef SST_ELEMENTS_CARCOSA_ECC_POLICY_H #define SST_ELEMENTS_CARCOSA_ECC_POLICY_H +#include "sst/elements/carcosa/components/configParse.h" #include "sst/elements/carcosa/components/eccScheme.h" #include #include @@ -139,10 +140,31 @@ class EccPolicyTable { continue; } } - if (parts.size() >= 3) e.ber = parseDouble(parts[2]); - if (parts.size() >= 4) e.correctable_latency_ps = parseUInt64(parts[3]); - if (parts.size() >= 5) e.due_latency_ps = parseUInt64(parts[4]); - if (parts.size() >= 6) e.escape_latency_ps = parseUInt64(parts[5]); + if (parts.size() >= 3 && + (!ConfigParse::parseDouble(parts[2], e.ber) || + !ConfigParse::isProbability(e.ber))) { + errors.push_back("ecc_kernel_policy: invalid BER '" + parts[2] + "' for '" + parts[0] + "'"); + continue; + } + if (parts.size() >= 4 && + !ConfigParse::parseUint64(parts[3], e.correctable_latency_ps)) { + errors.push_back("ecc_kernel_policy: invalid correctable latency '" + parts[3] + "'"); + continue; + } + if (parts.size() >= 5 && + !ConfigParse::parseUint64(parts[4], e.due_latency_ps)) { + errors.push_back("ecc_kernel_policy: invalid DUE latency '" + parts[4] + "'"); + continue; + } + if (parts.size() >= 6 && + !ConfigParse::parseUint64(parts[5], e.escape_latency_ps)) { + errors.push_back("ecc_kernel_policy: invalid escape latency '" + parts[5] + "'"); + continue; + } + if (parts.size() > 6) { + errors.push_back("ecc_kernel_policy: too many fields in '" + buf + "'"); + continue; + } if (!kernel_any && !region_any) { setPerKernelRegion(kernel_tok, region_tok, e); @@ -198,12 +220,6 @@ class EccPolicyTable { return out; } - static double parseDouble(const std::string& s) { - try { return std::stod(s); } catch (...) { return 0.0; } - } - static uint64_t parseUInt64(const std::string& s) { - try { return static_cast(std::stoull(s)); } catch (...) { return 0; } - } }; } // namespace Carcosa diff --git a/src/sst/elements/carcosa/components/fourStateAgent.cc b/src/sst/elements/carcosa/components/fourStateAgent.cc index 41e8dde0ad..3868834b82 100644 --- a/src/sst/elements/carcosa/components/fourStateAgent.cc +++ b/src/sst/elements/carcosa/components/fourStateAgent.cc @@ -28,7 +28,7 @@ FourStateAgent::FourStateAgent(ComponentId_t id, Params& params) { out_ = new Output("", 1, 0, Output::STDOUT); - stateKey_ = params.find("state_key", ""); + state_key_ = params.find("state_key", ""); regionSize_ = params.find("region_size", 4096); regionsCsv_ = params.find("regions", ""); initialCommand_ = params.find("initial_command", 0); @@ -42,7 +42,7 @@ FourStateAgent::FourStateAgent(ComponentId_t id, Params& params) "FourStateAgent: 'num_commands' must be >= 1 (got %d).\n", numCommands_); } - if (stateKey_.empty()) { + if (state_key_.empty()) { out_->fatal(CALL_INFO, -1, "FourStateAgent: 'state_key' is required (pick something unique per core, " "e.g. 'core0').\n"); @@ -59,33 +59,26 @@ void FourStateAgent::agentSetup() nextCommand_ = initialCommand_; // Establish the registry entry for this core. After this point any - // PortModule (e.g. PortModuleStateGate) looking up stateKey_ will see + // PortModule (e.g. PortModuleStateGate) looking up state_key_ will see // a live snapshot instead of nullptr. - PipelineStateBase* s = PipelineStateRegistry::getOrCreate(stateKey_); - s->currentKernel = IDLE; - s->currentKernelName = kernelNameFor(IDLE); - s->pipelineCycle = 0; + PipelineStateBase* s = PipelineStateRegistry::getOrCreate(state_key_); + s->publishKernel(IDLE, kernelNameFor(IDLE), 0); publishedKernel_ = IDLE; // Publish the MMIO control region as a named region so that // `region_names="mmio_control"` predicates can match. - s->ensureRegionSlot(0); - s->regions[0].base = controlAddrBase_; - s->regions[0].size = regionSize_; - s->regions[0].valid = regionSize_ > 0; - s->regions[0].id = 0; - s->regions[0].name = "mmio_control"; - - int n_user = publishUserRegions(stateKey_, regionsCsv_, out_, "FourStateAgent"); + s->publishRegion(0, controlAddrBase_, regionSize_, "mmio_control"); + + int n_user = publishUserRegions(state_key_, regionsCsv_, out_, "FourStateAgent"); if (verbose_ && n_user > 0) { out_->output("FourStateAgent[%s]: published %d user region(s)\n", - stateKey_.c_str(), n_user); + state_key_.c_str(), n_user); } if (verbose_) { out_->output("FourStateAgent[%s]: setup initial_command=%d num_commands=%d " "max_iterations=%d mmio_base=0x%" PRIx64 " size=%" PRIu64 "\n", - stateKey_.c_str(), initialCommand_, numCommands_, maxIterations_, + state_key_.c_str(), initialCommand_, numCommands_, maxIterations_, controlAddrBase_, regionSize_); } @@ -98,20 +91,19 @@ void FourStateAgent::agentSetup() void FourStateAgent::publishState(int kernel) { - PipelineStateBase* s = PipelineStateRegistry::getMutable(stateKey_); + PipelineStateBase* s = PipelineStateRegistry::getMutable(state_key_); if (!s) { // agentSetup() should have created the entry; defensively re-create // so a stray lookup from a PortModule never sees nullptr mid-run. - s = PipelineStateRegistry::getOrCreate(stateKey_); + s = PipelineStateRegistry::getOrCreate(state_key_); } - s->currentKernel = kernel; - s->currentKernelName = kernelNameFor(kernel); - s->pipelineCycle = (numCommands_ > 0) ? (currentIteration_ / numCommands_) : 0; + s->publishKernel(kernel, kernelNameFor(kernel), + numCommands_ > 0 ? currentIteration_ / numCommands_ : 0); publishedKernel_ = kernel; if (verbose_) { out_->output("FourStateAgent[%s]: publish currentKernel=%d ('%s') pipelineCycle=%d\n", - stateKey_.c_str(), s->currentKernel, s->currentKernelName.c_str(), + state_key_.c_str(), s->currentKernel, s->currentKernelName.c_str(), s->pipelineCycle); } } @@ -156,7 +148,7 @@ bool FourStateAgent::handleInterceptedEvent(MemEvent* ev, Link* highlink) } if (verbose_) { out_->output("FourStateAgent[%s]: sent done iteration %u\n", - stateKey_.c_str(), currentIteration_); + state_key_.c_str(), currentIteration_); } checkBothDone(); return true; @@ -174,7 +166,7 @@ void FourStateAgent::notifyPartnerDone(unsigned iteration) partnerDone_ = true; if (verbose_) { out_->output("FourStateAgent[%s]: partner done (iteration=%u)\n", - stateKey_.c_str(), iteration); + state_key_.c_str(), iteration); } checkBothDone(); } @@ -207,15 +199,10 @@ void FourStateAgent::setInterceptBase(uint64_t base) { // agentSetup() hasn't necessarily run yet, but if the registry entry // already exists (e.g. a gate looked it up), keep the region info // consistent with the base Hali handed us. - if (!stateKey_.empty()) { - PipelineStateBase* s = PipelineStateRegistry::getMutable(stateKey_); + if (!state_key_.empty()) { + PipelineStateBase* s = PipelineStateRegistry::getMutable(state_key_); if (s) { - s->ensureRegionSlot(0); - s->regions[0].base = controlAddrBase_; - s->regions[0].size = regionSize_; - s->regions[0].valid = regionSize_ > 0; - s->regions[0].id = 0; - s->regions[0].name = "mmio_control"; + s->publishRegion(0, controlAddrBase_, regionSize_, "mmio_control"); } } } diff --git a/src/sst/elements/carcosa/components/fourStateAgent.h b/src/sst/elements/carcosa/components/fourStateAgent.h index f38c18b990..0829aa1a6e 100644 --- a/src/sst/elements/carcosa/components/fourStateAgent.h +++ b/src/sst/elements/carcosa/components/fourStateAgent.h @@ -95,7 +95,7 @@ class FourStateAgent : public InterceptionAgentAPI SST::MemHierarchy::MemEvent* pendingCommandRead_ = nullptr; // Registry publishing state - std::string stateKey_; + std::string state_key_; std::string regionsCsv_; std::vector kernelNames_; int publishedKernel_ = IDLE; diff --git a/src/sst/elements/carcosa/components/framePipelineDriver.cc b/src/sst/elements/carcosa/components/framePipelineDriver.cc index 61560bd8e1..c394020d02 100644 --- a/src/sst/elements/carcosa/components/framePipelineDriver.cc +++ b/src/sst/elements/carcosa/components/framePipelineDriver.cc @@ -83,15 +83,8 @@ FramePipelineDriver::FramePipelineDriver(ComponentId_t id, Params& params) // across components is unspecified. Constructors all run first. state_ptr_ = PipelineStateRegistry::getOrCreate(state_key_); state_ptr_->actuationKernelName = "ACTUATE"; - state_ptr_->currentKernel = -1; - state_ptr_->currentKernelName = "IDLE"; - state_ptr_->pipelineCycle = 0; - state_ptr_->ensureRegionSlot(0); - state_ptr_->regions[0].base = region_base_; - state_ptr_->regions[0].size = region_size_; - state_ptr_->regions[0].valid = true; - state_ptr_->regions[0].id = 0; - state_ptr_->regions[0].name = region_name_; + state_ptr_->publishKernel(-1, "IDLE", 0); + state_ptr_->publishRegion(0, region_base_, region_size_, region_name_); std::string extra = params.find("extra_region", ""); if (!extra.empty()) { @@ -103,12 +96,8 @@ FramePipelineDriver::FramePipelineDriver(ComponentId_t id, Params& params) "FramePipelineDriver '%s': extra_region must be " "'name:base:size' (got '%s').\n", getName().c_str(), extra.c_str()); } - state_ptr_->ensureRegionSlot(1); - state_ptr_->regions[1].base = std::stoull(base_s, nullptr, 0); - state_ptr_->regions[1].size = std::stoull(size_s, nullptr, 0); - state_ptr_->regions[1].valid = true; - state_ptr_->regions[1].id = 1; - state_ptr_->regions[1].name = name; + state_ptr_->publishRegion(1, std::stoull(base_s, nullptr, 0), + std::stoull(size_s, nullptr, 0), name); } buildScript(); @@ -148,9 +137,7 @@ bool FramePipelineDriver::clockTick(Cycle_t) { const Op& op = script_[pc_++]; switch (op.kind) { case Op::Kind::Publish: - state_ptr_->currentKernel = op.kernelId; - state_ptr_->currentKernelName = op.kernelName; - state_ptr_->pipelineCycle = op.cycle; + state_ptr_->publishKernel(op.kernelId, op.kernelName, op.cycle); cur_frame_ = op.cycle; if (verbose_) { out_->output("FramePipelineDriver '%s': publish kernel=%d " @@ -188,7 +175,7 @@ void FramePipelineDriver::stampFrame() { fr.actionToken = frame_tokens_[cur_frame_]; if (std::find(escape_frames_.begin(), escape_frames_.end(), cur_frame_) != escape_frames_.end()) { - ++state_ptr_->eccCumulativeEscapes; + state_ptr_->addEccCounts(1, 0); } fr.cumulativeEscapes = state_ptr_->eccCumulativeEscapes; fr.cumulativeFlips = state_ptr_->eccCumulativeFlips; @@ -199,7 +186,7 @@ void FramePipelineDriver::stampFrame() { fr.actionChecksum = state_ptr_->watcherActionChecksum; } fr.simTimePs = getCurrentSimTimeNano() * 1000; - state_ptr_->frames.push_back(fr); + state_ptr_->appendFrame(fr); if (verbose_) { out_->output("FramePipelineDriver '%s': stamped frame cycle=%d " "checksum=%" PRIu64 "\n", getName().c_str(), diff --git a/src/sst/elements/carcosa/components/pipelineStateRegistry.h b/src/sst/elements/carcosa/components/pipelineStateRegistry.h index 23ecec8809..c5234e3b55 100644 --- a/src/sst/elements/carcosa/components/pipelineStateRegistry.h +++ b/src/sst/elements/carcosa/components/pipelineStateRegistry.h @@ -21,6 +21,7 @@ #include #include #include +#include #include namespace SST { @@ -148,6 +149,55 @@ struct PipelineStateBase { stagedSize = 0; } + void publishKernel(int id, std::string name, int cycle) { + currentKernel = id; + currentKernelName = std::move(name); + pipelineCycle = cycle; + } + + void publishRegion(size_t id, uint64_t base, uint64_t size, + std::string name = {}) { + ensureRegionSlot(id); + regions[id] = {base, size, size > 0, static_cast(id), + std::move(name)}; + } + + bool requestFrameAbort() { + if (frameAbortRequested) return false; + frameAbortRequested = true; + return true; + } + + bool consumeFrameAbort() { + if (!frameAbortRequested) return false; + frameAbortRequested = false; + ++framesDropped; + return true; + } + + void addEccCounts(uint64_t escapes, uint64_t flips) { + eccCumulativeEscapes += escapes; + eccCumulativeFlips += flips; + } + + void addKernelEscape(const std::string& kernel) { + ++eccPerFrameEscapesByKernel[kernel]; + } + + void publishWatcherChecksum(uint64_t checksum) { + watcherActionChecksum = checksum; + watcherActionChecksumValid = true; + } + + void retireWatcherChecksum() { watcherActionChecksumValid = false; } + + void recordWatcherCorruption(bool corrupted) { + watcherCriticalCorrupted = corrupted; + if (corrupted) ++framesCriticalRegionCorrupted; + } + + void appendFrame(FrameRecord frame) { frames.push_back(std::move(frame)); } + virtual ~PipelineStateBase() = default; }; diff --git a/src/sst/elements/carcosa/components/vlaRegions.h b/src/sst/elements/carcosa/components/vlaRegions.h index d8ba355fd3..dfe98bcaa3 100644 --- a/src/sst/elements/carcosa/components/vlaRegions.h +++ b/src/sst/elements/carcosa/components/vlaRegions.h @@ -12,9 +12,7 @@ #ifndef CARCOSA_VLA_REGIONS_H #define CARCOSA_VLA_REGIONS_H -// Publish labeled regions into PipelineStateRegistry for region-aware ECC. -// Slot 0 = MMIO control; CSV NAME:BASE:SIZE fills slots 1..N (hex or decimal). - +#include #include #include #include @@ -27,17 +25,6 @@ namespace SST { namespace Carcosa { -inline uint64_t parseUint64Token(const std::string& tok) { - if (tok.empty()) return 0; - try { - if (tok.size() > 2 && (tok.substr(0, 2) == "0x" || tok.substr(0, 2) == "0X")) - return std::stoull(tok, nullptr, 16); - return std::stoull(tok, nullptr, 10); - } catch (...) { - return 0; - } -} - inline void trimRegionTok(std::string& s) { size_t b = 0; while (b < s.size() && std::isspace(static_cast(s[b]))) ++b; @@ -82,15 +69,16 @@ inline int publishUserRegions(const std::string& stateKey, } const std::string& name = parts[0]; - uint64_t base = parseUint64Token(parts[1]); - uint64_t size = parseUint64Token(parts[2]); + uint64_t base = 0, size = 0; + if (!ConfigParse::parseUint64(parts[1], base) || + !ConfigParse::parseUint64(parts[2], size)) { + if (log) log->fatal(CALL_INFO, -1, + "%s: invalid region entry '%s'.\n", who, + entry.c_str()); + return published; + } - s->ensureRegionSlot(slot); - s->regions[slot].base = base; - s->regions[slot].size = size; - s->regions[slot].valid = (size > 0); - s->regions[slot].id = slot; - s->regions[slot].name = name; + s->publishRegion(slot, base, size, name); ++slot; ++published; } @@ -103,10 +91,7 @@ inline bool consumeFrameAbort(const std::string& stateKey) { PipelineStateBase* s = PipelineStateRegistry::getMutable(stateKey); if (!s) return false; - if (!s->frameAbortRequested) return false; - s->frameAbortRequested = false; - s->framesDropped += 1; - return true; + return s->consumeFrameAbort(); } } // namespace Carcosa diff --git a/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.cc b/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.cc index c0bf7568fc..316adee111 100644 --- a/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.cc +++ b/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.cc @@ -42,11 +42,11 @@ SimplePipelineProducer::SimplePipelineProducer(ComponentId_t id, Params& params) { out_ = new Output("", 1, 0, Output::STDOUT); - stateKey_ = params.find("state_key", ""); + state_key_ = params.find("state_key", ""); totalCycles_ = params.find("total_cycles", 4); verbose_ = params.find("verbose", false); - if (stateKey_.empty()) stateKey_ = getName(); + if (state_key_.empty()) state_key_ = getName(); outLink_ = configureLink("out"); sst_assert(outLink_, CALL_INFO, -1, @@ -56,9 +56,8 @@ SimplePipelineProducer::SimplePipelineProducer(ComponentId_t id, Params& params) // Publish the initial snapshot. The gate may consult this before our first // tick fires; seed currentKernel=-1 so it doesn't accidentally match any // real stage id in the drop set. - state_ = PipelineStateRegistry::getOrCreate(stateKey_); - state_->currentKernel = -1; - state_->pipelineCycle = 0; + state_ = PipelineStateRegistry::getOrCreate(state_key_); + state_->publishKernel(-1, "IDLE", 0); // Demonstrate the region-publish API with a single dummy region; not used // by the gate in this example but exercised so the pattern is visible. @@ -76,7 +75,7 @@ SimplePipelineProducer::SimplePipelineProducer(ComponentId_t id, Params& params) if (verbose_) { out_->output("%s: producer ready, state_key='%s', total_cycles=%d, clock=%s\n", - getName().c_str(), stateKey_.c_str(), totalCycles_, clock_freq.c_str()); + getName().c_str(), state_key_.c_str(), totalCycles_, clock_freq.c_str()); } } @@ -92,8 +91,7 @@ bool SimplePipelineProducer::tick(Cycle_t /*cycle*/) // Publish BEFORE sending, so any PortModule on the receiving side observes // the snapshot that corresponds to the event it is about to see. - state_->currentKernel = stage; - state_->pipelineCycle = cycle; + state_->publishKernel(stage, simpleStageName(stage), cycle); outLink_->send(new SimpleStageEvent(stage)); @@ -194,10 +192,10 @@ SimpleStageGate::SimpleStageGate(Params& params) verbose_ = params.find("verbose", false); out_ = new Output("", verbose_ ? 1 : 0, 0, Output::STDOUT); - stateKey_ = params.find("state_key", ""); + state_key_ = params.find("state_key", ""); dropStages_ = parseIntCsv(params.find("drop_stages", "")); - if (stateKey_.empty()) { + if (state_key_.empty()) { out_->fatal(CALL_INFO, -1, "SimpleStageGate: 'state_key' is required (must match the producer's state_key or getName()).\n"); } @@ -209,7 +207,7 @@ SimpleStageGate::SimpleStageGate(Params& params) list += simpleStageName(s); } out_->output("SimpleStageGate: state_key='%s' drop_stages=[%s]\n", - stateKey_.c_str(), list.c_str()); + state_key_.c_str(), list.c_str()); } } @@ -217,7 +215,7 @@ SimpleStageGate::~SimpleStageGate() { if (out_) { out_->output("[SimpleStageGate %s] summary: evaluated=%" PRIu64 " dropped=%" PRIu64 "\n", - stateKey_.c_str(), evaluated_, dropped_); + state_key_.c_str(), evaluated_, dropped_); delete out_; } } @@ -225,7 +223,7 @@ SimpleStageGate::~SimpleStageGate() const PipelineStateBase* SimpleStageGate::resolveState() const { if (cached_) return cached_; - cached_ = PipelineStateRegistry::get(stateKey_); + cached_ = PipelineStateRegistry::get(state_key_); return cached_; } @@ -249,7 +247,7 @@ void SimpleStageGate::interceptHandler(uintptr_t /*key*/, Event*& ev, bool& canc if (verbose_) { out_->output("[SimpleStageGate %s] DROP stage=%s cycle=%d\n", - stateKey_.c_str(), + state_key_.c_str(), simpleStageName(st->currentKernel), st->pipelineCycle); } diff --git a/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.h b/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.h index 2a5711ab56..d317cd1969 100644 --- a/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.h +++ b/src/sst/elements/carcosa/examples/SimplePipeline/simplePipelineExample.h @@ -119,7 +119,7 @@ class SimplePipelineProducer : public SST::Component { SST::Link* outLink_ = nullptr; PipelineStateBase* state_ = nullptr; - std::string stateKey_; + std::string state_key_; int totalCycles_ = 0; int tickCount_ = 0; bool verbose_ = false; @@ -198,7 +198,7 @@ class SimpleStageGate : public SST::PortModule { static std::set parseIntCsv(const std::string& s); SST::Output* out_ = nullptr; - std::string stateKey_; + std::string state_key_; std::set dropStages_; bool verbose_ = false; diff --git a/src/sst/elements/carcosa/injectors/portModuleStateGate.cc b/src/sst/elements/carcosa/injectors/portModuleStateGate.cc index ee14953b16..69a95b1ab0 100644 --- a/src/sst/elements/carcosa/injectors/portModuleStateGate.cc +++ b/src/sst/elements/carcosa/injectors/portModuleStateGate.cc @@ -10,6 +10,7 @@ // distribution. #include "sst/elements/carcosa/injectors/portModuleStateGate.h" +#include "sst/elements/carcosa/components/configParse.h" #include "sst/elements/carcosa/faultlogic/randomFlipFault.h" #include "sst/core/params.h" @@ -43,10 +44,13 @@ std::vector splitCsv(const std::string& csv) { return out; } -std::set parseIntSet(const std::string& csv) { +std::set parseIntSet(const std::string& csv, bool& valid) { std::set out; + valid = true; for (const auto& tok : splitCsv(csv)) { - try { out.insert(std::stoi(tok)); } catch (...) { /* skip bad tokens */ } + int value = 0; + if (!ConfigParse::parseInt(tok, value)) valid = false; + else out.insert(value); } return out; } @@ -59,29 +63,38 @@ std::set parseStringSet(const std::string& csv) { } // namespace -PortModuleStateGate::Mode -PortModuleStateGate::parseMode(const std::string& s) { +bool PortModuleStateGate::parseMode(const std::string& s, Mode& mode) { std::string v; v.reserve(s.size()); for (char c : s) v.push_back(static_cast(std::tolower(static_cast(c)))); - if (v == "drop") return Mode::Drop; - if (v == "flip") return Mode::Flip; - if (v == "drop_flip" || v == "dropflip" || v == "both") return Mode::DropFlip; - return Mode::Drop; + if (v == "drop") mode = Mode::Drop; + else if (v == "flip") mode = Mode::Flip; + else if (v == "drop_flip" || v == "dropflip" || v == "both") mode = Mode::DropFlip; + else return false; + return true; } PortModuleStateGate::PortModuleStateGate(Params& params) : FaultInjectorBase(params) { - stateKey_ = params.find("state_key", ""); - if (stateKey_.empty()) { + state_key_ = params.find("state_key", ""); + if (state_key_.empty()) { out_->fatal(CALL_INFO_LONG, -1, "PortModuleStateGate: 'state_key' is required.\n"); } - mode_ = parseMode(params.find("fault_mode", "drop")); - dropProb_ = params.find("drop_probability", 1.0); - flipProb_ = params.find("flip_probability", 1.0); + std::string mode = params.find("fault_mode", "drop"); + if (!parseMode(mode, mode_)) { + out_->fatal(CALL_INFO_LONG, -1, + "PortModuleStateGate: unknown fault_mode '%s'.\n", mode.c_str()); + } + drop_probability_ = params.find("drop_probability", 1.0); + flip_probability_ = params.find("flip_probability", 1.0); + if (!ConfigParse::isProbability(drop_probability_) || + !ConfigParse::isProbability(flip_probability_)) { + out_->fatal(CALL_INFO_LONG, -1, + "PortModuleStateGate: probabilities must be in [0,1].\n"); + } // Fixed layout [drop=0, flip=1]. Drop is inline (cancelDelivery on any Event); // fault[0] stays null. fault[1] is RandomFlipFault only when flip is enabled. @@ -93,13 +106,18 @@ PortModuleStateGate::PortModuleStateGate(Params& params) buildPredicates(params); setValidInstallation(params, SEND_RECEIVE_VALID); + if (getInstallDirection() == installDirection::Send && + (mode_ == Mode::Drop || mode_ == Mode::DropFlip)) { + out_->fatal(CALL_INFO_LONG, -1, + "PortModuleStateGate: drop modes require install_direction='Receive'.\n"); + } #ifdef __SST_DEBUG_OUTPUT__ dbg_->debug(CALL_INFO_LONG, 1, 0, "PortModuleStateGate: state_key='%s' mode=%d drop_p=%f flip_p=%f " "predicates=%zu\n", - stateKey_.c_str(), static_cast(mode_), - dropProb_, flipProb_, predicates_.size()); + state_key_.c_str(), static_cast(mode_), + drop_probability_, flip_probability_, predicates_.size()); #endif } @@ -108,13 +126,13 @@ PortModuleStateGate::buildPredicates(Params& params) { // Parse into serializable members first; rebuildPredicates() turns them // into lambdas and runs again after checkpoint restore. - kernelsCsv_ = params.find("kernels", ""); - hasCycleRange_ = params.contains("pipeline_cycle_start") + kernels_csv_ = params.find("kernels", ""); + has_cycle_range_ = params.contains("pipeline_cycle_start") || params.contains("pipeline_cycle_end"); - cycleStart_ = params.find("pipeline_cycle_start", 0); - cycleEnd_ = params.find("pipeline_cycle_end", INT32_MAX); - regionIdsCsv_ = params.find("region_ids", ""); - regionNamesCsv_ = params.find("region_names", ""); + cycle_start_ = params.find("pipeline_cycle_start", 0); + cycle_end_ = params.find("pipeline_cycle_end", INT32_MAX); + region_ids_csv_ = params.find("region_ids", ""); + region_names_csv_ = params.find("region_names", ""); rebuildPredicates(); } @@ -125,8 +143,12 @@ PortModuleStateGate::rebuildPredicates() predicates_.clear(); // kernel-id set predicate: matches when currentKernel is in the set. - if (!kernelsCsv_.empty()) { - auto allowed = parseIntSet(kernelsCsv_); + if (!kernels_csv_.empty()) { + bool valid = false; + auto allowed = parseIntSet(kernels_csv_, valid); + if (!valid) out_->fatal(CALL_INFO_LONG, -1, + "PortModuleStateGate: invalid kernels CSV '%s'.\n", + kernels_csv_.c_str()); predicates_.emplace_back( [allowed = std::move(allowed)](const PipelineStateBase& s, const EventAddress&) { @@ -136,9 +158,13 @@ PortModuleStateGate::rebuildPredicates() // pipeline cycle range predicate: [start, end] inclusive; either bound optional. // Use a large sentinel for "unset" to keep the comparison branch-free. - if (hasCycleRange_) { - const int start = cycleStart_; - const int end = cycleEnd_; + if (has_cycle_range_) { + if (cycle_start_ > cycle_end_) { + out_->fatal(CALL_INFO_LONG, -1, + "PortModuleStateGate: pipeline_cycle_start exceeds pipeline_cycle_end.\n"); + } + const int start = cycle_start_; + const int end = cycle_end_; predicates_.emplace_back( [start, end](const PipelineStateBase& s, const EventAddress&) { return s.pipelineCycle >= start && s.pipelineCycle <= end; @@ -154,8 +180,12 @@ PortModuleStateGate::rebuildPredicates() return ea.addr < r.base + r.size && ea.addr + sz > r.base; }; - if (!regionIdsCsv_.empty()) { - auto allowed = parseIntSet(regionIdsCsv_); + if (!region_ids_csv_.empty()) { + bool valid = false; + auto allowed = parseIntSet(region_ids_csv_, valid); + if (!valid) out_->fatal(CALL_INFO_LONG, -1, + "PortModuleStateGate: invalid region_ids CSV '%s'.\n", + region_ids_csv_.c_str()); predicates_.emplace_back( [allowed = std::move(allowed), overlaps](const PipelineStateBase& s, const EventAddress& ea) { @@ -167,8 +197,8 @@ PortModuleStateGate::rebuildPredicates() }); } - if (!regionNamesCsv_.empty()) { - auto allowed = parseStringSet(regionNamesCsv_); + if (!region_names_csv_.empty()) { + auto allowed = parseStringSet(region_names_csv_); predicates_.emplace_back( [allowed = std::move(allowed), overlaps](const PipelineStateBase& s, const EventAddress& ea) { @@ -197,7 +227,7 @@ PortModuleStateGate::doInjection(Event* ev) triggered_ = {{false, false}}; const PipelineStateBase* state = - PipelineStateRegistry::get(stateKey_); + PipelineStateRegistry::get(state_key_); if (!state) { // Agent hasn't published yet; no gate can match. return false; @@ -220,17 +250,17 @@ PortModuleStateGate::doInjection(Event* ev) switch (mode_) { case Mode::Drop: - triggered_[0] = (this->randFloat(0.0, 1.0) <= dropProb_); + triggered_[0] = (this->randFloat(0.0, 1.0) < drop_probability_); return triggered_[0]; case Mode::Flip: - triggered_[1] = (this->randFloat(0.0, 1.0) <= flipProb_); + triggered_[1] = (this->randFloat(0.0, 1.0) < flip_probability_); return triggered_[1]; case Mode::DropFlip: - triggered_[0] = (this->randFloat(0.0, 1.0) <= dropProb_); + triggered_[0] = (this->randFloat(0.0, 1.0) < drop_probability_); // Only roll for flip if we didn't already decide to drop the event; // a dropped event has nothing left to flip. triggered_[1] = !triggered_[0] && - (this->randFloat(0.0, 1.0) <= flipProb_); + (this->randFloat(0.0, 1.0) < flip_probability_); return triggered_[0] || triggered_[1]; } return false; @@ -240,19 +270,9 @@ void PortModuleStateGate::executeFaults(Event*& ev) { if (triggered_[0]) { - // Generic drop via cancelDelivery(); interceptor must delete the event - // (same contract as RandomDropFault::faultLogic). - if (getInstallDirection() == installDirection::Receive) { - delete ev; - ev = nullptr; - this->cancelDelivery(); - } else { -#ifdef __SST_DEBUG_OUTPUT__ - dbg_->debug(CALL_INFO_LONG, 1, 0, - "PortModuleStateGate: drop requested in Send direction is a no-op " - "(the framework doesn't expose a cancel hook on Send).\n"); -#endif - } + delete ev; + ev = nullptr; + this->cancelDelivery(); return; } if (triggered_[1]) { diff --git a/src/sst/elements/carcosa/injectors/portModuleStateGate.h b/src/sst/elements/carcosa/injectors/portModuleStateGate.h index 54473c29a2..b4a969d8bd 100644 --- a/src/sst/elements/carcosa/injectors/portModuleStateGate.h +++ b/src/sst/elements/carcosa/injectors/portModuleStateGate.h @@ -67,19 +67,19 @@ class PortModuleStateGate : public FaultInjectorBase { enum class Mode { Drop, Flip, DropFlip }; // Configuration - std::string stateKey_; + std::string state_key_; Mode mode_ = Mode::Drop; - double dropProb_ = 1.0; - double flipProb_ = 1.0; + double drop_probability_ = 1.0; + double flip_probability_ = 1.0; // Predicate configuration, kept in serializable form so checkpoint // restore can rebuild predicates_ (std::function is not serializable). - std::string kernelsCsv_; - bool hasCycleRange_ = false; - int cycleStart_ = 0; - int cycleEnd_ = INT32_MAX; - std::string regionIdsCsv_; - std::string regionNamesCsv_; + std::string kernels_csv_; + bool has_cycle_range_ = false; + int cycle_start_ = 0; + int cycle_end_ = INT32_MAX; + std::string region_ids_csv_; + std::string region_names_csv_; // Composed predicates (AND semantics; empty list => always-match). std::vector predicates_; @@ -104,17 +104,17 @@ class PortModuleStateGate : public FaultInjectorBase { void serialize_order(SST::Core::Serialization::serializer& ser) override { FaultInjectorBase::serialize_order(ser); - SST_SER(stateKey_); + SST_SER(state_key_); SST_SER(mode_); - SST_SER(dropProb_); - SST_SER(flipProb_); + SST_SER(drop_probability_); + SST_SER(flip_probability_); SST_SER(triggered_); - SST_SER(kernelsCsv_); - SST_SER(hasCycleRange_); - SST_SER(cycleStart_); - SST_SER(cycleEnd_); - SST_SER(regionIdsCsv_); - SST_SER(regionNamesCsv_); + SST_SER(kernels_csv_); + SST_SER(has_cycle_range_); + SST_SER(cycle_start_); + SST_SER(cycle_end_); + SST_SER(region_ids_csv_); + SST_SER(region_names_csv_); // predicates_ is a vector and cannot be serialized; // checkpoint restore uses the serialization ctor (NOT the params // ctor), so rebuild the lambdas from the config members here. @@ -124,7 +124,7 @@ class PortModuleStateGate : public FaultInjectorBase { ImplementVirtualSerializable(SST::Carcosa::PortModuleStateGate) private: - static Mode parseMode(const std::string& s); + static bool parseMode(const std::string& s, Mode& mode); }; } // namespace SST::Carcosa