From 07073b943d06de6424a53dabde6a480d8403bd90 Mon Sep 17 00:00:00 2001 From: "Samuel Filho (WIPRO LIMITED)" Date: Thu, 8 Oct 2026 22:53:37 -0300 Subject: [PATCH 1/9] create spec cpu pinning to numa feature --- .../SpecCpuProfileTests.cs | 23 ++ .../SPEC/SpecCpuExecutorTests.cs | 223 +++++++++++++++++- .../SPECcpu/SpecCpuExecutor.cs | 207 ++++++++++++++-- .../ProcessExtensionsAffinityTests.cs | 38 +++ .../VirtualClient.Core/ProcessExtensions.cs | 30 +++ .../profiles/PERF-SPECCPU-FPRATE.json | 16 +- .../profiles/PERF-SPECCPU-INTRATE.json | 13 +- 7 files changed, 525 insertions(+), 25 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs index 35069a938b..0a9010d312 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs @@ -5,6 +5,8 @@ namespace VirtualClient.Actions { using System; using System.Collections.Generic; + using System.IO; + using System.Linq; using System.Threading; using System.Threading.Tasks; using global::VirtualClient; @@ -38,6 +40,27 @@ public void SpecCpuWorkloadProfileParametersAreInlinedCorrectly(string profile) } } + [Test] + [TestCase("PERF-SPECCPU-INTRATE.json")] + [TestCase("PERF-SPECCPU-FPRATE.json")] + public async Task SpecCpuRateProfilesForwardProcessorAffinityToEveryAction(string profileName) + { + this.mockFixture.Setup(PlatformID.Unix); + + ExecutionProfile profile = await ExecutionProfile.ReadProfileAsync(Path.Combine(TestDependencies.ProfileDirectory, profileName)); + profile.Parameters["ProcessorAffinity"] = "2,4,8,10"; + profile.Parameters["Copies"] = 4; + profile.Inline(); + + List actions = profile.Actions.Where(action => action.Type == "SpecCpuExecutor").ToList(); + Assert.IsNotEmpty(actions); + foreach (ExecutionProfileElement action in actions) + { + Assert.AreEqual("2,4,8,10", action.Parameters["ProcessorAffinity"]); + Assert.AreEqual("4", action.Parameters["Copies"].ToString()); + } + } + [Test] [Ignore("We need to rethink how to do dependency testing with extension model.")] [TestCase("PERF-SPECCPU-FPRATE.json")] diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpuExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpuExecutorTests.cs index d40b34ecb6..0ed5504b9d 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpuExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpuExecutorTests.cs @@ -10,6 +10,7 @@ namespace VirtualClient.Actions using System.IO.Abstractions; using System.Linq; using System.Reflection; + using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -517,10 +518,228 @@ public async Task SpecCpuExecutorDoesNotApplyGcc15WorkaroundWhenGccVersionIsLess Assert.IsFalse(writtenConfigText.Contains("$Gcc10Workaround$"), "Placeholder '$Gcc10Workaround$' should be replaced."); } - private void SetupLinux() + [TestCase("intrate", "0-3", "$SPECCOPYNUM", null, "--localalloc -C 0-3 true")] + [TestCase("intrate", "2,4,8,10", "$BIND", "bind = 2,4,8,10", "--localalloc -C 2,4,8,10 true")] + [TestCase("fprate", "2,4,8,10", "$BIND", "bind = 2,4,8,10", "--localalloc -C 2,4,8,10 true")] + public async Task SpecCpuExecutorPinsEachRateCopyToItsSelectedCore(string specProfile, string coreList, string copyCpu, string expectedBind, string expectedPreflight) + { + this.SetupLinux(); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.SpecProfile)] = specProfile; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.Benchmarks)] = specProfile; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = coreList; + this.SetupLinuxAffinityProcesses(); + + List> writtenConfigs = new List>(); + this.mockFixture.File.Setup(f => f.WriteAllTextAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((path, content, token) => writtenConfigs.Add(new KeyValuePair(path, content))) + .Returns(Task.CompletedTask); + + string runArguments = null; + bool numactlChecked = false; + string preflightArguments = null; + Func createProcess = this.mockFixture.ProcessManager.OnCreateProcess; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + if (exe == "numactl" && arguments == "--version") + { + numactlChecked = true; + } + + if (exe == "numactl" && arguments.StartsWith("--localalloc -C ")) + { + preflightArguments = arguments; + } + + if (exe == "bash") + { + runArguments = arguments; + } + + return createProcess(exe, arguments, workingDir); + }; + + using (TestSpecCpuExecutor executor = new TestSpecCpuExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await executor.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + } + + KeyValuePair pinnedConfig = writtenConfigs.Single(config => config.Key.Contains("-affinity-")); + Assert.IsTrue(numactlChecked); + Assert.AreEqual(expectedPreflight, preflightArguments); + StringAssert.Contains($"submit = numactl --localalloc --physcpubind={copyCpu} -- $command", pinnedConfig.Value); + if (expectedBind != null) + { + StringAssert.Contains(expectedBind, pinnedConfig.Value); + } + + StringAssert.Contains($"--config {Path.GetFileName(pinnedConfig.Key)}", runArguments); + StringAssert.Contains(specProfile, runArguments); + } + + [Test] + public async Task SpecCpuExecutorUsesTheArm64ConfigurationWhenPinningRateCopies() + { + this.SetupLinux(Architecture.Arm64); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = "0-3"; + this.SetupLinuxAffinityProcesses(); + + string pinnedConfigPath = null; + this.mockFixture.File.Setup(f => f.WriteAllTextAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((path, content, token) => + { + if (path.Contains("-affinity-")) + { + pinnedConfigPath = path; + StringAssert.Contains("submit = numactl --localalloc --physcpubind=$SPECCOPYNUM -- $command", content); + } + }) + .Returns(Task.CompletedTask); + + using (TestSpecCpuExecutor executor = new TestSpecCpuExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await executor.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + } + + StringAssert.Contains("vc-linux-arm64-affinity-", Path.GetFileName(pinnedConfigPath)); + } + + [Test] + public async Task SpecCpuExecutorKeepsTheUnpinnedConfigAfterAnAffinityRunOnAnInstalledPackage() + { + this.SetupLinux(); + State installedState = new State(new Dictionary { ["SpecCpuInitialized"] = true }); + this.mockFixture.StateManager.OnGetState().ReturnsAsync(JObject.Parse(installedState.ToJson())); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = "0-3"; + this.SetupLinuxAffinityProcesses(); + + List writtenConfigs = new List(); + this.mockFixture.File.Setup(f => f.WriteAllTextAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((path, content, token) => writtenConfigs.Add(path)) + .Returns(Task.CompletedTask); + + List runArguments = new List(); + Func createProcess = this.mockFixture.ProcessManager.OnCreateProcess; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + if (exe == "bash") + { + runArguments.Add(arguments); + } + + return createProcess(exe, arguments, workingDir); + }; + + using (TestSpecCpuExecutor executor = new TestSpecCpuExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await executor.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + } + + Assert.AreEqual(1, writtenConfigs.Count); + StringAssert.Contains("-affinity-", writtenConfigs[0]); + StringAssert.Contains("-affinity-", runArguments[0]); + + this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = string.Empty; + using (TestSpecCpuExecutor executor = new TestSpecCpuExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await executor.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + } + + Assert.AreEqual(1, writtenConfigs.Count); + Assert.AreEqual(2, runArguments.Count); + StringAssert.Contains("--config vc-linux-x64.cfg", runArguments[1]); + } + + [TestCase("0-2")] + [TestCase("0,invalid,2,3")] + public void SpecCpuExecutorRejectsAnInvalidProcessorAffinity(string coreList) + { + this.SetupLinux(); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = coreList; + this.SetupLinuxAffinityProcesses(); + + using (TestSpecCpuExecutor executor = new TestSpecCpuExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.ThrowsAsync(async () => await executor.ExecuteAsync(CancellationToken.None)); + } + } + + [Test] + public void SpecCpuExecutorRequiresNumactlWhenPinningRateCopies() + { + this.SetupLinux(); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = "0-3"; + this.SetupLinuxAffinityProcesses(numactlExitCode: 1); + + using (TestSpecCpuExecutor executor = new TestSpecCpuExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.ThrowsAsync(async () => await executor.ExecuteAsync(CancellationToken.None)); + } + } + + [Test] + public void SpecCpuExecutorRejectsCoresThatNumactlCannotBind() + { + this.SetupLinux(); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = "0-3"; + this.SetupLinuxAffinityProcesses(preflightExitCode: 1); + + using (TestSpecCpuExecutor executor = new TestSpecCpuExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.ThrowsAsync(async () => await executor.ExecuteAsync(CancellationToken.None)); + } + } + + [Test] + public void SpecCpuExecutorRejectsProcessorAffinityForLinuxSpeedRuns() + { + this.SetupLinux(); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.SpecProfile)] = "intspeed"; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.Benchmarks)] = "intspeed"; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = "0-3"; + this.SetupLinuxAffinityProcesses(); + + using (TestSpecCpuExecutor executor = new TestSpecCpuExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.ThrowsAsync(async () => await executor.ExecuteAsync(CancellationToken.None)); + } + } + + [Test] + public void SpecCpuExecutorRejectsProcessorAffinityOnWindows() + { + this.SetupWindows(); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = "0-3"; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, + StandardOutput = new ConcurrentBuffer(new StringBuilder(arguments.Contains("Get-DiskImage") ? "X" : string.Empty)), + ExitCode = 0, + OnStart = () => true, + OnHasExited = () => true + }; + + using (TestSpecCpuExecutor executor = new TestSpecCpuExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.ThrowsAsync(async () => await executor.ExecuteAsync(CancellationToken.None)); + } + } + + private void SetupLinuxAffinityProcesses(int numactlExitCode = 0, int preflightExitCode = 0) + { + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, + StandardOutput = new ConcurrentBuffer(new StringBuilder(arguments == "gcc -dumpversion" ? "10" : string.Empty)), + ExitCode = exe == "numactl" ? (arguments == "--version" ? numactlExitCode : preflightExitCode) : 0, + OnStart = () => true, + OnHasExited = () => true + }; + } + + private void SetupLinux(Architecture architecture = Architecture.X64) { this.mockFixture = new MockFixture(); - this.mockFixture.Setup(PlatformID.Unix); + this.mockFixture.Setup(PlatformID.Unix, architecture); this.mockPackage = new DependencyPath("SPECcpu", this.mockFixture.PlatformSpecifics.GetPackagePath("speccpu", "1.1.8")); this.mockFixture.PackageManager.OnGetPackage().ReturnsAsync(this.mockPackage); diff --git a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs index 700d3486bd..a06149b324 100644 --- a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs @@ -5,19 +5,23 @@ namespace VirtualClient.Actions { using System; using System.Collections.Generic; + using System.ComponentModel; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Runtime.InteropServices; + using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using global::VirtualClient; using global::VirtualClient.Common; using global::VirtualClient.Common.Extensions; + using global::VirtualClient.Common.ProcessAffinity; using global::VirtualClient.Common.Telemetry; using global::VirtualClient.Contracts; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; using VirtualClient.Contracts.Metadata; /// @@ -145,6 +149,17 @@ public int Copies } } + /// + /// Optional list of physical CPU IDs to assign to SPECrate copies, one CPU per copy. + /// + public string ProcessorAffinity + { + get + { + return this.Parameters.GetValue(nameof(SpecCpuExecutor.ProcessorAffinity), string.Empty); + } + } + /// /// A feature flag to apply. For example 'UseCsvResults' can be used to parse the CSV file results vs. the standard output. /// The CSV results have finer-grained results with additional significant figures. @@ -178,11 +193,20 @@ public string FeatureFlag /// protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken) { + string stage = "Configuration"; try { using (BackgroundOperations profiling = BackgroundOperations.BeginProfiling(this, cancellationToken)) { - string commandLineArguments = this.GetCommandLineArguments(); + string configurationFile = this.GetConfigurationFileName(); + if (!string.IsNullOrWhiteSpace(this.ProcessorAffinity)) + { + stage = "AffinityConfiguration"; + configurationFile = await this.WritePinnedSpecCpuConfigAsync(telemetryContext, cancellationToken); + } + + this.LogSpecCpuStep(telemetryContext, "ConfigSelected", ("configurationFile", configurationFile)); + string commandLineArguments = this.GetCommandLineArguments(configurationFile); string command = null; string commandArguments = null; @@ -198,18 +222,33 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel commandArguments = $"/c {SpecCpuExecutor.SpecCpuRunBat} {commandLineArguments}"; } + stage = "Run"; + this.LogSpecCpuStep(telemetryContext, "RunStarted", ("configurationFile", configurationFile), ("command", command), ("arguments", commandArguments)); + DateTime runStart = DateTime.UtcNow; using (IProcessProxy process = await this.ExecuteCommandAsync(command, commandArguments, this.PackageDirectory, telemetryContext, cancellationToken, runElevated: false)) { if (!cancellationToken.IsCancellationRequested) { await this.LogProcessDetailsAsync(process, telemetryContext, "SPECcpu", logToFile: true); + this.LogSpecCpuStep(telemetryContext, "RunCompleted", ("configurationFile", configurationFile), ("exitCode", process.ExitCode), ("durationMs", (DateTime.UtcNow - runStart).TotalMilliseconds)); process.ThrowIfWorkloadFailed(); + stage = "Metrics"; await this.CaptureMetricsAsync(process, commandLineArguments, telemetryContext, cancellationToken); } } } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + this.LogSpecCpuStep(telemetryContext, "Cancelled", ("stage", stage)); + throw; + } + catch (Exception exc) + { + this.LogSpecCpuFailure(telemetryContext, stage, exc); + throw; + } finally { if (!cancellationToken.IsCancellationRequested) @@ -224,29 +263,48 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel /// protected override async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken) { - DependencyPath workloadPackage = await this.packageManager.GetPackageAsync(this.PackageName, CancellationToken.None); - - if (workloadPackage == null) + string stage = "PackageLookup"; + try { - throw new DependencyException( - $"The expected package '{this.PackageName}' does not exist on the system or is not registered.", - ErrorReason.WorkloadDependencyMissing); - } + this.LogSpecCpuStep(telemetryContext, "PackageLookupStarted", ("packageName", this.PackageName)); + DependencyPath workloadPackage = await this.packageManager.GetPackageAsync(this.PackageName, CancellationToken.None); - this.PackageDirectory = workloadPackage.Path; - this.ResultsDirectory = this.Combine(this.PackageDirectory, "result"); + if (workloadPackage == null) + { + throw new DependencyException( + $"The expected package '{this.PackageName}' does not exist on the system or is not registered.", + ErrorReason.WorkloadDependencyMissing); + } - // Clean any previous results from the system. - if (this.fileSystem.Directory.Exists(this.ResultsDirectory)) - { - await this.fileSystem.Directory.DeleteAsync(this.ResultsDirectory); - this.fileSystem.Directory.CreateDirectory(this.ResultsDirectory); - } + this.PackageDirectory = workloadPackage.Path; + this.ResultsDirectory = this.Combine(this.PackageDirectory, "result"); + this.LogSpecCpuStep(telemetryContext, "PackageReady", ("packageName", this.PackageName)); + + // Clean any previous results from the system. + if (this.fileSystem.Directory.Exists(this.ResultsDirectory)) + { + await this.fileSystem.Directory.DeleteAsync(this.ResultsDirectory); + this.fileSystem.Directory.CreateDirectory(this.ResultsDirectory); + } - string imageFile = this.GetIsoFilePath(workloadPackage); - telemetryContext.AddContext(nameof(imageFile), imageFile); + stage = "Setup"; + string imageFile = this.GetIsoFilePath(workloadPackage); + telemetryContext.AddContext(nameof(imageFile), imageFile); - await this.SetupSpecCpuAsync(imageFile, telemetryContext, cancellationToken); + this.LogSpecCpuStep(telemetryContext, "SetupStarted", ("imageFile", Path.GetFileName(imageFile))); + await this.SetupSpecCpuAsync(imageFile, telemetryContext, cancellationToken); + this.LogSpecCpuStep(telemetryContext, "SetupCompleted"); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + this.LogSpecCpuStep(telemetryContext, "Cancelled", ("stage", stage)); + throw; + } + catch (Exception exc) + { + this.LogSpecCpuFailure(telemetryContext, stage, exc); + throw; + } } private string GetConfigurationFileName() @@ -295,8 +353,10 @@ private async Task SetupSpecCpuAsync(string isoFilePath, EventContext telemetryC SpecCpuState state = await this.stateManager.GetStateAsync($"{nameof(SpecCpuState)}", cancellationToken) ?? new SpecCpuState(); + this.LogSpecCpuStep(telemetryContext, "SetupStateRead", ("alreadyInitialized", state.SpecCpuInitialized)); if (!state.SpecCpuInitialized) { + this.LogSpecCpuStep(telemetryContext, "InstallationStarted"); string mountPath = this.PlatformSpecifics.Combine(this.PlatformSpecifics.GetPackagePath(), "speccpu_mount"); this.fileSystem.Directory.CreateDirectory(mountPath); @@ -331,6 +391,7 @@ private async Task SetupSpecCpuAsync(string isoFilePath, EventContext telemetryC } state.SpecCpuInitialized = true; + this.LogSpecCpuStep(telemetryContext, "InstallationCompleted"); } await this.stateManager.SaveStateAsync($"{nameof(SpecCpuState)}", state, cancellationToken); @@ -388,6 +449,7 @@ private async Task CaptureMetricsAsync(IProcessProxy process, string commandArgu outputFiles = this.fileSystem.Directory.GetFiles(this.ResultsDirectory, "CPU2017.*.txt", SearchOption.TopDirectoryOnly); } + int metricCount = 0; foreach (string file in outputFiles) { KeyValuePair results = await this.LoadResultsAsync(file, cancellationToken); @@ -397,6 +459,7 @@ private async Task CaptureMetricsAsync(IProcessProxy process, string commandArgu IList metrics = parser.Parse(); metrics.LogConsole(this.Scenario, "SPECcpu"); + metricCount += metrics.Count; metrics.ToList().ForEach(m => m.Categorization = $"{this.SpecProfile}-{this.tuning}"); foreach (Metric metric in metrics) @@ -412,6 +475,8 @@ private async Task CaptureMetricsAsync(IProcessProxy process, string commandArgu this.Tags); } } + + this.LogSpecCpuStep(telemetryContext, "MetricsCaptured", ("resultFileCount", outputFiles.Length), ("metricCount", metricCount), ("format", useCsv ? "csv" : "txt")); } } @@ -447,12 +512,11 @@ private async Task RequestLogUploadsAsync(CancellationToken cancellationToken) } } - private string GetCommandLineArguments() + private string GetCommandLineArguments(string configurationFile) { List suites = new List { "intrate", "intspeed", "fprate", "fpspeed" }; // runcpu arguments document: https://www.spec.org/cpu2017/Docs/runcpu.html#strict - string configurationFile = this.GetConfigurationFileName(); string cmd = @$"--config {configurationFile} --iterations {this.Iterations} --copies {this.Copies} --threads {this.Threads} --tune {this.tuning}"; // For linux runs we are doing reportable. For windows since not all benchmarks could be run, it will be noreportable. @@ -464,6 +528,107 @@ private string GetCommandLineArguments() return cmd; } + private async Task WritePinnedSpecCpuConfigAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + this.LogSpecCpuStep(telemetryContext, "AffinityValidationStarted", ("requestedCpuIds", this.ProcessorAffinity)); + if (this.Platform != PlatformID.Unix || + (!string.Equals(this.SpecProfile, "intrate", StringComparison.OrdinalIgnoreCase) && + !string.Equals(this.SpecProfile, "fprate", StringComparison.OrdinalIgnoreCase))) + { + throw new NotSupportedException("SPEC CPU per-copy processor affinity is supported only for Linux intrate and fprate runs."); + } + + LinuxProcessAffinityConfiguration affinityConfig = (LinuxProcessAffinityConfiguration)ProcessAffinityConfiguration.Create(this.Platform, this.ProcessorAffinity); + IReadOnlyList cores = affinityConfig.Cores; + if (cores.Count != this.Copies) + { + throw new ArgumentException($"ProcessorAffinity must specify exactly one distinct CPU per SPEC copy (Copies={this.Copies}, CPUs={cores.Count}).", nameof(this.ProcessorAffinity)); + } + + bool useCopyNumber = cores.Select((core, copy) => core == copy).All(matches => matches); + string copyCpu = useCopyNumber ? "$SPECCOPYNUM" : "$BIND"; + this.LogSpecCpuStep(telemetryContext, "AffinityValidated", ("cpuIds", string.Join(",", cores)), ("mapping", copyCpu)); + + this.LogSpecCpuStep(telemetryContext, "NumactlCheckStarted"); + try + { + using (IProcessProxy process = this.systemManager.ProcessManager.CreateProcess("numactl", "--version", this.PackageDirectory)) + { + await process.StartAndWaitAsync(cancellationToken); + if (!cancellationToken.IsCancellationRequested && process.IsErrored()) + { + throw new DependencyException("SPEC CPU processor affinity requires a working numactl installation.", ErrorReason.WorkloadDependencyMissing); + } + + this.LogSpecCpuStep(telemetryContext, "NumactlChecked", ("exitCode", process.ExitCode)); + } + } + catch (Win32Exception exc) + { + throw new DependencyException("SPEC CPU processor affinity requires numactl to be installed.", exc, ErrorReason.WorkloadDependencyMissing); + } + + (string executable, string arguments) = affinityConfig.GetAffinityProcessInfo("true"); + this.LogSpecCpuStep(telemetryContext, "AffinityPreflightStarted", ("arguments", $"--localalloc {arguments}")); + using (IProcessProxy process = this.systemManager.ProcessManager.CreateProcess(executable, $"--localalloc {arguments}", this.PackageDirectory)) + { + await process.StartAndWaitAsync(cancellationToken); + if (!cancellationToken.IsCancellationRequested && process.IsErrored()) + { + throw new ArgumentException($"ProcessorAffinity contains CPUs that cannot be bound with numactl --localalloc. {process.StandardError}", nameof(this.ProcessorAffinity)); + } + + this.LogSpecCpuStep(telemetryContext, "AffinityPreflightCompleted", ("exitCode", process.ExitCode)); + } + + string configurationFile = this.GetConfigurationFileName(); + string configurationPath = this.Combine(this.PackageDirectory, "config", configurationFile); + string configText = await this.fileSystem.File.ReadAllTextAsync(configurationPath); + const string rateSection = "intrate,fprate:"; + if (!configText.Contains(rateSection, StringComparison.OrdinalIgnoreCase)) + { + throw new WorkloadException($"SPEC CPU configuration '{configurationPath}' is missing the '{rateSection}' section required for processor affinity."); + } + + string bind = useCopyNumber ? string.Empty : $" bind = {string.Join(",", cores)}\n"; + string submit = $" submit = numactl --localalloc --physcpubind={copyCpu} -- $command"; + configText = configText.Replace(rateSection, $"{rateSection}\n{bind}{submit}", StringComparison.OrdinalIgnoreCase); + + string configId = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join(",", cores)))).Substring(0, 16); + string pinnedConfigFile = $"{Path.GetFileNameWithoutExtension(configurationFile)}-affinity-{configId}.cfg"; + await this.fileSystem.File.WriteAllTextAsync(this.Combine(this.PackageDirectory, "config", pinnedConfigFile), configText, cancellationToken); + this.LogSpecCpuStep(telemetryContext, "ConfigGenerated", ("configurationFile", pinnedConfigFile), ("cpuIds", string.Join(",", cores)), ("submit", submit)); + return pinnedConfigFile; + } + + private void LogSpecCpuStep(EventContext telemetryContext, string step, params (string Name, object Value)[] details) + { + EventContext stepContext = telemetryContext.Clone() + .AddContext("scenario", this.Scenario ?? string.Empty) + .AddContext("specProfile", this.SpecProfile ?? string.Empty) + .AddContext("benchmarks", this.Benchmarks ?? string.Empty) + .AddContext("copies", this.Copies) + .AddContext("pinningEnabled", !string.IsNullOrWhiteSpace(this.ProcessorAffinity)); + + foreach ((string name, object value) in details) + { + stepContext.AddContext(name, value); + } + + this.Logger.LogMessage($"{this.TypeName}.{step}", LogLevel.Information, stepContext); + } + + private void LogSpecCpuFailure(EventContext telemetryContext, string stage, Exception exception) + { + EventContext errorContext = telemetryContext.Clone() + .AddContext("scenario", this.Scenario ?? string.Empty) + .AddContext("specProfile", this.SpecProfile ?? string.Empty) + .AddContext("stage", stage) + .AddError(exception); + + this.Logger.LogMessage($"{this.TypeName}.Failed", LogLevel.Error, errorContext); + } + private async Task WriteSpecCpuConfigAsync(CancellationToken cancellationToken) { // Copy SPECcpu configuration file to the config folder. diff --git a/src/VirtualClient/VirtualClient.Core.UnitTests/ProcessExtensionsAffinityTests.cs b/src/VirtualClient/VirtualClient.Core.UnitTests/ProcessExtensionsAffinityTests.cs index 2d35af8ede..9510e9179d 100644 --- a/src/VirtualClient/VirtualClient.Core.UnitTests/ProcessExtensionsAffinityTests.cs +++ b/src/VirtualClient/VirtualClient.Core.UnitTests/ProcessExtensionsAffinityTests.cs @@ -57,6 +57,24 @@ public void CreateProcessWithAffinityCreatesExpectedBashCommandOnLinux() } } + [Test] + public void CreateProcessAffinityOverloadCreatesExpectedBashCommandOnLinux() + { + this.SetupDefaults(PlatformID.Unix); + + ProcessAffinityConfiguration config = ProcessAffinityConfiguration.Create(PlatformID.Unix, new[] { 0, 1, 2 }); + + using (IProcessProxy process = this.mockFixture.ProcessManager.CreateProcess( + "bash -c", + "myworkload --option1=value", + "/home/user/workdir", + config)) + { + Assert.AreEqual("bash -c \"numactl -C 0-2 myworkload --option1=value\"", process.StartInfo.FileName); + Assert.AreEqual("/home/user/workdir", process.StartInfo.WorkingDirectory); + } + } + [Test] public void CreateProcessWithAffinityThrowsOnNullConfiguration() { @@ -109,6 +127,26 @@ public void CreateElevatedProcessWithAffinityCreatesExpectedCommandOnLinux() } } + [Test] + public void CreateProcessElevatedAffinityOverloadCreatesExpectedCommandOnLinux() + { + this.SetupDefaults(PlatformID.Unix); + + ProcessAffinityConfiguration config = ProcessAffinityConfiguration.Create(PlatformID.Unix, new[] { 0, 1, 2 }); + + using (IProcessProxy process = this.mockFixture.ProcessManager.CreateProcess( + PlatformID.Unix, + "bash -c", + "myworkload --option1=value", + "/home/user/workdir", + config)) + { + Assert.AreEqual("sudo", process.StartInfo.FileName); + Assert.AreEqual("bash -c \"numactl -C 0-2 myworkload --option1=value\"", process.StartInfo.Arguments); + Assert.AreEqual("/home/user/workdir", process.StartInfo.WorkingDirectory); + } + } + [Test] public void CreateElevatedProcessWithAffinityHandlesComplexArguments() { diff --git a/src/VirtualClient/VirtualClient.Core/ProcessExtensions.cs b/src/VirtualClient/VirtualClient.Core/ProcessExtensions.cs index 01344e3da2..15df14d3f0 100644 --- a/src/VirtualClient/VirtualClient.Core/ProcessExtensions.cs +++ b/src/VirtualClient/VirtualClient.Core/ProcessExtensions.cs @@ -154,6 +154,36 @@ public static IProcessProxy CreateElevatedProcessWithAffinity(this ProcessManage } } + /// + /// Creates a Linux process with CPU affinity binding to specific cores. + /// + /// The process manager used to create the process. + /// The command to run. + /// The command line arguments to supply to the command. + /// The working directory for the command. + /// The CPU affinity configuration specifying which cores to bind to. + /// A process proxy with CPU affinity applied via numactl. + public static IProcessProxy CreateProcess(this ProcessManager processManager, string command, string arguments, string workingDir, ProcessAffinityConfiguration affinityConfig) + { + return processManager.CreateProcessWithAffinity(command, arguments, workingDir, affinityConfig); + } + + /// + /// Creates a Linux process with CPU affinity binding and elevated privileges when needed. + /// + /// The process manager used to create the process. + /// The OS platform. + /// The command to run. + /// The command line arguments to supply to the command. + /// The working directory for the command. + /// The CPU affinity configuration specifying which cores to bind to. + /// The username to use for running the command (Linux only). + /// A process proxy with CPU affinity and elevated privileges applied. + public static IProcessProxy CreateProcess(this ProcessManager processManager, PlatformID platform, string command, string arguments, string workingDir, ProcessAffinityConfiguration affinityConfig, string username = null) + { + return processManager.CreateElevatedProcessWithAffinity(platform, command, arguments, workingDir, affinityConfig, username); + } + /// /// Applies Windows CPU affinity to a running process. /// This should be called after the process has started. diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json index 54aec9fd27..cc533692af 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json @@ -10,7 +10,7 @@ "RecommendedMinimumExecutionTime": "(4-cores)=02:00:00,(16-cores)=05:00:00,(64-cores)=10:00:00", "SupportedPlatforms": "linux-x64,linux-arm64,win-x64,win-arm64", "SupportedOperatingSystems": "Azure Linux,CentOS,Debian,RedHat,Suse,Ubuntu,Windows", - "Notes": "Using a private package 'speccpu.2017.1.1.9-danielbowers.zip' to enable support for speccpu on Ubuntu 24. SPEC CPU 2017 was not initially designed for win-arm64.", + "Notes": "Using a private package 'speccpu.2017.1.1.9-danielbowers.zip' to enable support for speccpu on Ubuntu 24. SPEC CPU 2017 was not initially designed for win-arm64. Optional ProcessorAffinity is Linux-only: provide one distinct CPU ID per copy (e.g. 0-3 for Copies=4), install numactl, or leave empty to disable pinning.", "Limitations": "The following benchmarks don't work on Windows: 507, 511, 521, 526, 527, 538" }, "Parameters": { @@ -19,6 +19,7 @@ "RunPeak": false, "Threads": "{LogicalCoreCount}", "Copies": "{LogicalCoreCount}", + "ProcessorAffinity": "", "BaseOptimizingFlags": "-g -O3 -march={calculate((\"{Platform}\".StartsWith(\"win\") || \"{Architecture}\" == \"x64\") ? \"native\" : \"armv8-a\")} -frecord-gcc-switches", "PeakOptimizingFlags": "-g -Ofast -march={calculate((\"{Platform}\".StartsWith(\"win\") || \"{Architecture}\" == \"x64\") ? \"native\" : \"armv8-a\")} -flto -frecord-gcc-switches" }, @@ -34,6 +35,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } @@ -49,6 +51,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -65,6 +68,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } @@ -80,6 +84,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } @@ -95,6 +100,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -111,6 +117,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } @@ -126,6 +133,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -142,6 +150,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -158,6 +167,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -174,6 +184,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -190,6 +201,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } @@ -205,6 +217,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } @@ -220,6 +233,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json index 844f93d5db..8a2fca8740 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json @@ -10,7 +10,7 @@ "RecommendedMinimumExecutionTime": "(8-cores)=04:00:00,(16-cores)=08:00:00,(64-cores)=10:00:00", "SupportedPlatforms": "linux-x64,linux-arm64,win-x64", "SupportedOperatingSystems": "Azure Linux,CentOS,Debian,RedHat,Suse,Ubuntu,Windows", - "Notes": "Using a private package 'speccpu.2017.1.1.9-danielbowers.zip' to enable support for speccpu on Ubuntu 24. SPEC CPU 2017 was not initially designed for win-arm64.", + "Notes": "Using a private package 'speccpu.2017.1.1.9-danielbowers.zip' to enable support for speccpu on Ubuntu 24. SPEC CPU 2017 was not initially designed for win-arm64. Optional ProcessorAffinity is Linux-only: provide one distinct CPU ID per copy (e.g. 0-3 for Copies=4), install numactl, or leave empty to disable pinning.", "Limitations": "The following benchmarks don't work on Windows: 500, 502, 520, 523, 531" }, "Parameters": { @@ -19,6 +19,7 @@ "RunPeak": false, "Threads": "{LogicalCoreCount}", "Copies": "{LogicalCoreCount}", + "ProcessorAffinity": "", "BaseOptimizingFlags": "-g -O3 -march={calculate((\"{Platform}\".StartsWith(\"win\") || \"{Architecture}\" == \"x64\") ? \"native\" : \"armv8-a\")} -frecord-gcc-switches", "PeakOptimizingFlags": "-g -Ofast -march={calculate((\"{Platform}\".StartsWith(\"win\") || \"{Architecture}\" == \"x64\") ? \"native\" : \"armv8-a\")} -flto -frecord-gcc-switches" }, @@ -34,6 +35,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -50,6 +52,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -66,6 +69,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } @@ -81,6 +85,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -97,6 +102,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -113,6 +119,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } @@ -128,6 +135,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", "SupportedPlatforms": "linux-x64,linux-arm64" @@ -144,6 +152,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } @@ -159,6 +168,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } @@ -174,6 +184,7 @@ "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", "Copies": "$.Parameters.Copies", + "ProcessorAffinity": "$.Parameters.ProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } From 5aac2ec952ac889b38342e0038afe35d555c1544 Mon Sep 17 00:00:00 2001 From: "Samuel Filho (WIPRO LIMITED)" Date: Wed, 14 Oct 2026 19:28:17 -0300 Subject: [PATCH 2/9] Address all reviewer comments and add optional per-copy CPU pinning for Linux SPEC CPU 2017 rate profiles. Validate CPU selection, capture NUMA diagnostics, add tests, and document default, single-node, and cross-node usage. --- .../SpecCpuProfileTests.cs | 41 +++- .../SPEC/SpecCpu2017ExecutorTests.cs | 128 ++++++++++- .../SPECcpu/SpecCpuExecutor.cs | 209 +++++++++++++++++- .../Logging/MetricsCsvFileLoggerTests.cs | 27 +++ .../profiles/PERF-SPECCPU-FPRATE.json | 200 +---------------- .../profiles/PERF-SPECCPU-INTRATE.json | 151 +------------ .../workloads/speccpu/speccpu-profiles.md | 12 +- 7 files changed, 397 insertions(+), 371 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs index 35540bd19a..3e87a6bc96 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs @@ -47,24 +47,41 @@ public void SpecCpuWorkloadProfileParametersAreInlinedCorrectly(string profile) } [Test] - [TestCase("PERF-SPECCPU-INTRATE.json")] - [TestCase("PERF-SPECCPU-FPRATE.json")] - public async Task SpecCpuRateProfilesForwardProcessorAffinityToEveryAction(string profileName) + [TestCase("PERF-SPECCPU-INTRATE.json", PlatformID.Unix, "intrate", null)] + [TestCase("PERF-SPECCPU-INTRATE.json", PlatformID.Unix, "intrate", "2,4,8,10")] + [TestCase("PERF-SPECCPU-INTRATE.json", PlatformID.Win32NT, "505 525 541 548 557", null)] + [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Unix, "fprate", null)] + [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Unix, "fprate", "2,4,8,10")] + [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Win32NT, "503 508 510 519 544 549 554", null)] + public async Task SpecCpuRateProfilesRunTheFullSuiteWithOptionalProcessorAffinity( + string profileName, + PlatformID platform, + string expectedBenchmarks, + string processorAffinity) { - this.mockFixture.Setup(PlatformID.Unix); + this.mockFixture.Setup(platform); ExecutionProfile profile = await ExecutionProfile.ReadProfileAsync(Path.Combine(MockFixture.TestResourcesDirectory, "profiles", profileName)); - profile.Parameters["ProcessorAffinity"] = "2,4,8,10"; + if (processorAffinity != null) + { + profile.Parameters["ProcessorAffinity"] = processorAffinity; + } + profile.Parameters["Copies"] = 4; + + Assert.AreEqual(1, profile.Actions.Count, "A SPEC CPU rate suite must run as one action, not separate benchmarks."); + ExecutionProfileElement action = profile.Actions.Single(); + Assert.AreEqual("SpecCpuExecutor", action.Type); + Assert.AreEqual("ExecuteSPECBenchmark", action.Parameters["Scenario"]); + Assert.AreEqual("$.Parameters.Benchmarks", action.Parameters["Benchmarks"], "The suite action must use the platform-specific benchmark selection."); + Assert.IsFalse(profile.Actions.Any(item => item.Parameters["Scenario"].ToString().StartsWith("benchmark_", StringComparison.OrdinalIgnoreCase))); + profile.Inline(); + await ProfileExpressionEvaluator.Instance.EvaluateAsync(this.mockFixture.Dependencies, action.Parameters); - List actions = profile.Actions.Where(action => action.Type == "SpecCpuExecutor").ToList(); - Assert.IsNotEmpty(actions); - foreach (ExecutionProfileElement action in actions) - { - Assert.AreEqual("2,4,8,10", action.Parameters["ProcessorAffinity"]); - Assert.AreEqual("4", action.Parameters["Copies"].ToString()); - } + Assert.AreEqual(expectedBenchmarks, action.Parameters["Benchmarks"]); + Assert.AreEqual(processorAffinity ?? string.Empty, action.Parameters["ProcessorAffinity"]); + Assert.AreEqual("4", action.Parameters["Copies"].ToString()); } [Test] diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs index 70d5f3a65b..29437cf22f 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs @@ -10,6 +10,7 @@ namespace VirtualClient.Actions using System.IO.Abstractions; using System.Linq; using System.Reflection; + using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -639,6 +640,129 @@ public async Task SpecCpu2017ExecutorPinsRateCopies(string profile, string cpuId Assert.IsTrue(commands.Any(command => command.Contains($"--config {Path.GetFileName(pinnedPath)}"))); } + [TestCase("", false, Architecture.X64)] + [TestCase("0-3", true, Architecture.X64)] + [TestCase("0-3", true, Architecture.Arm64)] + public async Task SpecCpu2017ExecutorPreservesSuiteTelemetryAndMetadataWithOptionalAffinity(string processorAffinity, bool pinned, Architecture architecture) + { + this.SetupLinux(architecture); + this.SetupInstalledAffinityRun(processorAffinity); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.Scenario)] = "ExecuteSPECBenchmark"; + string topology = "available: 2 nodes (0-1)\nnode 0 cpus: 0 1\nnode 0 size: 16000 MB\nnode 0 free: 12000 MB\nnode 1 cpus: 2 3\nnode 1 size: 16000 MB\nnode 1 free: 11000 MB"; + + string configurationFile = null; + this.mockFixture.File.Setup(f => f.WriteAllTextAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((path, content, token) => configurationFile = Path.GetFileName(path)) + .Returns(Task.CompletedTask); + + List commands = new List(); + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + commands.Add($"{exe} {arguments}"); + return new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, + StandardOutput = new ConcurrentBuffer(new StringBuilder(exe == "numactl" && arguments == "--hardware" ? topology : string.Empty)), + ExitCode = 0, + OnStart = () => true, + OnHasExited = () => true + }; + }; + + using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await executor.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + } + + string expectedConfig = pinned ? configurationFile : $"vc-linux-{(architecture == Architecture.Arm64 ? "arm64" : "x64")}-2017.cfg"; + Assert.AreEqual(pinned, commands.Any(command => command == "numactl --version")); + Assert.AreEqual(pinned, commands.Any(command => command == "numactl --hardware")); + Assert.AreEqual(1, commands.Count(command => command.StartsWith("bash runspeccpu.sh", StringComparison.Ordinal))); + Assert.IsTrue(commands.Any(command => command.Contains($"--config {expectedConfig}") && command.EndsWith("--reportable intrate\""))); + + var topologyEvents = this.mockFixture.Logger.Where(entry => entry.Item2.Name.EndsWith(".NumactlTopology", StringComparison.Ordinal)).ToList(); + Assert.AreEqual(pinned ? 1 : 0, topologyEvents.Count); + if (pinned) + { + EventContext topologyContext = (EventContext)topologyEvents.Single().Item3; + Assert.AreEqual("0,1,2,3", topologyContext.Properties["cpuIds"]); + Assert.AreEqual(true, topologyContext.Properties["available"]); + Assert.AreEqual(topology, topologyContext.Properties["topology"]); + } + + EventContext runContext = (EventContext)this.mockFixture.Logger.Single(entry => entry.Item2.Name.EndsWith(".RunStarted", StringComparison.Ordinal)).Item3; + Assert.AreEqual("ExecuteSPECBenchmark", runContext.Properties["scenario"]); + Assert.AreEqual(pinned, runContext.Properties["pinningEnabled"]); + Assert.AreEqual(expectedConfig, runContext.Properties["configurationFile"]); + + EventContext metricsContext = (EventContext)this.mockFixture.Logger.Single(entry => entry.Item2.Name.EndsWith(".MetricsCaptured", StringComparison.Ordinal)).Item3; + IDictionary metadata = (IDictionary)metricsContext.Properties["metadata_scenario"]; + Assert.AreEqual("SPECcpu/intrate", metadata["toolName"]); + StringAssert.Contains($"--config {expectedConfig}", metadata["toolArguments"].ToString()); + + var numaMetrics = this.mockFixture.Logger.Where(entry => entry.Item2.Name == "SPECcpu.ScenarioResult").ToList(); + Assert.AreEqual(pinned ? 6 : 0, numaMetrics.Count); + if (pinned) + { + EventContext cpuMetric = (EventContext)numaMetrics.Single(entry => ((EventContext)entry.Item3).Properties["metricName"].Equals("node-cpus-0")).Item3; + Assert.AreEqual(2, cpuMetric.Properties["metricValue"]); + Assert.AreEqual("count", cpuMetric.Properties["metricUnit"]); + Assert.AreEqual("NUMA-diagnostic", cpuMetric.Properties["metricCategorization"]); + Assert.AreEqual("ExecuteSPECBenchmark", cpuMetric.Properties["scenarioName"]); + IDictionary cpuMetadata = (IDictionary)cpuMetric.Properties["metadata_metrics"]; + Assert.AreEqual("0,1", cpuMetadata["cpuIds"]); + Assert.AreEqual("0,1", cpuMetadata["selectedCpuIds"]); + Assert.AreEqual("0,1,2,3", cpuMetadata["processorAffinity"]); + Assert.AreEqual("physcpubind", cpuMetadata["processorBinding"]); + Assert.AreEqual("localalloc", cpuMetadata["numaMemoryPolicy"]); + Assert.AreEqual("5", cpuMetric.Properties["metricVerbosity"]); + EventContext secondNodeCpuMetric = (EventContext)numaMetrics.Single(entry => ((EventContext)entry.Item3).Properties["metricName"].Equals("node-cpus-1")).Item3; + IDictionary secondNodeMetadata = (IDictionary)secondNodeCpuMetric.Properties["metadata_metrics"]; + Assert.AreEqual("2,3", secondNodeMetadata["cpuIds"]); + Assert.AreEqual("2,3", secondNodeMetadata["selectedCpuIds"]); + + EventContext sizeMetric = (EventContext)numaMetrics.Single(entry => ((EventContext)entry.Item3).Properties["metricName"].Equals("node-size-0")).Item3; + Assert.AreEqual(16000, sizeMetric.Properties["metricValue"]); + Assert.AreEqual("MB", sizeMetric.Properties["metricUnit"]); + EventContext freeMetric = (EventContext)numaMetrics.Single(entry => ((EventContext)entry.Item3).Properties["metricName"].Equals("node-free-1")).Item3; + Assert.AreEqual(11000, freeMetric.Properties["metricValue"]); + Assert.AreEqual(cpuMetadata["topologyCapturedAt"], ((IDictionary)freeMetric.Properties["metadata_metrics"])["topologyCapturedAt"]); + Assert.Less(this.mockFixture.Logger.IndexOf(numaMetrics.Last()), this.mockFixture.Logger.FindIndex(entry => entry.Item2.Name.EndsWith(".MetricsCaptured", StringComparison.Ordinal))); + } + } + + [TestCase("", 1)] + [TestCase("available: 2 nodes (0-1)\nnode 0 cpus: 0 1\nnode 0 size: 16000 MB\nnode 0 free: 12000 MB\nnode 1 cpus: 2 3\nnode 1 size: 16000 MB", 0)] + [TestCase("available: 1 nodes (0)\nnode 0 cpus: 8 9 10 11\nnode 0 size: 16000 MB\nnode 0 free: 12000 MB", 0)] + public async Task SpecCpu2017ExecutorReportsUnavailableTopologyWithoutBlockingTheRun(string topology, int probeExitCode) + { + this.SetupLinux(Architecture.Arm64); + this.SetupInstalledAffinityRun("0-3"); + bool ranSuite = false; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + ranSuite |= exe == "bash"; + return new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, + StandardOutput = new ConcurrentBuffer(new StringBuilder(exe == "numactl" && arguments == "--hardware" ? topology : string.Empty)), + ExitCode = exe == "numactl" && arguments == "--hardware" ? probeExitCode : 0, + OnStart = () => true, + OnHasExited = () => true + }; + }; + + using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await executor.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + } + + EventContext topologyContext = (EventContext)this.mockFixture.Logger.Single(entry => entry.Item2.Name.EndsWith(".NumactlTopology", StringComparison.Ordinal)).Item3; + Assert.AreEqual(false, topologyContext.Properties["available"]); + Assert.IsFalse(this.mockFixture.Logger.Any(entry => entry.Item2.Name == "SPECcpu.ScenarioResult")); + Assert.IsTrue(ranSuite); + } + [TestCase("0-2")] [TestCase("0,invalid,2,3")] public void SpecCpu2017ExecutorRejectsInvalidProcessorAffinity(string cpuIds) @@ -704,10 +828,10 @@ private void SetupInstalledAffinityRun(string cpuIds) this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = cpuIds; } - private void SetupLinux() + private void SetupLinux(Architecture architecture = Architecture.X64) { this.mockFixture = new MockFixture(); - this.mockFixture.Setup(PlatformID.Unix); + this.mockFixture.Setup(PlatformID.Unix, architecture); this.mockPackage = new DependencyPath("SPECcpu", this.mockFixture.PlatformSpecifics.GetPackagePath("speccpu", "1.1.8")); this.mockFixture.PackageManager.OnGetPackage().ReturnsAsync(this.mockPackage); diff --git a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs index 3ce246b22d..caafe74fd5 100644 --- a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs @@ -6,12 +6,14 @@ namespace VirtualClient.Actions using System; using System.Collections.Generic; using System.ComponentModel; + using System.Globalization; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; + using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using global::VirtualClient.Common; @@ -41,6 +43,7 @@ public class SpecCpuExecutor : VirtualClientComponent private IPackageManager packageManager; private IStateManager stateManager; private ISystemManagement systemManager; + private NumaTopologySnapshot numaTopology; private string tuning; /// @@ -233,6 +236,7 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel protected async Task ExecuteSpecCpuAsync(EventContext telemetryContext, CancellationToken cancellationToken) { string stage = "Configuration"; + this.numaTopology = null; try { using (BackgroundOperations profiling = BackgroundOperations.BeginProfiling(this, cancellationToken)) @@ -279,6 +283,9 @@ protected async Task ExecuteSpecCpuAsync(EventContext telemetryContext, Cancella process.ThrowIfWorkloadFailed(); stage = "Metrics"; + this.MetadataContract.AddForScenario($"SPECcpu/{this.SpecProfile}", process.FullCommand(), toolVersion: null); + this.MetadataContract.Apply(telemetryContext); + await this.CaptureNumaMetricsAsync(commandLineArguments, telemetryContext, cancellationToken); await this.CaptureMetricsAsync(process, commandLineArguments, telemetryContext, cancellationToken); } } @@ -621,6 +628,49 @@ private async Task WindowsSetupAsync( } } + private Task CaptureNumaMetricsAsync(string commandArguments, EventContext telemetryContext, CancellationToken cancellationToken) + { + if (this.numaTopology == null || cancellationToken.IsCancellationRequested) + { + return Task.CompletedTask; + } + + string affinity = string.Join(",", this.numaTopology.SelectedCpus); + foreach (NumaNode node in this.numaTopology.Nodes) + { + Metric[] metrics = + [ + new Metric($"node-cpus-{node.Id}", node.Cpus.Count, "count"), + new Metric($"node-size-{node.Id}", node.SizeMb.Value, "MB"), + new Metric($"node-free-{node.Id}", node.FreeMb.Value, "MB") + ]; + + foreach (Metric metric in metrics) + { + metric.Categorization = "NUMA-diagnostic"; + metric.Verbosity = 5; + metric.Metadata["cpuIds"] = string.Join(",", node.Cpus); + metric.Metadata["selectedCpuIds"] = string.Join(",", this.numaTopology.SelectedCpus.Where(node.Cpus.Contains)); + metric.Metadata["processorAffinity"] = affinity; + metric.Metadata["processorBinding"] = "physcpubind"; + metric.Metadata["numaMemoryPolicy"] = "localalloc"; + metric.Metadata["topologyCapturedAt"] = this.numaTopology.CapturedAt.ToString("o", CultureInfo.InvariantCulture); + + this.Logger.LogMetric( + metric, + toolName: "SPECcpu", + scenarioName: this.Scenario, + this.numaTopology.CapturedAt, + this.numaTopology.CapturedAt, + commandArguments, + telemetryContext, + this.Tags); + } + } + + return Task.CompletedTask; + } + private async Task CaptureMetricsAsync( IProcessProxy process, string commandArguments, @@ -629,13 +679,6 @@ private async Task CaptureMetricsAsync( { if (!cancellationToken.IsCancellationRequested) { - this.MetadataContract.AddForScenario( - $"SPECcpu/{this.SpecProfile}", - process.FullCommand(), - toolVersion: null); - - this.MetadataContract.Apply(telemetryContext); - bool useCsv = string.Equals(this.FeatureFlag, SpecCpuExecutor.FeatureFlagUseCsvResults, StringComparison.OrdinalIgnoreCase); string extension = useCsv ? "csv" : "txt"; string[] outputFiles = this.fileSystem.Directory.GetFiles( @@ -761,6 +804,36 @@ private async Task WritePinnedSpecCpuConfigAsync(EventContext telemetryC throw new DependencyException("SPEC CPU processor affinity requires numactl to be installed.", exc, ErrorReason.WorkloadDependencyMissing); } + try + { + using (IProcessProxy process = this.systemManager.ProcessManager.CreateProcess("numactl", "--hardware", this.PackageDirectory)) + { + await process.StartAndWaitAsync(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + string topology = process.StandardOutput.ToString().Trim(); + IReadOnlyList nodes = null; + bool available = !process.IsErrored() && SpecCpuExecutor.TryParseNumaTopology(topology, cores, out nodes); + if (available) + { + this.numaTopology = new NumaTopologySnapshot(nodes, cores.ToArray(), DateTime.UtcNow); + } + + this.LogSpecCpuStep( + telemetryContext, + "NumactlTopology", + ("cpuIds", string.Join(",", cores)), + ("topology", topology), + ("available", available), + ("exitCode", process.ExitCode), + ("error", process.IsErrored() ? process.StandardError.ToString().Trim() : available ? string.Empty : "Unable to parse NUMA topology for the selected CPUs.")); + } + } + catch (Win32Exception exc) + { + this.LogSpecCpuStep(telemetryContext, "NumactlTopology", ("cpuIds", string.Join(",", cores)), ("available", false), ("error", exc.Message)); + } + (string executable, string arguments) = affinityConfig.GetAffinityProcessInfo("true"); this.LogSpecCpuStep(telemetryContext, "AffinityPreflightStarted", ("arguments", $"--localalloc {arguments}")); using (IProcessProxy process = this.systemManager.ProcessManager.CreateProcess(executable, $"--localalloc {arguments}", this.PackageDirectory)) @@ -794,6 +867,96 @@ private async Task WritePinnedSpecCpuConfigAsync(EventContext telemetryC return pinnedConfigFile; } + private static bool TryParseNumaTopology(string topology, IReadOnlyList selectedCpus, out IReadOnlyList parsedNodes) + { + parsedNodes = null; + Match header = Regex.Match(topology, @"(?m)^available:[ \t]*(?\d+)[ \t]+nodes\b"); + if (!header.Success || !int.TryParse(header.Groups["count"].Value, out int nodeCount) || nodeCount == 0) + { + return false; + } + + Dictionary nodes = new Dictionary(); + MatchCollection entries = Regex.Matches(topology, @"(?m)^node[ \t]+(?\d+)[ \t]+(?cpus|size|free):[ \t]*(?[^\r\n]*)"); + foreach (Match entry in entries) + { + if (!int.TryParse(entry.Groups["id"].Value, out int nodeId)) + { + return false; + } + + if (!nodes.TryGetValue(nodeId, out NumaNode node)) + { + node = new NumaNode(nodeId); + nodes.Add(nodeId, node); + } + + string field = entry.Groups["field"].Value; + string value = entry.Groups["value"].Value.Trim(); + if (field == "cpus") + { + if (node.Cpus != null) + { + return false; + } + + List cpus = new List(); + foreach (string cpu in value.Split((char[])null, StringSplitOptions.RemoveEmptyEntries)) + { + if (!int.TryParse(cpu, out int cpuId) || cpuId < 0) + { + return false; + } + + cpus.Add(cpuId); + } + + node.Cpus = cpus; + } + else + { + Match memory = Regex.Match(value, @"^(?\d+(?:\.\d+)?)[ \t]+MB$"); + if (!memory.Success || !double.TryParse(memory.Groups["mb"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double megabytes)) + { + return false; + } + + if (field == "size") + { + if (node.SizeMb.HasValue) + { + return false; + } + + node.SizeMb = megabytes; + } + else + { + if (node.FreeMb.HasValue) + { + return false; + } + + node.FreeMb = megabytes; + } + } + } + + if (nodes.Count != nodeCount || nodes.Values.Any(node => node.Cpus == null || !node.SizeMb.HasValue || !node.FreeMb.HasValue)) + { + return false; + } + + if (nodes.Values.SelectMany(node => node.Cpus).Distinct().Count() != nodes.Values.Sum(node => node.Cpus.Count) + || selectedCpus.Any(cpu => nodes.Values.Count(node => node.Cpus.Contains(cpu)) != 1)) + { + return false; + } + + parsedNodes = nodes.Values.OrderBy(node => node.Id).ToList(); + return true; + } + private void LogSpecCpuStep(EventContext telemetryContext, string step, params (string Name, object Value)[] details) { EventContext stepContext = telemetryContext.Clone() @@ -878,6 +1041,38 @@ public bool SpecCpuInitialized } } + private sealed class NumaNode + { + public NumaNode(int id) + { + this.Id = id; + } + + public int Id { get; } + + public List Cpus { get; set; } + + public double? SizeMb { get; set; } + + public double? FreeMb { get; set; } + } + + private sealed class NumaTopologySnapshot + { + public NumaTopologySnapshot(IReadOnlyList nodes, IReadOnlyList selectedCpus, DateTime capturedAt) + { + this.Nodes = nodes; + this.SelectedCpus = selectedCpus; + this.CapturedAt = capturedAt; + } + + public IReadOnlyList Nodes { get; } + + public IReadOnlyList SelectedCpus { get; } + + public DateTime CapturedAt { get; } + } + private static class SpecCpuConfigPlaceholder { public const string BaseOptimizingFlags = "$BaseOptimizingFlags$"; diff --git a/src/VirtualClient/VirtualClient.Core.UnitTests/Logging/MetricsCsvFileLoggerTests.cs b/src/VirtualClient/VirtualClient.Core.UnitTests/Logging/MetricsCsvFileLoggerTests.cs index 63ab50d5c8..c85cdd7da7 100644 --- a/src/VirtualClient/VirtualClient.Core.UnitTests/Logging/MetricsCsvFileLoggerTests.cs +++ b/src/VirtualClient/VirtualClient.Core.UnitTests/Logging/MetricsCsvFileLoggerTests.cs @@ -58,6 +58,33 @@ public void MetricsCsvFileLoggerUsesTheExpectedColumnNamesForTheCsvFiles() CollectionAssert.AreEqual(expectedColumnHeaders, MetricsCsvFileLogger.CsvFields.Select(field => field.ColumnName)); } + [Test] + public void MetricsCsvFileLoggerDoesNotExportNumaCpuListsFromMetricMetadata() + { + EventContext context = new EventContext(Guid.NewGuid(), new Dictionary + { + ["toolName"] = "SPECcpu", + ["scenarioName"] = "ExecuteSPECBenchmark", + ["metricName"] = "node-cpus-0", + ["metricValue"] = 4, + ["metricUnit"] = "count", + ["metadata_metrics"] = new Dictionary + { + ["cpuIds"] = "0,1,48,49", + ["selectedCpuIds"] = "0,48", + ["processorAffinity"] = "0,1,48,49" + } + }); + + string csv = MetricsCsvFileLogger.CreateMessage(context); + + StringAssert.Contains("\"SPECcpu\"", csv); + StringAssert.Contains("\"node-cpus-0\",\"4\",\"count\"", csv); + StringAssert.DoesNotContain("0,1,48,49", csv); + StringAssert.DoesNotContain("0,48", csv); + StringAssert.DoesNotContain("cpuIds", csv); + } + [Test] public void MetricsCsvFileLoggerWritesTheExpectedCsvToFiles_1() { diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json index a9e6d99fb1..9c434beb85 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json @@ -31,205 +31,7 @@ "Scenario": "ExecuteSPECBenchmark", "Iterations": "$.Parameters.Iterations", "SpecProfile": "fprate", - "Benchmarks": "503", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_507.cactuBSSN_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "507", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_508.namd_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "508", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_510.parest_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "510", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_511.povray_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "511", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_519.lbm_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "519", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_521.wrf_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "521", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_526.blender_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "526", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_527.cam4_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "527", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_538.imagick_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "538", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_544.nab_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "544", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_549.fotonik3d_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "549", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_554.roms_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "fprate", - "Benchmarks": "554", + "Benchmarks": "$.Parameters.Benchmarks", "PackageName": "speccpu2017", "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json index 37f787a18e..cadc262bcd 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json @@ -31,156 +31,7 @@ "Scenario": "ExecuteSPECBenchmark", "Iterations": "$.Parameters.Iterations", "SpecProfile": "intrate", - "Benchmarks": "500", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_502.gcc_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "intrate", - "Benchmarks": "502", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_505.mcf_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "intrate", - "Benchmarks": "505", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_520.omnetpp_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "intrate", - "Benchmarks": "520", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_523.xalancbmk_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "intrate", - "Benchmarks": "523", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_525.x264_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "intrate", - "Benchmarks": "525", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_531.deepsjeng_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "intrate", - "Benchmarks": "531", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags", - "SupportedPlatforms": "linux-x64,linux-arm64" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_541.leela_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "intrate", - "Benchmarks": "541", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_548.exchange2_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "intrate", - "Benchmarks": "548", - "PackageName": "speccpu2017", - "RunPeak": "$.Parameters.RunPeak", - "Threads": "$.Parameters.Threads", - "Copies": "$.Parameters.Copies", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", - "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", - "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" - } - }, - { - "Type": "SpecCpuExecutor", - "Parameters": { - "Scenario": "benchmark_557.xz_r", - "Iterations": "$.Parameters.Iterations", - "SpecProfile": "intrate", - "Benchmarks": "557", + "Benchmarks": "$.Parameters.Benchmarks", "PackageName": "speccpu2017", "RunPeak": "$.Parameters.RunPeak", "Threads": "$.Parameters.Threads", diff --git a/website/docs/workloads/speccpu/speccpu-profiles.md b/website/docs/workloads/speccpu/speccpu-profiles.md index 4480cf1e9a..a391f704d4 100644 --- a/website/docs/workloads/speccpu/speccpu-profiles.md +++ b/website/docs/workloads/speccpu/speccpu-profiles.md @@ -44,6 +44,7 @@ for evaluating the performance of the CPU for processing calculations. | RunPeak | Optional. True to run the workload 'Peak' scenario, False to run the workload 'Base' scenario. | false (Base) | | Threads | Optional. Determines the number of threads to use for running the benchmark. | # logical processors | | Copies | Optional. Determines the number of copies of the benchmark to run concurrently. | # logical processors | + | ProcessorAffinity | Optional. Linux SPECrate only (intrate/fprate). Assign one distinct CPU ID to each copy; requires `numactl`. Leave empty to run without pinning. | empty (no pinning) | | BaseOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Base' scenario |
  • X64 systems: -g -O2 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O2 -march=armv8.2-a -flto -frecord-gcc-switches
| | PeakOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Peak' scenario |
  • X64 systems: -g -O3 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O3 -march=armv8.2-a -flto -frecord-gcc-switches
| @@ -64,6 +65,7 @@ for evaluating the performance of the CPU for processing calculations. * **Notes** On Windows, only the following benchmarks are supported: 503.bwaves_r, 508.namd_r, 510.parest_r, 519.lbm_r, 544.nab_r, 549.fotonik3d_r, 554.roms_r. + The Linux `ProcessorAffinity` examples in the INTRATE section below also apply to this profile when `PERF-SPECCPU-FPRATE.json` is used instead. ``` bash # Execute the workload profile @@ -155,6 +157,7 @@ for evaluating the performance of the CPU for processing calculations. | RunPeak | Optional. True to run the workload 'Peak' scenario, False to run the workload 'Base' scenario. | false (Base) | | Threads | Optional. Determines the number of threads to use for running the benchmark. | # logical processors | | Copies | Optional. Determines the number of copies of the benchmark to run concurrently. | # logical processors | + | ProcessorAffinity | Optional. Linux SPECrate only (intrate/fprate). Assign one distinct CPU ID to each copy; requires `numactl`. Leave empty to run without pinning. | empty (no pinning) | | BaseOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Base' scenario |
  • X64 systems: -g -O2 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O2 -march=armv8.2-a -flto -frecord-gcc-switches
| | PeakOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Peak' scenario |
  • X64 systems: -g -O3 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O3 -march=armv8.2-a -flto -frecord-gcc-switches
| @@ -174,10 +177,17 @@ for evaluating the performance of the CPU for processing calculations. * **Notes** On Windows, only the following benchmarks are supported: 505.mcf_r, 525.x264_r, 541.leela_r, 548.exchange2_r, 557.xz_r. + On Linux, check the CPU-to-node mapping with `numactl --hardware` before choosing CPU IDs. `ProcessorAffinity` must contain exactly as many distinct CPU IDs as `Copies`. Pinned copies use `numactl --localalloc` for local memory allocation. The CPU IDs below are examples; use IDs appropriate for your machine. ``` bash - # Execute the workload profile + # Empty ProcessorAffinity (default): run without pinning, as before. ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --logger=csv + + # Single NUMA node: CPUs 0-3 must all belong to the same node. + ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="Copies=4,,,ProcessorAffinity=0-3" --logger=csv + + # Two NUMA nodes: CPUs 0-1 on node 0 and 48-49 on node 1. + ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="Copies=4,,,ProcessorAffinity=0,1,48,49" --logger=csv ``` ## PERF-SPECCPU-INTSPEED.json From 3975b5fcc0952c445f8c339bf603feb3da62ec7b Mon Sep 17 00:00:00 2001 From: "Samuel Filho (WIPRO LIMITED)" Date: Mon, 19 Oct 2026 19:08:16 -0300 Subject: [PATCH 3/9] Renamed the SPEC CPU rate-profile setting from ProcessorAffinity to NumaProcessorAffinity in the executor, INTRATE/FPRATE profiles, tests, and documentation. --- .../SpecCpuProfileTests.cs | 10 +++++----- .../SPEC/SpecCpu2017ExecutorTests.cs | 10 +++++----- .../SPECcpu/SpecCpuExecutor.cs | 18 +++++++++--------- .../profiles/PERF-SPECCPU-FPRATE.json | 6 +++--- .../profiles/PERF-SPECCPU-INTRATE.json | 6 +++--- .../docs/workloads/speccpu/speccpu-profiles.md | 14 +++++++------- 6 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs index 27aa281c28..f0bef411fb 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs @@ -53,18 +53,18 @@ public void SpecCpuWorkloadProfileParametersAreInlinedCorrectly(string profile) [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Unix, "fprate", null)] [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Unix, "fprate", "2,4,8,10")] [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Win32NT, "503 508 510 519 544 549 554", null)] - public async Task SpecCpuRateProfilesRunTheFullSuiteWithOptionalProcessorAffinity( + public async Task SpecCpuRateProfilesRunTheFullSuiteWithOptionalNumaProcessorAffinity( string profileName, PlatformID platform, string expectedBenchmarks, - string processorAffinity) + string numaProcessorAffinity) { this.mockFixture.Setup(platform); ExecutionProfile profile = await ExecutionProfile.ReadProfileAsync(Path.Combine(MockFixture.TestResourcesDirectory, "profiles", profileName)); - if (processorAffinity != null) + if (numaProcessorAffinity != null) { - profile.Parameters["ProcessorAffinity"] = processorAffinity; + profile.Parameters["NumaProcessorAffinity"] = numaProcessorAffinity; } profile.Parameters["CopyCount"] = 4; @@ -80,7 +80,7 @@ public async Task SpecCpuRateProfilesRunTheFullSuiteWithOptionalProcessorAffinit await ProfileExpressionEvaluator.Instance.EvaluateAsync(this.mockFixture.Dependencies, action.Parameters); Assert.AreEqual(expectedBenchmarks, action.Parameters["Benchmarks"]); - Assert.AreEqual(processorAffinity ?? string.Empty, action.Parameters["ProcessorAffinity"]); + Assert.AreEqual(numaProcessorAffinity ?? string.Empty, action.Parameters["NumaProcessorAffinity"]); Assert.AreEqual("4", action.Parameters["CopyCount"].ToString()); } diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs index c30bde0807..6139f41fd3 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs @@ -644,10 +644,10 @@ public async Task SpecCpu2017ExecutorPinsRateCopies(string profile, string cpuId [TestCase("", false, Architecture.X64)] [TestCase("0-3", true, Architecture.X64)] [TestCase("0-3", true, Architecture.Arm64)] - public async Task SpecCpu2017ExecutorPreservesSuiteTelemetryAndMetadataWithOptionalAffinity(string processorAffinity, bool pinned, Architecture architecture) + public async Task SpecCpu2017ExecutorPreservesSuiteTelemetryAndMetadataWithOptionalAffinity(string numaProcessorAffinity, bool pinned, Architecture architecture) { this.SetupLinux(architecture); - this.SetupInstalledAffinityRun(processorAffinity); + this.SetupInstalledAffinityRun(numaProcessorAffinity); this.mockFixture.Parameters[nameof(SpecCpuExecutor.Scenario)] = "ExecuteSPECBenchmark"; string topology = "available: 2 nodes (0-1)\nnode 0 cpus: 0 1\nnode 0 size: 16000 MB\nnode 0 free: 12000 MB\nnode 1 cpus: 2 3\nnode 1 size: 16000 MB\nnode 1 free: 11000 MB"; @@ -713,7 +713,7 @@ public async Task SpecCpu2017ExecutorPreservesSuiteTelemetryAndMetadataWithOptio IDictionary cpuMetadata = (IDictionary)cpuMetric.Properties["metadata_metrics"]; Assert.AreEqual("0,1", cpuMetadata["cpuIds"]); Assert.AreEqual("0,1", cpuMetadata["selectedCpuIds"]); - Assert.AreEqual("0,1,2,3", cpuMetadata["processorAffinity"]); + Assert.AreEqual("0,1,2,3", cpuMetadata["numaProcessorAffinity"]); Assert.AreEqual("physcpubind", cpuMetadata["processorBinding"]); Assert.AreEqual("localalloc", cpuMetadata["numaMemoryPolicy"]); Assert.AreEqual("5", cpuMetric.Properties["metricVerbosity"]); @@ -766,7 +766,7 @@ public async Task SpecCpu2017ExecutorReportsUnavailableTopologyWithoutBlockingTh [TestCase("0-2")] [TestCase("0,invalid,2,3")] - public void SpecCpu2017ExecutorRejectsInvalidProcessorAffinity(string cpuIds) + public void SpecCpu2017ExecutorRejectsInvalidNumaProcessorAffinity(string cpuIds) { this.SetupLinux(); this.SetupInstalledAffinityRun(cpuIds); @@ -826,7 +826,7 @@ private void SetupInstalledAffinityRun(string cpuIds) { State state = new State(new Dictionary { ["SpecCpuInitialized"] = true }); this.mockFixture.StateManager.OnGetState().ReturnsAsync(JObject.Parse(state.ToJson())); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.ProcessorAffinity)] = cpuIds; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinity)] = cpuIds; } private void SetupLinux(Architecture architecture = Architecture.X64) diff --git a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs index 89ee60a033..4a7ce25aa9 100644 --- a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs @@ -163,11 +163,11 @@ public int CopyCount /// /// Optional list of physical CPU IDs to assign to SPECrate copies, one CPU per copy. /// - public string ProcessorAffinity + public string NumaProcessorAffinity { get { - return this.Parameters.GetValue(nameof(SpecCpuExecutor.ProcessorAffinity), string.Empty); + return this.Parameters.GetValue(nameof(SpecCpuExecutor.NumaProcessorAffinity), string.Empty); } } @@ -242,7 +242,7 @@ protected async Task ExecuteSpecCpuAsync(EventContext telemetryContext, Cancella using (BackgroundOperations profiling = BackgroundOperations.BeginProfiling(this, cancellationToken)) { string configurationFile = this.GetConfigurationFileName(); - if (!string.IsNullOrWhiteSpace(this.ProcessorAffinity)) + if (!string.IsNullOrWhiteSpace(this.NumaProcessorAffinity)) { stage = "AffinityConfiguration"; configurationFile = await this.WritePinnedSpecCpuConfigAsync(telemetryContext, cancellationToken); @@ -651,7 +651,7 @@ private Task CaptureNumaMetricsAsync(string commandArguments, EventContext telem metric.Verbosity = 5; metric.Metadata["cpuIds"] = string.Join(",", node.Cpus); metric.Metadata["selectedCpuIds"] = string.Join(",", this.numaTopology.SelectedCpus.Where(node.Cpus.Contains)); - metric.Metadata["processorAffinity"] = affinity; + metric.Metadata["numaProcessorAffinity"] = affinity; metric.Metadata["processorBinding"] = "physcpubind"; metric.Metadata["numaMemoryPolicy"] = "localalloc"; metric.Metadata["topologyCapturedAt"] = this.numaTopology.CapturedAt.ToString("o", CultureInfo.InvariantCulture); @@ -766,7 +766,7 @@ private string GetCommandLineArguments(string configurationFile) private async Task WritePinnedSpecCpuConfigAsync(EventContext telemetryContext, CancellationToken cancellationToken) { - this.LogSpecCpuStep(telemetryContext, "AffinityValidationStarted", ("requestedCpuIds", this.ProcessorAffinity)); + this.LogSpecCpuStep(telemetryContext, "AffinityValidationStarted", ("requestedCpuIds", this.NumaProcessorAffinity)); if (this.Platform != PlatformID.Unix || (!string.Equals(this.SpecProfile, "intrate", StringComparison.OrdinalIgnoreCase) && !string.Equals(this.SpecProfile, "fprate", StringComparison.OrdinalIgnoreCase))) @@ -774,11 +774,11 @@ private async Task WritePinnedSpecCpuConfigAsync(EventContext telemetryC throw new NotSupportedException("SPEC CPU per-copy processor affinity is supported only for Linux intrate and fprate runs."); } - LinuxProcessAffinityConfiguration affinityConfig = (LinuxProcessAffinityConfiguration)ProcessAffinityConfiguration.Create(this.Platform, this.ProcessorAffinity); + LinuxProcessAffinityConfiguration affinityConfig = (LinuxProcessAffinityConfiguration)ProcessAffinityConfiguration.Create(this.Platform, this.NumaProcessorAffinity); IReadOnlyList cores = affinityConfig.Cores; if (cores.Count != this.CopyCount) { - throw new ArgumentException($"ProcessorAffinity must specify exactly one distinct CPU per SPEC copy (CopyCount={this.CopyCount}, CPUs={cores.Count}).", nameof(this.ProcessorAffinity)); + throw new ArgumentException($"NumaProcessorAffinity must specify exactly one distinct CPU per SPEC copy (CopyCount={this.CopyCount}, CPUs={cores.Count}).", nameof(this.NumaProcessorAffinity)); } bool useCopyNumber = cores.Select((core, copy) => core == copy).All(matches => matches); @@ -841,7 +841,7 @@ private async Task WritePinnedSpecCpuConfigAsync(EventContext telemetryC await process.StartAndWaitAsync(cancellationToken); if (!cancellationToken.IsCancellationRequested && process.IsErrored()) { - throw new ArgumentException($"ProcessorAffinity contains CPUs that cannot be bound with numactl --localalloc. {process.StandardError}", nameof(this.ProcessorAffinity)); + throw new ArgumentException($"NumaProcessorAffinity contains CPUs that cannot be bound with numactl --localalloc. {process.StandardError}", nameof(this.NumaProcessorAffinity)); } this.LogSpecCpuStep(telemetryContext, "AffinityPreflightCompleted", ("exitCode", process.ExitCode)); @@ -964,7 +964,7 @@ private void LogSpecCpuStep(EventContext telemetryContext, string step, params ( .AddContext("specProfile", this.SpecProfile ?? string.Empty) .AddContext("benchmarks", this.Benchmarks ?? string.Empty) .AddContext("copies", this.CopyCount) - .AddContext("pinningEnabled", !string.IsNullOrWhiteSpace(this.ProcessorAffinity)); + .AddContext("pinningEnabled", !string.IsNullOrWhiteSpace(this.NumaProcessorAffinity)); foreach ((string name, object value) in details) { diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json index 783996acc2..b5b9e95613 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json @@ -10,7 +10,7 @@ "RecommendedMinimumExecutionTime": "(4-cores)=02:00:00,(16-cores)=05:00:00,(64-cores)=10:00:00", "SupportedPlatforms": "linux-x64,linux-arm64,win-x64,win-arm64", "SupportedOperatingSystems": "AzureLinux,CentOS,Debian,OpenSuse,RedHat,Ubuntu,Windows", - "Notes": "Using a private package 'speccpu.2017.1.1.9-danielbowers.zip' to enable support for speccpu on Ubuntu 24. SPEC CPU 2017 was not initially designed for win-arm64. Optional ProcessorAffinity is Linux-only: provide one distinct CPU ID per copy (e.g. 0-3 for CopyCount=4), install numactl, or leave empty to disable pinning.", + "Notes": "Using a private package 'speccpu.2017.1.1.9-danielbowers.zip' to enable support for speccpu on Ubuntu 24. SPEC CPU 2017 was not initially designed for win-arm64. Optional NumaProcessorAffinity is Linux-only: provide one distinct CPU ID per copy (e.g. 0-3 for CopyCount=4), or leave empty to disable pinning.", "Limitations": "The following benchmarks don't work on Windows: 507, 511, 521, 526, 527, 538" }, "Parameters": { @@ -20,7 +20,7 @@ "RunPeak": false, "ThreadCount": "{LogicalCoreCount}", "CopyCount": "{LogicalCoreCount}", - "ProcessorAffinity": "", + "NumaProcessorAffinity": "", "BaseOptimizingFlags": "-O2 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches", "PeakOptimizingFlags": "-O3 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches" }, @@ -36,7 +36,7 @@ "RunPeak": "$.Parameters.RunPeak", "ThreadCount": "$.Parameters.ThreadCount", "CopyCount": "$.Parameters.CopyCount", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", + "NumaProcessorAffinity": "$.Parameters.NumaProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json index dc0fa4df9b..281f8e24d0 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json @@ -10,7 +10,7 @@ "RecommendedMinimumExecutionTime": "(8-cores)=04:00:00,(16-cores)=08:00:00,(64-cores)=10:00:00", "SupportedPlatforms": "linux-x64,linux-arm64,win-x64", "SupportedOperatingSystems": "AzureLinux,CentOS,Debian,OpenSuse,RedHat,Ubuntu,Windows", - "Notes": "Using a private package 'speccpu.2017.1.1.9-danielbowers.zip' to enable support for speccpu on Ubuntu 24. SPEC CPU 2017 was not initially designed for win-arm64. Optional ProcessorAffinity is Linux-only: provide one distinct CPU ID per copy (e.g. 0-3 for CopyCount=4), install numactl, or leave empty to disable pinning.", + "Notes": "Using a private package 'speccpu.2017.1.1.9-danielbowers.zip' to enable support for speccpu on Ubuntu 24. SPEC CPU 2017 was not initially designed for win-arm64. Optional NumaProcessorAffinity is Linux-only: provide one distinct CPU ID per copy (e.g. 0-3 for CopyCount=4), or leave empty to disable pinning.", "Limitations": "The following benchmarks don't work on Windows: 500, 502, 520, 523, 531" }, "Parameters": { @@ -20,7 +20,7 @@ "RunPeak": false, "ThreadCount": "{LogicalCoreCount}", "CopyCount": "{LogicalCoreCount}", - "ProcessorAffinity": "", + "NumaProcessorAffinity": "", "BaseOptimizingFlags": "-O2 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches", "PeakOptimizingFlags": "-O3 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches" }, @@ -36,7 +36,7 @@ "RunPeak": "$.Parameters.RunPeak", "ThreadCount": "$.Parameters.ThreadCount", "CopyCount": "$.Parameters.CopyCount", - "ProcessorAffinity": "$.Parameters.ProcessorAffinity", + "NumaProcessorAffinity": "$.Parameters.NumaProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } diff --git a/website/docs/workloads/speccpu/speccpu-profiles.md b/website/docs/workloads/speccpu/speccpu-profiles.md index 609e5bd05f..9b285be645 100644 --- a/website/docs/workloads/speccpu/speccpu-profiles.md +++ b/website/docs/workloads/speccpu/speccpu-profiles.md @@ -44,7 +44,7 @@ for evaluating the performance of the CPU for processing calculations. | RunPeak | Optional. True to run the workload 'Peak' scenario, False to run the workload 'Base' scenario. | false (Base) | | ThreadCount | Optional. Determines the number of threads to use for running the benchmark. | # logical processors | | CopyCount | Optional. Determines the number of copies of the benchmark to run concurrently. | # logical processors | - | ProcessorAffinity | Optional. Linux SPECrate only (intrate/fprate). Assign one distinct CPU ID to each copy; requires `numactl`. Leave empty to run without pinning. | empty (no pinning) | + | NumaProcessorAffinity | Optional. Linux SPECrate only (intrate/fprate). Assign one distinct CPU ID to each copy; requires `numactl`. Leave empty to run without pinning. | empty (no pinning) | | BaseOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Base' scenario |
  • X64 systems: -g -O2 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O2 -march=armv8.2-a -flto -frecord-gcc-switches
| | PeakOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Peak' scenario |
  • X64 systems: -g -O3 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O3 -march=armv8.2-a -flto -frecord-gcc-switches
| @@ -65,7 +65,7 @@ for evaluating the performance of the CPU for processing calculations. * **Notes** On Windows, only the following benchmarks are supported: 503.bwaves_r, 508.namd_r, 510.parest_r, 519.lbm_r, 544.nab_r, 549.fotonik3d_r, 554.roms_r. - The Linux `ProcessorAffinity` examples in the INTRATE section below also apply to this profile when `PERF-SPECCPU-FPRATE.json` is used instead. + The Linux `NumaProcessorAffinity` examples in the INTRATE section below also apply to this profile when `PERF-SPECCPU-FPRATE.json` is used instead. ``` bash # Execute the workload profile @@ -157,7 +157,7 @@ for evaluating the performance of the CPU for processing calculations. | RunPeak | Optional. True to run the workload 'Peak' scenario, False to run the workload 'Base' scenario. | false (Base) | | ThreadCount | Optional. Determines the number of threads to use for running the benchmark. | # logical processors | | CopyCount | Optional. Determines the number of copies of the benchmark to run concurrently. | # logical processors | - | ProcessorAffinity | Optional. Linux SPECrate only (intrate/fprate). Assign one distinct CPU ID to each copy; requires `numactl`. Leave empty to run without pinning. | empty (no pinning) | + | NumaProcessorAffinity | Optional. Linux SPECrate only (intrate/fprate). Assign one distinct CPU ID to each copy; requires `numactl`. Leave empty to run without pinning. | empty (no pinning) | | BaseOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Base' scenario |
  • X64 systems: -g -O2 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O2 -march=armv8.2-a -flto -frecord-gcc-switches
| | PeakOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Peak' scenario |
  • X64 systems: -g -O3 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O3 -march=armv8.2-a -flto -frecord-gcc-switches
| @@ -177,17 +177,17 @@ for evaluating the performance of the CPU for processing calculations. * **Notes** On Windows, only the following benchmarks are supported: 505.mcf_r, 525.x264_r, 541.leela_r, 548.exchange2_r, 557.xz_r. - On Linux, check the CPU-to-node mapping with `numactl --hardware` before choosing CPU IDs. `ProcessorAffinity` must contain exactly as many distinct CPU IDs as `CopyCount`. Pinned copies use `numactl --localalloc` for local memory allocation. The CPU IDs below are examples; use IDs appropriate for your machine. + On Linux, check the CPU-to-node mapping with `numactl --hardware` before choosing CPU IDs. `NumaProcessorAffinity` must contain exactly as many distinct CPU IDs as `CopyCount`. Pinned copies use `numactl --localalloc` for local memory allocation. The CPU IDs below are examples; use IDs appropriate for your machine. ``` bash - # Empty ProcessorAffinity (default): run without pinning, as before. + # Empty NumaProcessorAffinity (default): run without pinning, as before. ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --logger=csv # Single NUMA node: CPUs 0-3 must all belong to the same node. - ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,ProcessorAffinity=0-3" --logger=csv + ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,NumaProcessorAffinity=0-3" --logger=csv # Two NUMA nodes: CPUs 0-1 on node 0 and 48-49 on node 1. - ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,ProcessorAffinity=0,1,48,49" --logger=csv + ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,NumaProcessorAffinity=0,1,48,49" --logger=csv ``` ## PERF-SPECCPU-INTSPEED.json From 70905d7e553fbcafc5ed43b7b40fe04cedb2cbb5 Mon Sep 17 00:00:00 2001 From: "Samuel Filho (WIPRO LIMITED)" Date: Mon, 19 Oct 2026 21:53:56 -0300 Subject: [PATCH 4/9] Removed CaptureNumaMetricsAsync, its call, snapshot state, and the unit-test assertions for NUMA-diagnostic metrics. SPEC CPU will no longer emit node-cpus-*, node-size-*, or node-free-* metric rows --- .../SPEC/SpecCpu2017ExecutorTests.cs | 30 -------- .../SPECcpu/SpecCpuExecutor.cs | 69 +------------------ 2 files changed, 1 insertion(+), 98 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs index 6139f41fd3..c9a10cb210 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs @@ -700,36 +700,6 @@ public async Task SpecCpu2017ExecutorPreservesSuiteTelemetryAndMetadataWithOptio IDictionary metadata = (IDictionary)metricsContext.Properties["metadata_scenario"]; Assert.AreEqual("SPECcpu/intrate", metadata["toolName"]); StringAssert.Contains($"--config {expectedConfig}", metadata["toolArguments"].ToString()); - - var numaMetrics = this.mockFixture.Logger.Where(entry => entry.Item2.Name == "SPECcpu.ScenarioResult").ToList(); - Assert.AreEqual(pinned ? 6 : 0, numaMetrics.Count); - if (pinned) - { - EventContext cpuMetric = (EventContext)numaMetrics.Single(entry => ((EventContext)entry.Item3).Properties["metricName"].Equals("node-cpus-0")).Item3; - Assert.AreEqual(2, cpuMetric.Properties["metricValue"]); - Assert.AreEqual("count", cpuMetric.Properties["metricUnit"]); - Assert.AreEqual("NUMA-diagnostic", cpuMetric.Properties["metricCategorization"]); - Assert.AreEqual("ExecuteSPECBenchmark", cpuMetric.Properties["scenarioName"]); - IDictionary cpuMetadata = (IDictionary)cpuMetric.Properties["metadata_metrics"]; - Assert.AreEqual("0,1", cpuMetadata["cpuIds"]); - Assert.AreEqual("0,1", cpuMetadata["selectedCpuIds"]); - Assert.AreEqual("0,1,2,3", cpuMetadata["numaProcessorAffinity"]); - Assert.AreEqual("physcpubind", cpuMetadata["processorBinding"]); - Assert.AreEqual("localalloc", cpuMetadata["numaMemoryPolicy"]); - Assert.AreEqual("5", cpuMetric.Properties["metricVerbosity"]); - EventContext secondNodeCpuMetric = (EventContext)numaMetrics.Single(entry => ((EventContext)entry.Item3).Properties["metricName"].Equals("node-cpus-1")).Item3; - IDictionary secondNodeMetadata = (IDictionary)secondNodeCpuMetric.Properties["metadata_metrics"]; - Assert.AreEqual("2,3", secondNodeMetadata["cpuIds"]); - Assert.AreEqual("2,3", secondNodeMetadata["selectedCpuIds"]); - - EventContext sizeMetric = (EventContext)numaMetrics.Single(entry => ((EventContext)entry.Item3).Properties["metricName"].Equals("node-size-0")).Item3; - Assert.AreEqual(16000, sizeMetric.Properties["metricValue"]); - Assert.AreEqual("MB", sizeMetric.Properties["metricUnit"]); - EventContext freeMetric = (EventContext)numaMetrics.Single(entry => ((EventContext)entry.Item3).Properties["metricName"].Equals("node-free-1")).Item3; - Assert.AreEqual(11000, freeMetric.Properties["metricValue"]); - Assert.AreEqual(cpuMetadata["topologyCapturedAt"], ((IDictionary)freeMetric.Properties["metadata_metrics"])["topologyCapturedAt"]); - Assert.Less(this.mockFixture.Logger.IndexOf(numaMetrics.Last()), this.mockFixture.Logger.FindIndex(entry => entry.Item2.Name.EndsWith(".MetricsCaptured", StringComparison.Ordinal))); - } } [TestCase("", 1)] diff --git a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs index 4a7ce25aa9..b360f08729 100644 --- a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs @@ -43,7 +43,6 @@ public class SpecCpuExecutor : VirtualClientComponent private IPackageManager packageManager; private IStateManager stateManager; private ISystemManagement systemManager; - private NumaTopologySnapshot numaTopology; private string tuning; /// @@ -236,7 +235,6 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel protected async Task ExecuteSpecCpuAsync(EventContext telemetryContext, CancellationToken cancellationToken) { string stage = "Configuration"; - this.numaTopology = null; try { using (BackgroundOperations profiling = BackgroundOperations.BeginProfiling(this, cancellationToken)) @@ -285,7 +283,6 @@ protected async Task ExecuteSpecCpuAsync(EventContext telemetryContext, Cancella stage = "Metrics"; this.MetadataContract.AddForScenario($"SPECcpu/{this.SpecProfile}", process.FullCommand(), toolVersion: null); this.MetadataContract.Apply(telemetryContext); - await this.CaptureNumaMetricsAsync(commandLineArguments, telemetryContext, cancellationToken); await this.CaptureMetricsAsync(process, commandLineArguments, telemetryContext, cancellationToken); } } @@ -628,49 +625,6 @@ private async Task WindowsSetupAsync( } } - private Task CaptureNumaMetricsAsync(string commandArguments, EventContext telemetryContext, CancellationToken cancellationToken) - { - if (this.numaTopology == null || cancellationToken.IsCancellationRequested) - { - return Task.CompletedTask; - } - - string affinity = string.Join(",", this.numaTopology.SelectedCpus); - foreach (NumaNode node in this.numaTopology.Nodes) - { - Metric[] metrics = - [ - new Metric($"node-cpus-{node.Id}", node.Cpus.Count, "count"), - new Metric($"node-size-{node.Id}", node.SizeMb.Value, "MB"), - new Metric($"node-free-{node.Id}", node.FreeMb.Value, "MB") - ]; - - foreach (Metric metric in metrics) - { - metric.Categorization = "NUMA-diagnostic"; - metric.Verbosity = 5; - metric.Metadata["cpuIds"] = string.Join(",", node.Cpus); - metric.Metadata["selectedCpuIds"] = string.Join(",", this.numaTopology.SelectedCpus.Where(node.Cpus.Contains)); - metric.Metadata["numaProcessorAffinity"] = affinity; - metric.Metadata["processorBinding"] = "physcpubind"; - metric.Metadata["numaMemoryPolicy"] = "localalloc"; - metric.Metadata["topologyCapturedAt"] = this.numaTopology.CapturedAt.ToString("o", CultureInfo.InvariantCulture); - - this.Logger.LogMetric( - metric, - toolName: "SPECcpu", - scenarioName: this.Scenario, - this.numaTopology.CapturedAt, - this.numaTopology.CapturedAt, - commandArguments, - telemetryContext, - this.Tags); - } - } - - return Task.CompletedTask; - } - private async Task CaptureMetricsAsync( IProcessProxy process, string commandArguments, @@ -812,12 +766,7 @@ private async Task WritePinnedSpecCpuConfigAsync(EventContext telemetryC cancellationToken.ThrowIfCancellationRequested(); string topology = process.StandardOutput.ToString().Trim(); - IReadOnlyList nodes = null; - bool available = !process.IsErrored() && SpecCpuExecutor.TryParseNumaTopology(topology, cores, out nodes); - if (available) - { - this.numaTopology = new NumaTopologySnapshot(nodes, cores.ToArray(), DateTime.UtcNow); - } + bool available = !process.IsErrored() && SpecCpuExecutor.TryParseNumaTopology(topology, cores, out _); this.LogSpecCpuStep( telemetryContext, @@ -1057,22 +1006,6 @@ public NumaNode(int id) public double? FreeMb { get; set; } } - private sealed class NumaTopologySnapshot - { - public NumaTopologySnapshot(IReadOnlyList nodes, IReadOnlyList selectedCpus, DateTime capturedAt) - { - this.Nodes = nodes; - this.SelectedCpus = selectedCpus; - this.CapturedAt = capturedAt; - } - - public IReadOnlyList Nodes { get; } - - public IReadOnlyList SelectedCpus { get; } - - public DateTime CapturedAt { get; } - } - private static class SpecCpuConfigPlaceholder { public const string BaseOptimizingFlags = "$BaseOptimizingFlags$"; From 83e5e5575de9b3ac28caa2ceaccdc8020453aed8 Mon Sep 17 00:00:00 2001 From: "Samuel Filho (WIPRO LIMITED)" Date: Tue, 20 Oct 2026 18:24:40 -0300 Subject: [PATCH 5/9] Select one CPU per SPECrate copy from numactl --hardware, balancing copies across NUMA nodes as capacity allows. Expose NumaProcessorAffinitySymmetric in the INTRATE and FPRATE profiles with an unpinned default, and add validation, tests, and usage examples. --- .../SpecCpuProfileTests.cs | 45 ++++--- .../SPEC/SpecCpu2017ExecutorTests.cs | 121 ++++++++++++++++++ .../SPECcpu/SpecCpuExecutor.cs | 104 +++++++++++++++ .../profiles/PERF-SPECCPU-FPRATE.json | 6 +- .../profiles/PERF-SPECCPU-INTRATE.json | 6 +- .../workloads/speccpu/speccpu-profiles.md | 20 +-- 6 files changed, 271 insertions(+), 31 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs index f0bef411fb..c792f54d56 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs @@ -47,26 +47,18 @@ public void SpecCpuWorkloadProfileParametersAreInlinedCorrectly(string profile) } [Test] - [TestCase("PERF-SPECCPU-INTRATE.json", PlatformID.Unix, "intrate", null)] - [TestCase("PERF-SPECCPU-INTRATE.json", PlatformID.Unix, "intrate", "2,4,8,10")] - [TestCase("PERF-SPECCPU-INTRATE.json", PlatformID.Win32NT, "505 525 541 548 557", null)] - [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Unix, "fprate", null)] - [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Unix, "fprate", "2,4,8,10")] - [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Win32NT, "503 508 510 519 544 549 554", null)] - public async Task SpecCpuRateProfilesRunTheFullSuiteWithOptionalNumaProcessorAffinity( + [TestCase("PERF-SPECCPU-INTRATE.json", PlatformID.Unix, "intrate")] + [TestCase("PERF-SPECCPU-INTRATE.json", PlatformID.Win32NT, "505 525 541 548 557")] + [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Unix, "fprate")] + [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Win32NT, "503 508 510 519 544 549 554")] + public async Task SpecCpuRateProfilesRunTheFullSuiteWithoutManualNumaProcessorAffinity( string profileName, PlatformID platform, - string expectedBenchmarks, - string numaProcessorAffinity) + string expectedBenchmarks) { this.mockFixture.Setup(platform); ExecutionProfile profile = await ExecutionProfile.ReadProfileAsync(Path.Combine(MockFixture.TestResourcesDirectory, "profiles", profileName)); - if (numaProcessorAffinity != null) - { - profile.Parameters["NumaProcessorAffinity"] = numaProcessorAffinity; - } - profile.Parameters["CopyCount"] = 4; Assert.AreEqual(1, profile.Actions.Count, "A SPEC CPU rate suite must run as one action, not separate benchmarks."); @@ -74,16 +66,39 @@ public async Task SpecCpuRateProfilesRunTheFullSuiteWithOptionalNumaProcessorAff Assert.AreEqual("SpecCpuExecutor", action.Type); Assert.AreEqual("ExecuteSPECBenchmark", action.Parameters["Scenario"]); Assert.AreEqual("$.Parameters.Benchmarks", action.Parameters["Benchmarks"], "The suite action must use the platform-specific benchmark selection."); + Assert.IsFalse(profile.Parameters.ContainsKey("NumaProcessorAffinity")); + Assert.IsFalse(action.Parameters.ContainsKey("NumaProcessorAffinity")); Assert.IsFalse(profile.Actions.Any(item => item.Parameters["Scenario"].ToString().StartsWith("benchmark_", StringComparison.OrdinalIgnoreCase))); profile.Inline(); await ProfileExpressionEvaluator.Instance.EvaluateAsync(this.mockFixture.Dependencies, action.Parameters); Assert.AreEqual(expectedBenchmarks, action.Parameters["Benchmarks"]); - Assert.AreEqual(numaProcessorAffinity ?? string.Empty, action.Parameters["NumaProcessorAffinity"]); + Assert.IsFalse(action.Parameters.ContainsKey("NumaProcessorAffinity")); + Assert.AreEqual("False", action.Parameters["NumaProcessorAffinitySymmetric"].ToString()); Assert.AreEqual("4", action.Parameters["CopyCount"].ToString()); } + [Test] + [TestCase("PERF-SPECCPU-INTRATE.json", 2)] + [TestCase("PERF-SPECCPU-INTRATE.json", 4)] + [TestCase("PERF-SPECCPU-FPRATE.json", 2)] + [TestCase("PERF-SPECCPU-FPRATE.json", 4)] + public async Task SpecCpuRateProfilesForwardSymmetricNumaAffinity(string profileName, int copyCount) + { + this.mockFixture.Setup(PlatformID.Unix); + ExecutionProfile profile = await ExecutionProfile.ReadProfileAsync(Path.Combine(MockFixture.TestResourcesDirectory, "profiles", profileName)); + profile.Parameters["CopyCount"] = copyCount; + profile.Parameters["NumaProcessorAffinitySymmetric"] = true; + + profile.Inline(); + + ExecutionProfileElement action = profile.Actions.Single(); + Assert.AreEqual("True", action.Parameters["NumaProcessorAffinitySymmetric"].ToString()); + Assert.IsFalse(action.Parameters.ContainsKey("NumaProcessorAffinity")); + Assert.AreEqual(copyCount.ToString(), action.Parameters["CopyCount"].ToString()); + } + [Test] [TestCase("PERF-SPECCPU2026-FPRATE.json", Architecture.X64)] [TestCase("PERF-SPECCPU2026-FPRATE.json", Architecture.Arm64)] diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs index c9a10cb210..49efc4cef5 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs @@ -641,6 +641,127 @@ public async Task SpecCpu2017ExecutorPinsRateCopies(string profile, string cpuId Assert.IsTrue(commands.Any(command => command.Contains($"--config {Path.GetFileName(pinnedPath)}") && command.Contains("--copies 4"))); } + [TestCase(2, 2, "0,48", "0,48")] + [TestCase(2, 3, "0,1,48", "0,1,48")] + [TestCase(2, 4, "0,1,48,49", "0,1,48,49")] + [TestCase(2, 40, "0-19,48-67", "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67")] + [TestCase(3, 2, "0,32", "0,32")] + [TestCase(3, 3, "0,32,64", "0,32,64")] + [TestCase(3, 40, "0-13,32-44,64-76", "0,1,2,3,4,5,6,7,8,9,10,11,12,13,32,33,34,35,36,37,38,39,40,41,42,43,44,64,65,66,67,68,69,70,71,72,73,74,75,76")] + public async Task SpecCpu2017ExecutorSelectsSymmetricNumaAffinity(int nodeCount, int copyCount, string expectedCoreSpec, string expectedCpus) + { + this.SetupLinux(Architecture.Arm64); + this.SetupInstalledAffinityRun(string.Empty); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.CopyCount)] = copyCount; + + int coresPerNode = 96 / nodeCount; + string topology = $"available: {nodeCount} nodes (0-{nodeCount - 1})\n" + string.Join("\n", Enumerable.Range(0, nodeCount).Select(node => + $"node {node} cpus: {string.Join(" ", Enumerable.Range(node * coresPerNode, coresPerNode))}\nnode {node} size: 16000 MB\nnode {node} free: 12000 MB")); + + string pinnedConfig = null; + this.mockFixture.File.Setup(f => f.WriteAllTextAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((path, content, token) => pinnedConfig = content) + .Returns(Task.CompletedTask); + + List commands = new List(); + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => + { + commands.Add($"{exe} {arguments}"); + return new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, + StandardOutput = new ConcurrentBuffer(new StringBuilder(exe == "numactl" && arguments == "--hardware" ? topology : string.Empty)), + ExitCode = 0, + OnStart = () => true, + OnHasExited = () => true + }; + }; + + using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + await executor.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + } + + StringAssert.Contains($"bind = {expectedCpus}", pinnedConfig); + StringAssert.Contains("submit = numactl --localalloc --physcpubind=$BIND -- $command", pinnedConfig); + Assert.IsTrue(commands.Any(command => command == $"numactl --localalloc -C {expectedCoreSpec} true")); + Assert.IsTrue(commands.Any(command => command.Contains($"--copies {copyCount}"))); + EventContext selection = (EventContext)this.mockFixture.Logger.Single(entry => entry.Item2.Name.EndsWith(".SymmetricAffinitySelected", StringComparison.Ordinal)).Item3; + Assert.AreEqual(expectedCpus, selection.Properties["cpuIds"]); + Assert.AreEqual(true, selection.Properties["pinningEnabled"]); + Assert.AreEqual(expectedCpus, ((IDictionary)selection.Properties["metadata_scenario"])["NumaProcessorAffinity"]); + } + + [Test] + public void SpecCpu2017ExecutorRejectsManualAndSymmetricAffinityTogether() + { + this.SetupLinux(); + this.SetupInstalledAffinityRun("0-3"); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric)] = true; + + using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.ThrowsAsync(async () => await executor.ExecuteAsync(CancellationToken.None)); + } + } + + [Test] + public void SpecCpu2017ExecutorRejectsSymmetricAffinityWhenTopologyIsUnreadable() + { + this.SetupLinux(); + this.SetupInstalledAffinityRun(string.Empty); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric)] = true; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, + StandardOutput = new ConcurrentBuffer(new StringBuilder("invalid topology")), + ExitCode = 0, + OnStart = () => true, + OnHasExited = () => true + }; + + using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.ThrowsAsync(async () => await executor.ExecuteAsync(CancellationToken.None)); + } + } + + [Test] + public void SpecCpu2017ExecutorRejectsSymmetricAffinityOnSpeedRuns() + { + this.SetupLinux(); + this.SetupInstalledAffinityRun(string.Empty); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.SpecProfile)] = "intspeed"; + + using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.ThrowsAsync(async () => await executor.ExecuteAsync(CancellationToken.None)); + } + } + + [Test] + public void SpecCpu2017ExecutorRejectsSymmetricAffinityWhenTopologyHasTooFewCpus() + { + this.SetupLinux(); + this.SetupInstalledAffinityRun(string.Empty); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric)] = true; + this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => new InMemoryProcess + { + StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, + StandardOutput = new ConcurrentBuffer(new StringBuilder("available: 1 nodes (0)\nnode 0 cpus: 0 1\nnode 0 size: 16000 MB\nnode 0 free: 12000 MB")), + ExitCode = 0, + OnStart = () => true, + OnHasExited = () => true + }; + + using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.ThrowsAsync(async () => await executor.ExecuteAsync(CancellationToken.None)); + } + } + [TestCase("", false, Architecture.X64)] [TestCase("0-3", true, Architecture.X64)] [TestCase("0-3", true, Architecture.Arm64)] diff --git a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs index b360f08729..1332dc3946 100644 --- a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs @@ -170,6 +170,19 @@ public string NumaProcessorAffinity } } + /// + /// When enabled, distributes SPECrate copies as evenly as available CPUs allow across NUMA nodes, + /// assigning one CPU per copy using numactl. + /// Defaults to false; cannot be combined with manual NumaProcessorAffinity. + /// + public bool NumaProcessorAffinitySymmetric + { + get + { + return this.Parameters.GetValue(nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric), false); + } + } + /// /// A feature flag to apply. For example 'UseCsvResults' can be used to parse the CSV file results vs. the standard output. /// The CSV results have finer-grained results with additional significant figures. @@ -239,6 +252,16 @@ protected async Task ExecuteSpecCpuAsync(EventContext telemetryContext, Cancella { using (BackgroundOperations profiling = BackgroundOperations.BeginProfiling(this, cancellationToken)) { + if (this.NumaProcessorAffinitySymmetric) + { + stage = "SymmetricAffinityConfiguration"; + string affinity = await this.GetSymmetricNumaProcessorAffinityAsync(cancellationToken); + this.Parameters[nameof(this.NumaProcessorAffinity)] = affinity; + this.MetadataContract.Add(nameof(this.NumaProcessorAffinity), affinity, MetadataContract.ScenarioCategory); + this.MetadataContract.Apply(telemetryContext); + this.LogSpecCpuStep(telemetryContext, "SymmetricAffinitySelected", ("cpuIds", affinity)); + } + string configurationFile = this.GetConfigurationFileName(); if (!string.IsNullOrWhiteSpace(this.NumaProcessorAffinity)) { @@ -718,6 +741,87 @@ private string GetCommandLineArguments(string configurationFile) return $"{command} {this.Benchmarks}"; } + /// + /// Reads numactl --hardware and takes the lowest CPU IDs from each node in round-robin order, + /// distributing copies as evenly as node capacity allows. Returns a comma-separated list + /// containing one CPU per . + /// + /// + /// For two 48-CPU nodes (0-47, 48-95), 2 copies select 0 and 48; 4 copies select 0, 1, 48, 49; + /// and 40 copies select 0-19 and 48-67. For three 32-CPU nodes (0-31, 32-63, 64-95), + /// 3 copies select 0, 32, 64 and 40 copies select 0-13, 32-44, 64-76. + /// The returned list contains individual CPU IDs, not range notation. + /// + /// A token used to cancel the topology query. + /// The selected CPU IDs in ascending order, separated by commas. + private async Task GetSymmetricNumaProcessorAffinityAsync(CancellationToken cancellationToken) + { + if (this.Platform != PlatformID.Unix || + (!string.Equals(this.SpecProfile, "intrate", StringComparison.OrdinalIgnoreCase) && + !string.Equals(this.SpecProfile, "fprate", StringComparison.OrdinalIgnoreCase))) + { + throw new NotSupportedException("Symmetric NUMA processor affinity is supported only for Linux intrate and fprate runs."); + } + + if (!string.IsNullOrWhiteSpace(this.NumaProcessorAffinity)) + { + throw new ArgumentException("NumaProcessorAffinity and NumaProcessorAffinitySymmetric cannot both be specified.", nameof(this.NumaProcessorAffinity)); + } + + if (this.CopyCount <= 0) + { + throw new ArgumentOutOfRangeException(nameof(this.CopyCount), this.CopyCount, "CopyCount must be greater than zero for symmetric NUMA processor affinity."); + } + + IReadOnlyList nodes; + try + { + using (IProcessProxy process = this.systemManager.ProcessManager.CreateProcess("numactl", "--hardware", this.PackageDirectory)) + { + await process.StartAndWaitAsync(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + string topology = process.StandardOutput.ToString().Trim(); + if (process.IsErrored() || !SpecCpuExecutor.TryParseNumaTopology(topology, Array.Empty(), out nodes)) + { + throw new DependencyException("Symmetric NUMA processor affinity requires a readable numactl --hardware topology.", ErrorReason.WorkloadDependencyMissing); + } + } + } + catch (Win32Exception exc) + { + throw new DependencyException("Symmetric NUMA processor affinity requires numactl to be installed.", exc, ErrorReason.WorkloadDependencyMissing); + } + + List> nodeCpus = nodes + .Where(node => node.Cpus.Count > 0) + .Select(node => (IReadOnlyList)node.Cpus.OrderBy(cpu => cpu).ToList()) + .ToList(); + + if (nodeCpus.Sum(cpus => cpus.Count) < this.CopyCount) + { + throw new ArgumentException($"CopyCount={this.CopyCount} exceeds the number of CPUs available across NUMA nodes.", nameof(this.CopyCount)); + } + + List selectedCpus = new List(this.CopyCount); + for (int index = 0; selectedCpus.Count < this.CopyCount; index++) + { + foreach (IReadOnlyList cpus in nodeCpus) + { + if (index < cpus.Count) + { + selectedCpus.Add(cpus[index]); + if (selectedCpus.Count == this.CopyCount) + { + break; + } + } + } + } + + return string.Join(",", selectedCpus.OrderBy(cpu => cpu)); + } + private async Task WritePinnedSpecCpuConfigAsync(EventContext telemetryContext, CancellationToken cancellationToken) { this.LogSpecCpuStep(telemetryContext, "AffinityValidationStarted", ("requestedCpuIds", this.NumaProcessorAffinity)); diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json index b5b9e95613..7a9d1cc3b1 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json @@ -10,7 +10,7 @@ "RecommendedMinimumExecutionTime": "(4-cores)=02:00:00,(16-cores)=05:00:00,(64-cores)=10:00:00", "SupportedPlatforms": "linux-x64,linux-arm64,win-x64,win-arm64", "SupportedOperatingSystems": "AzureLinux,CentOS,Debian,OpenSuse,RedHat,Ubuntu,Windows", - "Notes": "Using a private package 'speccpu.2017.1.1.9-danielbowers.zip' to enable support for speccpu on Ubuntu 24. SPEC CPU 2017 was not initially designed for win-arm64. Optional NumaProcessorAffinity is Linux-only: provide one distinct CPU ID per copy (e.g. 0-3 for CopyCount=4), or leave empty to disable pinning.", + "Notes": "Private package speccpu.2017.1.1.9-danielbowers.zip supports Ubuntu 24; SPEC CPU 2017 was not designed for win-arm64. NumaProcessorAffinitySymmetric (Linux rate only): true balances copies across NUMA nodes, one CPU each; false (default) does not pin.", "Limitations": "The following benchmarks don't work on Windows: 507, 511, 521, 526, 527, 538" }, "Parameters": { @@ -20,7 +20,7 @@ "RunPeak": false, "ThreadCount": "{LogicalCoreCount}", "CopyCount": "{LogicalCoreCount}", - "NumaProcessorAffinity": "", + "NumaProcessorAffinitySymmetric": false, "BaseOptimizingFlags": "-O2 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches", "PeakOptimizingFlags": "-O3 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches" }, @@ -36,7 +36,7 @@ "RunPeak": "$.Parameters.RunPeak", "ThreadCount": "$.Parameters.ThreadCount", "CopyCount": "$.Parameters.CopyCount", - "NumaProcessorAffinity": "$.Parameters.NumaProcessorAffinity", + "NumaProcessorAffinitySymmetric": "$.Parameters.NumaProcessorAffinitySymmetric", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json index 281f8e24d0..d9b4ff5392 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json @@ -10,7 +10,7 @@ "RecommendedMinimumExecutionTime": "(8-cores)=04:00:00,(16-cores)=08:00:00,(64-cores)=10:00:00", "SupportedPlatforms": "linux-x64,linux-arm64,win-x64", "SupportedOperatingSystems": "AzureLinux,CentOS,Debian,OpenSuse,RedHat,Ubuntu,Windows", - "Notes": "Using a private package 'speccpu.2017.1.1.9-danielbowers.zip' to enable support for speccpu on Ubuntu 24. SPEC CPU 2017 was not initially designed for win-arm64. Optional NumaProcessorAffinity is Linux-only: provide one distinct CPU ID per copy (e.g. 0-3 for CopyCount=4), or leave empty to disable pinning.", + "Notes": "Private package speccpu.2017.1.1.9-danielbowers.zip supports Ubuntu 24; SPEC CPU 2017 was not designed for win-arm64. NumaProcessorAffinitySymmetric (Linux rate only): true balances copies across NUMA nodes, one CPU each; false (default) does not pin.", "Limitations": "The following benchmarks don't work on Windows: 500, 502, 520, 523, 531" }, "Parameters": { @@ -20,7 +20,7 @@ "RunPeak": false, "ThreadCount": "{LogicalCoreCount}", "CopyCount": "{LogicalCoreCount}", - "NumaProcessorAffinity": "", + "NumaProcessorAffinitySymmetric": false, "BaseOptimizingFlags": "-O2 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches", "PeakOptimizingFlags": "-O3 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches" }, @@ -36,7 +36,7 @@ "RunPeak": "$.Parameters.RunPeak", "ThreadCount": "$.Parameters.ThreadCount", "CopyCount": "$.Parameters.CopyCount", - "NumaProcessorAffinity": "$.Parameters.NumaProcessorAffinity", + "NumaProcessorAffinitySymmetric": "$.Parameters.NumaProcessorAffinitySymmetric", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } diff --git a/website/docs/workloads/speccpu/speccpu-profiles.md b/website/docs/workloads/speccpu/speccpu-profiles.md index 9b285be645..592ef8bf61 100644 --- a/website/docs/workloads/speccpu/speccpu-profiles.md +++ b/website/docs/workloads/speccpu/speccpu-profiles.md @@ -44,7 +44,7 @@ for evaluating the performance of the CPU for processing calculations. | RunPeak | Optional. True to run the workload 'Peak' scenario, False to run the workload 'Base' scenario. | false (Base) | | ThreadCount | Optional. Determines the number of threads to use for running the benchmark. | # logical processors | | CopyCount | Optional. Determines the number of copies of the benchmark to run concurrently. | # logical processors | - | NumaProcessorAffinity | Optional. Linux SPECrate only (intrate/fprate). Assign one distinct CPU ID to each copy; requires `numactl`. Leave empty to run without pinning. | empty (no pinning) | + | NumaProcessorAffinitySymmetric | Optional. Linux SPECrate only. Balances copies across NUMA nodes as available CPUs allow, assigning one CPU per copy using `numactl`; false leaves copies unpinned. | false | | BaseOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Base' scenario |
  • X64 systems: -g -O2 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O2 -march=armv8.2-a -flto -frecord-gcc-switches
| | PeakOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Peak' scenario |
  • X64 systems: -g -O3 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O3 -march=armv8.2-a -flto -frecord-gcc-switches
| @@ -65,7 +65,7 @@ for evaluating the performance of the CPU for processing calculations. * **Notes** On Windows, only the following benchmarks are supported: 503.bwaves_r, 508.namd_r, 510.parest_r, 519.lbm_r, 544.nab_r, 549.fotonik3d_r, 554.roms_r. - The Linux `NumaProcessorAffinity` examples in the INTRATE section below also apply to this profile when `PERF-SPECCPU-FPRATE.json` is used instead. + The Linux `NumaProcessorAffinitySymmetric` examples in the INTRATE section below also apply to this profile. ``` bash # Execute the workload profile @@ -157,7 +157,7 @@ for evaluating the performance of the CPU for processing calculations. | RunPeak | Optional. True to run the workload 'Peak' scenario, False to run the workload 'Base' scenario. | false (Base) | | ThreadCount | Optional. Determines the number of threads to use for running the benchmark. | # logical processors | | CopyCount | Optional. Determines the number of copies of the benchmark to run concurrently. | # logical processors | - | NumaProcessorAffinity | Optional. Linux SPECrate only (intrate/fprate). Assign one distinct CPU ID to each copy; requires `numactl`. Leave empty to run without pinning. | empty (no pinning) | + | NumaProcessorAffinitySymmetric | Optional. Linux SPECrate only. Balances copies across NUMA nodes as available CPUs allow, assigning one CPU per copy using `numactl`; false leaves copies unpinned. | false | | BaseOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Base' scenario |
  • X64 systems: -g -O2 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O2 -march=armv8.2-a -flto -frecord-gcc-switches
| | PeakOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Peak' scenario |
  • X64 systems: -g -O3 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O3 -march=armv8.2-a -flto -frecord-gcc-switches
| @@ -177,17 +177,17 @@ for evaluating the performance of the CPU for processing calculations. * **Notes** On Windows, only the following benchmarks are supported: 505.mcf_r, 525.x264_r, 541.leela_r, 548.exchange2_r, 557.xz_r. - On Linux, check the CPU-to-node mapping with `numactl --hardware` before choosing CPU IDs. `NumaProcessorAffinity` must contain exactly as many distinct CPU IDs as `CopyCount`. Pinned copies use `numactl --localalloc` for local memory allocation. The CPU IDs below are examples; use IDs appropriate for your machine. + On Linux, `NumaProcessorAffinitySymmetric=true` selects CPUs from `numactl --hardware`, giving each node as even a share of `CopyCount` as its available CPUs allow. If `CopyCount` is not divisible by the number of nodes, lower-numbered nodes receive the extra copies. Pinned copies use `numactl --localalloc` for local memory allocation. These examples assume 96 CPUs on two NUMA nodes: CPUs 0-47 on node 0 and 48-95 on node 1. Check the actual topology with `numactl --hardware`. ``` bash - # Empty NumaProcessorAffinity (default): run without pinning, as before. - ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --logger=csv + # Two INTRATE copies: CPU 0 on node 0 and CPU 48 on node 1. + ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=2,,,NumaProcessorAffinitySymmetric=true" --logger=csv - # Single NUMA node: CPUs 0-3 must all belong to the same node. - ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,NumaProcessorAffinity=0-3" --logger=csv + # Four INTRATE copies: CPUs 0-1 on node 0 and 48-49 on node 1. + ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,NumaProcessorAffinitySymmetric=true" --logger=csv - # Two NUMA nodes: CPUs 0-1 on node 0 and 48-49 on node 1. - ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,NumaProcessorAffinity=0,1,48,49" --logger=csv + # Four FPRATE copies: the same two-per-node distribution. + ./VirtualClient --profile=PERF-SPECCPU-FPRATE.json --iterations=1 --parameters="CopyCount=4,,,NumaProcessorAffinitySymmetric=true" --logger=csv ``` ## PERF-SPECCPU-INTSPEED.json From 642a46f3ee004ec23bfcf51c9a931bad0c507c82 Mon Sep 17 00:00:00 2001 From: "Samuel Filho (WIPRO LIMITED)" Date: Wed, 21 Oct 2026 16:27:11 -0300 Subject: [PATCH 6/9] better naming to enable numa pinning EnableNumaProcessorAffinity , also unit tests and comments review --- .../SpecCpuProfileTests.cs | 6 +++--- .../SPEC/SpecCpu2017ExecutorTests.cs | 10 +++++----- .../SPECcpu/SpecCpuExecutor.cs | 15 +++++++++++---- .../profiles/PERF-SPECCPU-FPRATE.json | 6 +++--- .../profiles/PERF-SPECCPU-INTRATE.json | 6 +++--- .../docs/workloads/speccpu/speccpu-profiles.md | 14 +++++++------- 6 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs index c792f54d56..69e8675034 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs @@ -75,7 +75,7 @@ public async Task SpecCpuRateProfilesRunTheFullSuiteWithoutManualNumaProcessorAf Assert.AreEqual(expectedBenchmarks, action.Parameters["Benchmarks"]); Assert.IsFalse(action.Parameters.ContainsKey("NumaProcessorAffinity")); - Assert.AreEqual("False", action.Parameters["NumaProcessorAffinitySymmetric"].ToString()); + Assert.AreEqual("False", action.Parameters["EnableNumaProcessorAffinity"].ToString()); Assert.AreEqual("4", action.Parameters["CopyCount"].ToString()); } @@ -89,12 +89,12 @@ public async Task SpecCpuRateProfilesForwardSymmetricNumaAffinity(string profile this.mockFixture.Setup(PlatformID.Unix); ExecutionProfile profile = await ExecutionProfile.ReadProfileAsync(Path.Combine(MockFixture.TestResourcesDirectory, "profiles", profileName)); profile.Parameters["CopyCount"] = copyCount; - profile.Parameters["NumaProcessorAffinitySymmetric"] = true; + profile.Parameters["EnableNumaProcessorAffinity"] = true; profile.Inline(); ExecutionProfileElement action = profile.Actions.Single(); - Assert.AreEqual("True", action.Parameters["NumaProcessorAffinitySymmetric"].ToString()); + Assert.AreEqual("True", action.Parameters["EnableNumaProcessorAffinity"].ToString()); Assert.IsFalse(action.Parameters.ContainsKey("NumaProcessorAffinity")); Assert.AreEqual(copyCount.ToString(), action.Parameters["CopyCount"].ToString()); } diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs index 49efc4cef5..79c4853c31 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs @@ -652,7 +652,7 @@ public async Task SpecCpu2017ExecutorSelectsSymmetricNumaAffinity(int nodeCount, { this.SetupLinux(Architecture.Arm64); this.SetupInstalledAffinityRun(string.Empty); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinity)] = true; this.mockFixture.Parameters[nameof(SpecCpuExecutor.CopyCount)] = copyCount; int coresPerNode = 96 / nodeCount; @@ -698,7 +698,7 @@ public void SpecCpu2017ExecutorRejectsManualAndSymmetricAffinityTogether() { this.SetupLinux(); this.SetupInstalledAffinityRun("0-3"); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinity)] = true; using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) { @@ -711,7 +711,7 @@ public void SpecCpu2017ExecutorRejectsSymmetricAffinityWhenTopologyIsUnreadable( { this.SetupLinux(); this.SetupInstalledAffinityRun(string.Empty); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinity)] = true; this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => new InMemoryProcess { StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, @@ -732,7 +732,7 @@ public void SpecCpu2017ExecutorRejectsSymmetricAffinityOnSpeedRuns() { this.SetupLinux(); this.SetupInstalledAffinityRun(string.Empty); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinity)] = true; this.mockFixture.Parameters[nameof(SpecCpuExecutor.SpecProfile)] = "intspeed"; using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) @@ -746,7 +746,7 @@ public void SpecCpu2017ExecutorRejectsSymmetricAffinityWhenTopologyHasTooFewCpus { this.SetupLinux(); this.SetupInstalledAffinityRun(string.Empty); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinity)] = true; this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => new InMemoryProcess { StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, diff --git a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs index 1332dc3946..e745cec23a 100644 --- a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs @@ -161,6 +161,13 @@ public int CopyCount /// /// Optional list of physical CPU IDs to assign to SPECrate copies, one CPU per copy. + /// For two 48-CPU NUMA nodes (CPUs 0-47 and 48-95): + /// 2 copies: NumaProcessorAffinity=0,48; + /// 4 copies: NumaProcessorAffinity=0,1,48,49; + /// 40 copies: NumaProcessorAffinity=0-19,48-67. + /// For three 32-CPU NUMA nodes (CPUs 0-31, 32-63, and 64-95): + /// 3 copies: NumaProcessorAffinity=0,32,64; + /// 40 copies: NumaProcessorAffinity=0-13,32-44,64-76. /// public string NumaProcessorAffinity { @@ -175,11 +182,11 @@ public string NumaProcessorAffinity /// assigning one CPU per copy using numactl. /// Defaults to false; cannot be combined with manual NumaProcessorAffinity. ///
- public bool NumaProcessorAffinitySymmetric + public bool EnableNumaProcessorAffinity { get { - return this.Parameters.GetValue(nameof(SpecCpuExecutor.NumaProcessorAffinitySymmetric), false); + return this.Parameters.GetValue(nameof(SpecCpuExecutor.EnableNumaProcessorAffinity), false); } } @@ -252,7 +259,7 @@ protected async Task ExecuteSpecCpuAsync(EventContext telemetryContext, Cancella { using (BackgroundOperations profiling = BackgroundOperations.BeginProfiling(this, cancellationToken)) { - if (this.NumaProcessorAffinitySymmetric) + if (this.EnableNumaProcessorAffinity) { stage = "SymmetricAffinityConfiguration"; string affinity = await this.GetSymmetricNumaProcessorAffinityAsync(cancellationToken); @@ -765,7 +772,7 @@ private async Task GetSymmetricNumaProcessorAffinityAsync(CancellationTo if (!string.IsNullOrWhiteSpace(this.NumaProcessorAffinity)) { - throw new ArgumentException("NumaProcessorAffinity and NumaProcessorAffinitySymmetric cannot both be specified.", nameof(this.NumaProcessorAffinity)); + throw new ArgumentException("NumaProcessorAffinity and EnableNumaProcessorAffinity cannot both be specified.", nameof(this.NumaProcessorAffinity)); } if (this.CopyCount <= 0) diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json index 7a9d1cc3b1..7f4d4409e6 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json @@ -10,7 +10,7 @@ "RecommendedMinimumExecutionTime": "(4-cores)=02:00:00,(16-cores)=05:00:00,(64-cores)=10:00:00", "SupportedPlatforms": "linux-x64,linux-arm64,win-x64,win-arm64", "SupportedOperatingSystems": "AzureLinux,CentOS,Debian,OpenSuse,RedHat,Ubuntu,Windows", - "Notes": "Private package speccpu.2017.1.1.9-danielbowers.zip supports Ubuntu 24; SPEC CPU 2017 was not designed for win-arm64. NumaProcessorAffinitySymmetric (Linux rate only): true balances copies across NUMA nodes, one CPU each; false (default) does not pin.", + "Notes": "Private package speccpu.2017.1.1.9-danielbowers.zip supports Ubuntu 24; SPEC CPU 2017 was not designed for win-arm64. EnableNumaProcessorAffinity (Linux rate only): true balances copies across NUMA nodes, one CPU each; false (default) does not pin.", "Limitations": "The following benchmarks don't work on Windows: 507, 511, 521, 526, 527, 538" }, "Parameters": { @@ -20,7 +20,7 @@ "RunPeak": false, "ThreadCount": "{LogicalCoreCount}", "CopyCount": "{LogicalCoreCount}", - "NumaProcessorAffinitySymmetric": false, + "EnableNumaProcessorAffinity": false, "BaseOptimizingFlags": "-O2 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches", "PeakOptimizingFlags": "-O3 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches" }, @@ -36,7 +36,7 @@ "RunPeak": "$.Parameters.RunPeak", "ThreadCount": "$.Parameters.ThreadCount", "CopyCount": "$.Parameters.CopyCount", - "NumaProcessorAffinitySymmetric": "$.Parameters.NumaProcessorAffinitySymmetric", + "EnableNumaProcessorAffinity": "$.Parameters.EnableNumaProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json index d9b4ff5392..d25ba4245a 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json @@ -10,7 +10,7 @@ "RecommendedMinimumExecutionTime": "(8-cores)=04:00:00,(16-cores)=08:00:00,(64-cores)=10:00:00", "SupportedPlatforms": "linux-x64,linux-arm64,win-x64", "SupportedOperatingSystems": "AzureLinux,CentOS,Debian,OpenSuse,RedHat,Ubuntu,Windows", - "Notes": "Private package speccpu.2017.1.1.9-danielbowers.zip supports Ubuntu 24; SPEC CPU 2017 was not designed for win-arm64. NumaProcessorAffinitySymmetric (Linux rate only): true balances copies across NUMA nodes, one CPU each; false (default) does not pin.", + "Notes": "Private package speccpu.2017.1.1.9-danielbowers.zip supports Ubuntu 24; SPEC CPU 2017 was not designed for win-arm64. EnableNumaProcessorAffinity (Linux rate only): true balances copies across NUMA nodes, one CPU each; false (default) does not pin.", "Limitations": "The following benchmarks don't work on Windows: 500, 502, 520, 523, 531" }, "Parameters": { @@ -20,7 +20,7 @@ "RunPeak": false, "ThreadCount": "{LogicalCoreCount}", "CopyCount": "{LogicalCoreCount}", - "NumaProcessorAffinitySymmetric": false, + "EnableNumaProcessorAffinity": false, "BaseOptimizingFlags": "-O2 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches", "PeakOptimizingFlags": "-O3 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches" }, @@ -36,7 +36,7 @@ "RunPeak": "$.Parameters.RunPeak", "ThreadCount": "$.Parameters.ThreadCount", "CopyCount": "$.Parameters.CopyCount", - "NumaProcessorAffinitySymmetric": "$.Parameters.NumaProcessorAffinitySymmetric", + "EnableNumaProcessorAffinity": "$.Parameters.EnableNumaProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } diff --git a/website/docs/workloads/speccpu/speccpu-profiles.md b/website/docs/workloads/speccpu/speccpu-profiles.md index 592ef8bf61..5e1b026096 100644 --- a/website/docs/workloads/speccpu/speccpu-profiles.md +++ b/website/docs/workloads/speccpu/speccpu-profiles.md @@ -44,7 +44,7 @@ for evaluating the performance of the CPU for processing calculations. | RunPeak | Optional. True to run the workload 'Peak' scenario, False to run the workload 'Base' scenario. | false (Base) | | ThreadCount | Optional. Determines the number of threads to use for running the benchmark. | # logical processors | | CopyCount | Optional. Determines the number of copies of the benchmark to run concurrently. | # logical processors | - | NumaProcessorAffinitySymmetric | Optional. Linux SPECrate only. Balances copies across NUMA nodes as available CPUs allow, assigning one CPU per copy using `numactl`; false leaves copies unpinned. | false | + | EnableNumaProcessorAffinity | Optional. Linux SPECrate only. Balances copies across NUMA nodes as available CPUs allow, assigning one CPU per copy using `numactl`; false leaves copies unpinned. | false | | BaseOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Base' scenario |
  • X64 systems: -g -O2 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O2 -march=armv8.2-a -flto -frecord-gcc-switches
| | PeakOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Peak' scenario |
  • X64 systems: -g -O3 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O3 -march=armv8.2-a -flto -frecord-gcc-switches
| @@ -65,7 +65,7 @@ for evaluating the performance of the CPU for processing calculations. * **Notes** On Windows, only the following benchmarks are supported: 503.bwaves_r, 508.namd_r, 510.parest_r, 519.lbm_r, 544.nab_r, 549.fotonik3d_r, 554.roms_r. - The Linux `NumaProcessorAffinitySymmetric` examples in the INTRATE section below also apply to this profile. + The Linux `EnableNumaProcessorAffinity` examples in the INTRATE section below also apply to this profile. ``` bash # Execute the workload profile @@ -157,7 +157,7 @@ for evaluating the performance of the CPU for processing calculations. | RunPeak | Optional. True to run the workload 'Peak' scenario, False to run the workload 'Base' scenario. | false (Base) | | ThreadCount | Optional. Determines the number of threads to use for running the benchmark. | # logical processors | | CopyCount | Optional. Determines the number of copies of the benchmark to run concurrently. | # logical processors | - | NumaProcessorAffinitySymmetric | Optional. Linux SPECrate only. Balances copies across NUMA nodes as available CPUs allow, assigning one CPU per copy using `numactl`; false leaves copies unpinned. | false | + | EnableNumaProcessorAffinity | Optional. Linux SPECrate only. Balances copies across NUMA nodes as available CPUs allow, assigning one CPU per copy using `numactl`; false leaves copies unpinned. | false | | BaseOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Base' scenario |
  • X64 systems: -g -O2 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O2 -march=armv8.2-a -flto -frecord-gcc-switches
| | PeakOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Peak' scenario |
  • X64 systems: -g -O3 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O3 -march=armv8.2-a -flto -frecord-gcc-switches
| @@ -177,17 +177,17 @@ for evaluating the performance of the CPU for processing calculations. * **Notes** On Windows, only the following benchmarks are supported: 505.mcf_r, 525.x264_r, 541.leela_r, 548.exchange2_r, 557.xz_r. - On Linux, `NumaProcessorAffinitySymmetric=true` selects CPUs from `numactl --hardware`, giving each node as even a share of `CopyCount` as its available CPUs allow. If `CopyCount` is not divisible by the number of nodes, lower-numbered nodes receive the extra copies. Pinned copies use `numactl --localalloc` for local memory allocation. These examples assume 96 CPUs on two NUMA nodes: CPUs 0-47 on node 0 and 48-95 on node 1. Check the actual topology with `numactl --hardware`. + On Linux, `EnableNumaProcessorAffinity=true` selects CPUs from `numactl --hardware`, giving each node as even a share of `CopyCount` as its available CPUs allow. If `CopyCount` is not divisible by the number of nodes, lower-numbered nodes receive the extra copies. Pinned copies use `numactl --localalloc` for local memory allocation. These examples assume 96 CPUs on two NUMA nodes: CPUs 0-47 on node 0 and 48-95 on node 1. Check the actual topology with `numactl --hardware`. ``` bash # Two INTRATE copies: CPU 0 on node 0 and CPU 48 on node 1. - ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=2,,,NumaProcessorAffinitySymmetric=true" --logger=csv + ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=2,,,EnableNumaProcessorAffinity=true" --logger=csv # Four INTRATE copies: CPUs 0-1 on node 0 and 48-49 on node 1. - ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,NumaProcessorAffinitySymmetric=true" --logger=csv + ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,EnableNumaProcessorAffinity=true" --logger=csv # Four FPRATE copies: the same two-per-node distribution. - ./VirtualClient --profile=PERF-SPECCPU-FPRATE.json --iterations=1 --parameters="CopyCount=4,,,NumaProcessorAffinitySymmetric=true" --logger=csv + ./VirtualClient --profile=PERF-SPECCPU-FPRATE.json --iterations=1 --parameters="CopyCount=4,,,EnableNumaProcessorAffinity=true" --logger=csv ``` ## PERF-SPECCPU-INTSPEED.json From 4b749a4f1885c082f3dd0868a9083f56f566b4e7 Mon Sep 17 00:00:00 2001 From: "Samuel Filho (WIPRO LIMITED)" Date: Wed, 21 Oct 2026 18:23:36 -0300 Subject: [PATCH 7/9] NumaProcessorAffinity is back to profiles with 'symmetric' value to balance copies across NUMA nodes, one CPU each; explicit CPU IDs pin copies them manually. --- .../SpecCpuProfileTests.cs | 26 +++++++-------- .../SPEC/SpecCpu2017ExecutorTests.cs | 32 +++++++++++-------- .../SPECcpu/SpecCpuExecutor.cs | 16 ++++++---- .../profiles/PERF-SPECCPU-FPRATE.json | 6 ++-- .../profiles/PERF-SPECCPU-INTRATE.json | 6 ++-- .../workloads/speccpu/speccpu-profiles.md | 14 ++++---- 6 files changed, 54 insertions(+), 46 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs index 69e8675034..6c4b84230b 100644 --- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/SpecCpuProfileTests.cs @@ -51,7 +51,7 @@ public void SpecCpuWorkloadProfileParametersAreInlinedCorrectly(string profile) [TestCase("PERF-SPECCPU-INTRATE.json", PlatformID.Win32NT, "505 525 541 548 557")] [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Unix, "fprate")] [TestCase("PERF-SPECCPU-FPRATE.json", PlatformID.Win32NT, "503 508 510 519 544 549 554")] - public async Task SpecCpuRateProfilesRunTheFullSuiteWithoutManualNumaProcessorAffinity( + public async Task SpecCpuRateProfilesRunTheFullSuiteWithNumaProcessorAffinityDisabledByDefault( string profileName, PlatformID platform, string expectedBenchmarks) @@ -66,36 +66,36 @@ public async Task SpecCpuRateProfilesRunTheFullSuiteWithoutManualNumaProcessorAf Assert.AreEqual("SpecCpuExecutor", action.Type); Assert.AreEqual("ExecuteSPECBenchmark", action.Parameters["Scenario"]); Assert.AreEqual("$.Parameters.Benchmarks", action.Parameters["Benchmarks"], "The suite action must use the platform-specific benchmark selection."); - Assert.IsFalse(profile.Parameters.ContainsKey("NumaProcessorAffinity")); - Assert.IsFalse(action.Parameters.ContainsKey("NumaProcessorAffinity")); + Assert.AreEqual(string.Empty, profile.Parameters["NumaProcessorAffinity"]); + Assert.AreEqual("$.Parameters.NumaProcessorAffinity", action.Parameters["NumaProcessorAffinity"]); Assert.IsFalse(profile.Actions.Any(item => item.Parameters["Scenario"].ToString().StartsWith("benchmark_", StringComparison.OrdinalIgnoreCase))); profile.Inline(); await ProfileExpressionEvaluator.Instance.EvaluateAsync(this.mockFixture.Dependencies, action.Parameters); Assert.AreEqual(expectedBenchmarks, action.Parameters["Benchmarks"]); - Assert.IsFalse(action.Parameters.ContainsKey("NumaProcessorAffinity")); - Assert.AreEqual("False", action.Parameters["EnableNumaProcessorAffinity"].ToString()); + Assert.AreEqual(string.Empty, action.Parameters["NumaProcessorAffinity"]); + Assert.IsFalse(action.Parameters.ContainsKey("EnableNumaProcessorAffinitySymmetric")); Assert.AreEqual("4", action.Parameters["CopyCount"].ToString()); } [Test] - [TestCase("PERF-SPECCPU-INTRATE.json", 2)] - [TestCase("PERF-SPECCPU-INTRATE.json", 4)] - [TestCase("PERF-SPECCPU-FPRATE.json", 2)] - [TestCase("PERF-SPECCPU-FPRATE.json", 4)] - public async Task SpecCpuRateProfilesForwardSymmetricNumaAffinity(string profileName, int copyCount) + [TestCase("PERF-SPECCPU-INTRATE.json", 2, "symmetric")] + [TestCase("PERF-SPECCPU-INTRATE.json", 4, "SyMmEtRiC")] + [TestCase("PERF-SPECCPU-FPRATE.json", 2, "symmetric")] + [TestCase("PERF-SPECCPU-FPRATE.json", 4, "0,1,48,49")] + public async Task SpecCpuRateProfilesForwardNumaProcessorAffinity(string profileName, int copyCount, string affinity) { this.mockFixture.Setup(PlatformID.Unix); ExecutionProfile profile = await ExecutionProfile.ReadProfileAsync(Path.Combine(MockFixture.TestResourcesDirectory, "profiles", profileName)); profile.Parameters["CopyCount"] = copyCount; - profile.Parameters["EnableNumaProcessorAffinity"] = true; + profile.Parameters["NumaProcessorAffinity"] = affinity; profile.Inline(); ExecutionProfileElement action = profile.Actions.Single(); - Assert.AreEqual("True", action.Parameters["EnableNumaProcessorAffinity"].ToString()); - Assert.IsFalse(action.Parameters.ContainsKey("NumaProcessorAffinity")); + Assert.AreEqual(affinity, action.Parameters["NumaProcessorAffinity"]); + Assert.IsFalse(action.Parameters.ContainsKey("EnableNumaProcessorAffinitySymmetric")); Assert.AreEqual(copyCount.ToString(), action.Parameters["CopyCount"].ToString()); } diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs index 79c4853c31..e08293b86d 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs @@ -641,18 +641,20 @@ public async Task SpecCpu2017ExecutorPinsRateCopies(string profile, string cpuId Assert.IsTrue(commands.Any(command => command.Contains($"--config {Path.GetFileName(pinnedPath)}") && command.Contains("--copies 4"))); } - [TestCase(2, 2, "0,48", "0,48")] - [TestCase(2, 3, "0,1,48", "0,1,48")] - [TestCase(2, 4, "0,1,48,49", "0,1,48,49")] - [TestCase(2, 40, "0-19,48-67", "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67")] - [TestCase(3, 2, "0,32", "0,32")] - [TestCase(3, 3, "0,32,64", "0,32,64")] - [TestCase(3, 40, "0-13,32-44,64-76", "0,1,2,3,4,5,6,7,8,9,10,11,12,13,32,33,34,35,36,37,38,39,40,41,42,43,44,64,65,66,67,68,69,70,71,72,73,74,75,76")] - public async Task SpecCpu2017ExecutorSelectsSymmetricNumaAffinity(int nodeCount, int copyCount, string expectedCoreSpec, string expectedCpus) + [TestCase(2, 2, "0,48", "0,48", null)] + [TestCase(2, 3, "0,1,48", "0,1,48", null)] + [TestCase(2, 4, "0,1,48,49", "0,1,48,49", null)] + [TestCase(2, 40, "0-19,48-67", "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67", null)] + [TestCase(3, 2, "0,32", "0,32", null)] + [TestCase(3, 3, "0,32,64", "0,32,64", null)] + [TestCase(3, 40, "0-13,32-44,64-76", "0,1,2,3,4,5,6,7,8,9,10,11,12,13,32,33,34,35,36,37,38,39,40,41,42,43,44,64,65,66,67,68,69,70,71,72,73,74,75,76", null)] + [TestCase(2, 2, "0,48", "0,48", "symmetric")] + [TestCase(2, 4, "0,1,48,49", "0,1,48,49", "SyMmEtRiC")] + public async Task SpecCpu2017ExecutorSelectsSymmetricNumaAffinity(int nodeCount, int copyCount, string expectedCoreSpec, string expectedCpus, string affinity) { this.SetupLinux(Architecture.Arm64); - this.SetupInstalledAffinityRun(string.Empty); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinity)] = true; + this.SetupInstalledAffinityRun(affinity ?? string.Empty); + this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinitySymmetric)] = affinity == null; this.mockFixture.Parameters[nameof(SpecCpuExecutor.CopyCount)] = copyCount; int coresPerNode = 96 / nodeCount; @@ -680,7 +682,9 @@ public async Task SpecCpu2017ExecutorSelectsSymmetricNumaAffinity(int nodeCount, using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) { + Assert.IsTrue(executor.EnableNumaProcessorAffinitySymmetric); await executor.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + Assert.IsTrue(executor.EnableNumaProcessorAffinitySymmetric); } StringAssert.Contains($"bind = {expectedCpus}", pinnedConfig); @@ -698,7 +702,7 @@ public void SpecCpu2017ExecutorRejectsManualAndSymmetricAffinityTogether() { this.SetupLinux(); this.SetupInstalledAffinityRun("0-3"); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinity)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinitySymmetric)] = true; using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) { @@ -711,7 +715,7 @@ public void SpecCpu2017ExecutorRejectsSymmetricAffinityWhenTopologyIsUnreadable( { this.SetupLinux(); this.SetupInstalledAffinityRun(string.Empty); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinity)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinitySymmetric)] = true; this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => new InMemoryProcess { StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, @@ -732,7 +736,7 @@ public void SpecCpu2017ExecutorRejectsSymmetricAffinityOnSpeedRuns() { this.SetupLinux(); this.SetupInstalledAffinityRun(string.Empty); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinity)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinitySymmetric)] = true; this.mockFixture.Parameters[nameof(SpecCpuExecutor.SpecProfile)] = "intspeed"; using (TestSpecCpu2017Executor executor = new TestSpecCpu2017Executor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) @@ -746,7 +750,7 @@ public void SpecCpu2017ExecutorRejectsSymmetricAffinityWhenTopologyHasTooFewCpus { this.SetupLinux(); this.SetupInstalledAffinityRun(string.Empty); - this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinity)] = true; + this.mockFixture.Parameters[nameof(SpecCpuExecutor.EnableNumaProcessorAffinitySymmetric)] = true; this.mockFixture.ProcessManager.OnCreateProcess = (exe, arguments, workingDir) => new InMemoryProcess { StartInfo = new ProcessStartInfo { FileName = exe, Arguments = arguments }, diff --git a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs index e745cec23a..38ebd5e5d8 100644 --- a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs @@ -161,6 +161,7 @@ public int CopyCount /// /// Optional list of physical CPU IDs to assign to SPECrate copies, one CPU per copy. + /// Set to "symmetric" (case-insensitive) to select CPUs evenly across NUMA nodes automatically. /// For two 48-CPU NUMA nodes (CPUs 0-47 and 48-95): /// 2 copies: NumaProcessorAffinity=0,48; /// 4 copies: NumaProcessorAffinity=0,1,48,49; @@ -180,13 +181,14 @@ public string NumaProcessorAffinity /// /// When enabled, distributes SPECrate copies as evenly as available CPUs allow across NUMA nodes, /// assigning one CPU per copy using numactl. - /// Defaults to false; cannot be combined with manual NumaProcessorAffinity. + /// Defaults to false unless NumaProcessorAffinity is "symmetric"; cannot be combined with manual CPU IDs. /// - public bool EnableNumaProcessorAffinity + public bool EnableNumaProcessorAffinitySymmetric { get { - return this.Parameters.GetValue(nameof(SpecCpuExecutor.EnableNumaProcessorAffinity), false); + return string.Equals(this.NumaProcessorAffinity, "symmetric", StringComparison.OrdinalIgnoreCase) + || this.Parameters.GetValue(nameof(SpecCpuExecutor.EnableNumaProcessorAffinitySymmetric), false); } } @@ -259,9 +261,10 @@ protected async Task ExecuteSpecCpuAsync(EventContext telemetryContext, Cancella { using (BackgroundOperations profiling = BackgroundOperations.BeginProfiling(this, cancellationToken)) { - if (this.EnableNumaProcessorAffinity) + if (this.NumaProcessorAffinity == "symmetric") { stage = "SymmetricAffinityConfiguration"; + this.Parameters[nameof(this.NumaProcessorAffinity)] = "symmetric"; string affinity = await this.GetSymmetricNumaProcessorAffinityAsync(cancellationToken); this.Parameters[nameof(this.NumaProcessorAffinity)] = affinity; this.MetadataContract.Add(nameof(this.NumaProcessorAffinity), affinity, MetadataContract.ScenarioCategory); @@ -770,9 +773,10 @@ private async Task GetSymmetricNumaProcessorAffinityAsync(CancellationTo throw new NotSupportedException("Symmetric NUMA processor affinity is supported only for Linux intrate and fprate runs."); } - if (!string.IsNullOrWhiteSpace(this.NumaProcessorAffinity)) + if (!string.IsNullOrWhiteSpace(this.NumaProcessorAffinity) && + !string.Equals(this.NumaProcessorAffinity, "symmetric", StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentException("NumaProcessorAffinity and EnableNumaProcessorAffinity cannot both be specified.", nameof(this.NumaProcessorAffinity)); + throw new ArgumentException("NumaProcessorAffinity and EnableNumaProcessorAffinitySymmetric cannot both be specified.", nameof(this.NumaProcessorAffinity)); } if (this.CopyCount <= 0) diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json index 7f4d4409e6..601d2854ed 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-FPRATE.json @@ -10,7 +10,7 @@ "RecommendedMinimumExecutionTime": "(4-cores)=02:00:00,(16-cores)=05:00:00,(64-cores)=10:00:00", "SupportedPlatforms": "linux-x64,linux-arm64,win-x64,win-arm64", "SupportedOperatingSystems": "AzureLinux,CentOS,Debian,OpenSuse,RedHat,Ubuntu,Windows", - "Notes": "Private package speccpu.2017.1.1.9-danielbowers.zip supports Ubuntu 24; SPEC CPU 2017 was not designed for win-arm64. EnableNumaProcessorAffinity (Linux rate only): true balances copies across NUMA nodes, one CPU each; false (default) does not pin.", + "Notes": "Private package speccpu.2017.1.1.9-danielbowers.zip supports Ubuntu 24; SPEC CPU 2017 was not designed for win-arm64. NumaProcessorAffinity (Linux rate only): 'symmetric' balances copies across NUMA nodes, one CPU each; explicit CPU IDs pin copies manually; empty (default) does not pin.", "Limitations": "The following benchmarks don't work on Windows: 507, 511, 521, 526, 527, 538" }, "Parameters": { @@ -20,7 +20,7 @@ "RunPeak": false, "ThreadCount": "{LogicalCoreCount}", "CopyCount": "{LogicalCoreCount}", - "EnableNumaProcessorAffinity": false, + "NumaProcessorAffinity": "", "BaseOptimizingFlags": "-O2 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches", "PeakOptimizingFlags": "-O3 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches" }, @@ -36,7 +36,7 @@ "RunPeak": "$.Parameters.RunPeak", "ThreadCount": "$.Parameters.ThreadCount", "CopyCount": "$.Parameters.CopyCount", - "EnableNumaProcessorAffinity": "$.Parameters.EnableNumaProcessorAffinity", + "NumaProcessorAffinity": "$.Parameters.NumaProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json index d25ba4245a..0a530d41e0 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-SPECCPU-INTRATE.json @@ -10,7 +10,7 @@ "RecommendedMinimumExecutionTime": "(8-cores)=04:00:00,(16-cores)=08:00:00,(64-cores)=10:00:00", "SupportedPlatforms": "linux-x64,linux-arm64,win-x64", "SupportedOperatingSystems": "AzureLinux,CentOS,Debian,OpenSuse,RedHat,Ubuntu,Windows", - "Notes": "Private package speccpu.2017.1.1.9-danielbowers.zip supports Ubuntu 24; SPEC CPU 2017 was not designed for win-arm64. EnableNumaProcessorAffinity (Linux rate only): true balances copies across NUMA nodes, one CPU each; false (default) does not pin.", + "Notes": "Private package speccpu.2017.1.1.9-danielbowers.zip supports Ubuntu 24; SPEC CPU 2017 was not designed for win-arm64. NumaProcessorAffinity (Linux rate only): 'symmetric' balances copies across NUMA nodes, one CPU each; explicit CPU IDs pin copies manually; empty (default) does not pin.", "Limitations": "The following benchmarks don't work on Windows: 500, 502, 520, 523, 531" }, "Parameters": { @@ -20,7 +20,7 @@ "RunPeak": false, "ThreadCount": "{LogicalCoreCount}", "CopyCount": "{LogicalCoreCount}", - "EnableNumaProcessorAffinity": false, + "NumaProcessorAffinity": "", "BaseOptimizingFlags": "-O2 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches", "PeakOptimizingFlags": "-O3 -march={calculate((\"{Architecture}\" == \"x64\") ? \"x86-64-v3\" : \"armv8.2-a\")} -flto -frecord-gcc-switches" }, @@ -36,7 +36,7 @@ "RunPeak": "$.Parameters.RunPeak", "ThreadCount": "$.Parameters.ThreadCount", "CopyCount": "$.Parameters.CopyCount", - "EnableNumaProcessorAffinity": "$.Parameters.EnableNumaProcessorAffinity", + "NumaProcessorAffinity": "$.Parameters.NumaProcessorAffinity", "BaseOptimizingFlags": "$.Parameters.BaseOptimizingFlags", "PeakOptimizingFlags": "$.Parameters.PeakOptimizingFlags" } diff --git a/website/docs/workloads/speccpu/speccpu-profiles.md b/website/docs/workloads/speccpu/speccpu-profiles.md index 5e1b026096..884e9b868f 100644 --- a/website/docs/workloads/speccpu/speccpu-profiles.md +++ b/website/docs/workloads/speccpu/speccpu-profiles.md @@ -44,7 +44,7 @@ for evaluating the performance of the CPU for processing calculations. | RunPeak | Optional. True to run the workload 'Peak' scenario, False to run the workload 'Base' scenario. | false (Base) | | ThreadCount | Optional. Determines the number of threads to use for running the benchmark. | # logical processors | | CopyCount | Optional. Determines the number of copies of the benchmark to run concurrently. | # logical processors | - | EnableNumaProcessorAffinity | Optional. Linux SPECrate only. Balances copies across NUMA nodes as available CPUs allow, assigning one CPU per copy using `numactl`; false leaves copies unpinned. | false | + | NumaProcessorAffinity | Optional. Linux SPECrate only. Set to `symmetric` to balance copies across NUMA nodes, one CPU per copy using `numactl`, or provide CPU IDs to pin copies manually. Empty leaves copies unpinned. | empty | | BaseOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Base' scenario |
  • X64 systems: -g -O2 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O2 -march=armv8.2-a -flto -frecord-gcc-switches
| | PeakOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Peak' scenario |
  • X64 systems: -g -O3 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O3 -march=armv8.2-a -flto -frecord-gcc-switches
| @@ -65,7 +65,7 @@ for evaluating the performance of the CPU for processing calculations. * **Notes** On Windows, only the following benchmarks are supported: 503.bwaves_r, 508.namd_r, 510.parest_r, 519.lbm_r, 544.nab_r, 549.fotonik3d_r, 554.roms_r. - The Linux `EnableNumaProcessorAffinity` examples in the INTRATE section below also apply to this profile. + The Linux `NumaProcessorAffinity` examples in the INTRATE section below also apply to this profile. ``` bash # Execute the workload profile @@ -157,7 +157,7 @@ for evaluating the performance of the CPU for processing calculations. | RunPeak | Optional. True to run the workload 'Peak' scenario, False to run the workload 'Base' scenario. | false (Base) | | ThreadCount | Optional. Determines the number of threads to use for running the benchmark. | # logical processors | | CopyCount | Optional. Determines the number of copies of the benchmark to run concurrently. | # logical processors | - | EnableNumaProcessorAffinity | Optional. Linux SPECrate only. Balances copies across NUMA nodes as available CPUs allow, assigning one CPU per copy using `numactl`; false leaves copies unpinned. | false | + | NumaProcessorAffinity | Optional. Linux SPECrate only. Set to `symmetric` to balance copies across NUMA nodes, one CPU per copy using `numactl`, or provide CPU IDs to pin copies manually. Empty leaves copies unpinned. | empty | | BaseOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Base' scenario |
  • X64 systems: -g -O2 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O2 -march=armv8.2-a -flto -frecord-gcc-switches
| | PeakOptimizationFlags | Optional. Optimization flags to pass to the GCC compiler when running the 'Peak' scenario |
  • X64 systems: -g -O3 -march=x86-64-v3 -flto -frecord-gcc-switches
  • ARM64 systems: -g -O3 -march=armv8.2-a -flto -frecord-gcc-switches
| @@ -177,17 +177,17 @@ for evaluating the performance of the CPU for processing calculations. * **Notes** On Windows, only the following benchmarks are supported: 505.mcf_r, 525.x264_r, 541.leela_r, 548.exchange2_r, 557.xz_r. - On Linux, `EnableNumaProcessorAffinity=true` selects CPUs from `numactl --hardware`, giving each node as even a share of `CopyCount` as its available CPUs allow. If `CopyCount` is not divisible by the number of nodes, lower-numbered nodes receive the extra copies. Pinned copies use `numactl --localalloc` for local memory allocation. These examples assume 96 CPUs on two NUMA nodes: CPUs 0-47 on node 0 and 48-95 on node 1. Check the actual topology with `numactl --hardware`. + On Linux, `NumaProcessorAffinity=symmetric` selects CPUs from `numactl --hardware`, giving each node as even a share of `CopyCount` as its available CPUs allow. If `CopyCount` is not divisible by the number of nodes, lower-numbered nodes receive the extra copies. Pinned copies use `numactl --localalloc` for local memory allocation. These examples assume 96 CPUs on two NUMA nodes: CPUs 0-47 on node 0 and 48-95 on node 1. Check the actual topology with `numactl --hardware`. ``` bash # Two INTRATE copies: CPU 0 on node 0 and CPU 48 on node 1. - ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=2,,,EnableNumaProcessorAffinity=true" --logger=csv + ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=2,,,NumaProcessorAffinity=symmetric" --logger=csv # Four INTRATE copies: CPUs 0-1 on node 0 and 48-49 on node 1. - ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,EnableNumaProcessorAffinity=true" --logger=csv + ./VirtualClient --profile=PERF-SPECCPU-INTRATE.json --iterations=1 --parameters="CopyCount=4,,,NumaProcessorAffinity=symmetric" --logger=csv # Four FPRATE copies: the same two-per-node distribution. - ./VirtualClient --profile=PERF-SPECCPU-FPRATE.json --iterations=1 --parameters="CopyCount=4,,,EnableNumaProcessorAffinity=true" --logger=csv + ./VirtualClient --profile=PERF-SPECCPU-FPRATE.json --iterations=1 --parameters="CopyCount=4,,,NumaProcessorAffinity=symmetric" --logger=csv ``` ## PERF-SPECCPU-INTSPEED.json From e3e14f6bf874e0706e82a4b25b8ba388290f72cf Mon Sep 17 00:00:00 2001 From: "Samuel Filho (WIPRO LIMITED)" Date: Wed, 21 Oct 2026 18:54:43 -0300 Subject: [PATCH 8/9] fix to have a better EnableNumaProcessorAffinitySymmetric approach --- .../VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs index 38ebd5e5d8..ed8d029298 100644 --- a/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/SPECcpu/SpecCpuExecutor.cs @@ -261,10 +261,10 @@ protected async Task ExecuteSpecCpuAsync(EventContext telemetryContext, Cancella { using (BackgroundOperations profiling = BackgroundOperations.BeginProfiling(this, cancellationToken)) { - if (this.NumaProcessorAffinity == "symmetric") + if (this.EnableNumaProcessorAffinitySymmetric) { stage = "SymmetricAffinityConfiguration"; - this.Parameters[nameof(this.NumaProcessorAffinity)] = "symmetric"; + this.Parameters[nameof(this.EnableNumaProcessorAffinitySymmetric)] = true; string affinity = await this.GetSymmetricNumaProcessorAffinityAsync(cancellationToken); this.Parameters[nameof(this.NumaProcessorAffinity)] = affinity; this.MetadataContract.Add(nameof(this.NumaProcessorAffinity), affinity, MetadataContract.ScenarioCategory); From f5d0f739cee37924ae5451f9c8635dd2fa8bc1ef Mon Sep 17 00:00:00 2001 From: "Samuel Filho (WIPRO LIMITED)" Date: Wed, 21 Oct 2026 20:52:34 -0300 Subject: [PATCH 9/9] I was covering 3 copies on three 32-core nodes from 96 size. from reviewer request, I added the 6, 8, 12, 13, and 14-copy cases. --- .../SPEC/SpecCpu2017ExecutorTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs index e08293b86d..f33e5e09e4 100644 --- a/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/SPEC/SpecCpu2017ExecutorTests.cs @@ -647,6 +647,11 @@ public async Task SpecCpu2017ExecutorPinsRateCopies(string profile, string cpuId [TestCase(2, 40, "0-19,48-67", "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67", null)] [TestCase(3, 2, "0,32", "0,32", null)] [TestCase(3, 3, "0,32,64", "0,32,64", null)] + [TestCase(3, 6, "0,1,32,33,64,65", "0,1,32,33,64,65", null)] + [TestCase(3, 8, "0-2,32-34,64,65", "0,1,2,32,33,34,64,65", null)] + [TestCase(3, 12, "0-3,32-35,64-67", "0,1,2,3,32,33,34,35,64,65,66,67", null)] + [TestCase(3, 13, "0-4,32-35,64-67", "0,1,2,3,4,32,33,34,35,64,65,66,67", null)] + [TestCase(3, 14, "0-4,32-36,64-67", "0,1,2,3,4,32,33,34,35,36,64,65,66,67", null)] [TestCase(3, 40, "0-13,32-44,64-76", "0,1,2,3,4,5,6,7,8,9,10,11,12,13,32,33,34,35,36,37,38,39,40,41,42,43,44,64,65,66,67,68,69,70,71,72,73,74,75,76", null)] [TestCase(2, 2, "0,48", "0,48", "symmetric")] [TestCase(2, 4, "0,1,48,49", "0,1,48,49", "SyMmEtRiC")]