Skip to content

allow affinity mask wider than 32 processors - #3242

Open
HuzaifaChaudary wants to merge 4 commits into
dotnet:masterfrom
HuzaifaChaudary:fix/affinity-over-32-processors
Open

allow affinity mask wider than 32 processors#3242
HuzaifaChaudary wants to merge 4 commits into
dotnet:masterfrom
HuzaifaChaudary:fix/affinity-over-32-processors

Conversation

@HuzaifaChaudary

@HuzaifaChaudary HuzaifaChaudary commented Aug 27, 2026

Copy link
Copy Markdown

problem

--affinity is parsed as int so any mask with a bit above 32 can not be passed at all.

[Option("affinity", Required = false, HelpText = "Affinity mask to set for the benchmark process")]
public int? Affinity { get; set; }

everything under it is already wider. the job stores affinity as IntPtr and FixAffinity in ProcessExtensions already masks against a 64 bit cpuMask and comments that the max supported affinity without cpu groups is 64. only the cli entry point was narrow.

fix

int? becomes ulong? on the option. unsigned so the 64th cpu is written as 9223372036854775808 the way the mask reads rather than as -9223372036854775808. thanks @timcassell for pushing for that.

three things had to move with it.

the perfonar projection. EnvironmentMode.ToPerfonar() did

Affinity = HasValue(AffinityCharacteristic) ? (int)Affinity : null

once a wider mask can actually reach that line the cast is a problem. on net10 IntPtr is nint so it truncates silently and on net472 the explicit operator goes through ToInt32() which throws OverflowException. so BdnEnvironment.Affinity becomes long? and the cast becomes (long). BdnEnvironment is internal so this is not a public api change.

it stays signed rather than unsigned on purpose. perfolizer 0.7.5 LightJsonSerializer has AppendInt(Int32) and AppendLong(Int64) and AppendDouble(Double) and no UInt64 case so a ulong there throws NotSupportedException: Unsupported type: System.UInt64. same bits either way.

the conversion to IntPtr. new IntPtr(long) is checked((int)value) on a 32 bit process so it throws instead of truncating. that would have made ConfigParser.Parse fail with an unhandled OverflowException on x86 rather than a normal option error. a full 32 processor mask of 0xFFFFFFFF does not fit in int either even though it is legal there. so the conversion goes through a helper that takes the pointer size

internal static bool TryConvertAffinity(ulong mask, int pointerSize, out IntPtr affinity)

which keeps the whole 64 bit mask on 8 bytes and takes the low 32 bits on 4 bytes and reports failure for anything wider so Validate can print an error. on a 64 bit process it is bit for bit the same as before.

a new env requirement. EnvRequirement.Platform64BitOnly so the wide mask parse tests skip rather than fail on a 32 bit runtime.

tests

in ConfigParserTests

  • UserCanSpecifyAffinity for a normal small mask
  • UserCanSpecifyAffinityBeyondThirtyTwoProcessors for 1UL << 40
  • UserCanSpecifyAffinityForTheSixtyFourthProcessor for 1UL << 63
  • AffinityThatFitsThirtyTwoBitsIsAcceptedByAThirtyTwoBitProcess
  • AffinityWiderThanThirtyTwoBitsIsRejectedByAThirtyTwoBitProcess
  • AffinityOfAnyWidthIsAcceptedByASixtyFourBitProcess

the last three take the pointer size as an argument so the 32 bit behaviour is checked on any machine.

they are real regression tests. with the option reverted to int? the wide mask test fails and the small one still passes. with the pointer size guard removed five of the nine theory cases fail.

verification

./build.cmd build and ./build.cmd unit-tests -e and ./build.cmd analyzer-tests -e which is what run-tests.yaml runs.

check result
build succeeded
unit tests 1073 total 1065 passed 0 failed 8 skipped
exporter tests 51 of 51
analyzer tests 20234 of 20234

net10.0 on macos arm64.

note

i did not touch cpu groups. this only widens what the cli can express up to the 64 that FixAffinity already supports.

closes #3231

the affinity cli option was parsed as int so a mask with any bit above 32
could not be passed at all even though the job stores it as IntPtr and
FixAffinity already handles a 64 bit mask

parsing it as long also means the perfonar model must hold long or the
value gets truncated on the way out
Copilot AI lite review requested due to automatic review settings August 27, 2026 21:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR widens the --affinity CLI option to accept masks beyond 32 bits, aligning the console entry point with the rest of the affinity pipeline (which already uses IntPtr and masks up to 64 bits).

Changes:

  • Change CommandLineOptions.Affinity from int? to long? so higher-bit masks can be parsed from the CLI.
  • Update Perfonar environment projection (EnvironmentMode.ToPerfonar() / BdnEnvironment) to avoid truncation/overflow when affinity exceeds int.
  • Add regression tests to ensure affinity parsing works for both small masks and a mask with a bit above 32.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/BenchmarkDotNet.Tests/Perfonar/PerfonarTests.cs Updates test helper signature to accept long? affinity for Perfonar output generation.
tests/BenchmarkDotNet.Tests/ConfigParserTests.cs Adds regression tests validating CLI affinity parsing for small and >32-bit masks.
src/BenchmarkDotNet/Models/BdnEnvironment.cs Widens internal Perfonar environment model affinity from int? to long? to prevent truncation/overflow.
src/BenchmarkDotNet/Jobs/EnvironmentMode.cs Updates affinity projection to Perfonar to use long instead of int.
src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs Widens CLI option type for --affinity from int? to long?.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/BenchmarkDotNet.Tests/ConfigParserTests.cs Outdated
Comment thread src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs Outdated
@HuzaifaChaudary

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

new IntPtr(1L << 40) throws OverflowException where IntPtr is four bytes, so
the test i added would fail on a 32 bit run rather than prove anything. added
a Platform64BitOnly requirement so it skips there, using the same
FactEnvSpecific mechanism the other platform bound tests already use

also added a test for the top bit. the 64th cpu is the last one FixAffinity
handles without cpu groups and its mask only fits in a signed long as the
negative value, so this pins down that it still reaches the right IntPtr
a mask is not a signed number and the 64th cpu needed to be written as
-9223372036854775808 to reach it. now it is written the way the mask reads

the perfonar model stays long. perfolizers LightJsonSerializer throws
Unsupported type: System.UInt64 so making that half unsigned breaks
PerfonarTableTest at runtime. the value is only carried there so a signed long
holds the same bits and nothing is lost

conversion to IntPtr goes through new IntPtr(unchecked((long)value)) which is
the same shape FixAffinity already uses
the option is now unsigned so a value above int.MaxValue can reach
new IntPtr(long) which throws on a 32 bit process instead of
truncating. that made ConfigParser.Parse fail with an unhandled
OverflowException rather than a normal option error.

the conversion now goes through TryConvertAffinity which takes the
pointer size. on 8 bytes it keeps the full 64 bit mask as before. on
4 bytes it takes the low 32 bits so a full 32 processor mask still
works and it reports failure for anything wider so Validate can print
an error.
@HuzaifaChaudary

Copy link
Copy Markdown
Author

follow up on the first copilot comment. i only fixed the test half of it and left the production half in place.

copilot said the overflow hits new IntPtr(1L << 40) and the cast to IntPtr in ConfigParser. i added Platform64BitOnly so the test skips on a 32 bit runtime but the parser itself still had the problem. new IntPtr(long) is checked((int)value) on a 32 bit process so it throws rather than truncating. Validate runs before CreateConfig and nothing catches it so --affinity 1099511627776 from an x86 host came out as an unhandled OverflowException instead of a normal option error. with the old int? option that was not reachable so this was a regression i added.

there is a second case that a plain reject would get wrong. a full 32 processor mask is 0xFFFFFFFF which is perfectly legal on a 32 bit process but it does not fit in int either so new IntPtr(long) throws on that too. it needs the low 32 bits taken not a rejection.

so the conversion goes through one helper that takes the pointer size:

internal static bool TryConvertAffinity(ulong mask, int pointerSize, out IntPtr affinity)
{
    if (pointerSize >= 8)
    {
        affinity = new IntPtr(unchecked((long)mask));
        return true;
    }

    affinity = new IntPtr(unchecked((int)mask));
    return mask <= uint.MaxValue;
}

Validate calls it with IntPtr.Size and writes an error when it comes back false. GetBaseJob calls it for the value. on a 64 bit process it is bit for bit what the line did before so nothing there changes.

passing the pointer size in is what makes the 32 bit path testable on a 64 bit machine. three theories in ConfigParserTests:

  • AffinityThatFitsThirtyTwoBitsIsAcceptedByAThirtyTwoBitProcess for 0b1010 and 1 << 31 and uint.MaxValue
  • AffinityWiderThanThirtyTwoBitsIsRejectedByAThirtyTwoBitProcess for 1 << 32 and 1 << 40 and 1 << 63
  • AffinityOfAnyWidthIsAcceptedByASixtyFourBitProcess to pin the 64 bit path

with the helper body swapped back to the old unguarded affinity = new IntPtr(unchecked((long)mask)); return true; five of the nine cases fail:

Failed AffinityWiderThanThirtyTwoBitsIsRejectedByAThirtyTwoBitProcess(affinity: 9223372036854775808)
   Assert.False() Failure
Failed AffinityWiderThanThirtyTwoBitsIsRejectedByAThirtyTwoBitProcess(affinity: 4294967296)
   Assert.False() Failure
Failed AffinityWiderThanThirtyTwoBitsIsRejectedByAThirtyTwoBitProcess(affinity: 1099511627776)
   Assert.False() Failure
Failed AffinityThatFitsThirtyTwoBitsIsAcceptedByAThirtyTwoBitProcess(affinity: 4294967295)
   Assert.Equal() Failure: Values differ
Failed AffinityThatFitsThirtyTwoBitsIsAcceptedByAThirtyTwoBitProcess(affinity: 2147483648)
   Assert.Equal() Failure: Values differ

Failed!  - Failed: 5, Passed: 134, Skipped: 3, Total: 142

put back it is 142 of 142 with 3 unrelated skips.

ran what run-tests.yaml runs rather than a plain dotnet test:

task result
./build.cmd build succeeded
./build.cmd unit-tests -e 1073 total 1065 passed 0 failed 8 skipped plus 51 of 51 exporter tests
./build.cmd analyzer-tests -e 20234 of 20234

net10.0 on macos arm64.

one more thing while i was in here. i wanted to back up my own claim about leaving BdnEnvironment.Affinity as long? rather than leave it as an assertion so i reflected over perfolizer 0.7.5:

Void AppendInt(Int32)
Void AppendLong(Int64)
Void AppendDouble(Double)

and called LightJsonSerializer.Serialize directly:

Int32 -> 5
Int64 -> 5
UInt64 -> NotSupportedException: Unsupported type: System.UInt64

so the perfonar model does have to stay signed until perfolizer grows a UInt64 case. it carries the same bits either way. the cli option stays ulong? as asked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failed to setting CPU affinity over 32-processor via cli arg

3 participants