Repository: carstenbauer/ThreadPinning.jl
Pull request #19 (2022-09-30 09:33:40)

Binding + Places and Environment Variables
**Binding strategy + places**
We move to `pinthreads(binding; places, kwargs...)` to separate the binding / distribution strategy (e.g. compact or spread) from the blocks that we want to bind to / distribute among (e.g. cores, sockets, or numa domains).

(Contains also some OMP-like variable work)

**New binding strategy: `:current`**
Pins threads to the cpu threads on which they are currently running.

**Environment variables**
* `JULIA_PINTHREADS`: can be any binding strategy symbol, e.g. `JULIA_PINTHREADS=compact`
* `JULIA_PINTHREADS`: can be any places symbol, e.g. `JULIA_PINTHREADS_PLACES=sockets`
* `JULIA_TP_AUTOUPDATE`: When turned off, we won't query `lscpu` when `using` the package but will use the sysinfo obtained during precompilation. (only safe if run on the same machine!)

**TODO**
* [ ] Optimize new `pinthreads`
* [ ] Update documentation
  * [ ] make sure that old things still work
  * [ ] document env variables


Patch:
diff --git a/Project.toml b/Project.toml
index 9ea8e30..cd7a566 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "ThreadPinning"
 uuid = "811555cd-349b-4f26-b7bc-1f208b848042"
 authors = ["Carsten Bauer <crstnbr@gmail.com> and contributors"]
-version = "0.4.6"
+version = "0.5.0-DEV"
 
 [deps]
 DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
diff --git a/docs/src/examples/ex_pinning_julia_threads.md b/docs/src/examples/ex_pinning_julia_threads.md
index a163f14..35cee73 100644
--- a/docs/src/examples/ex_pinning_julia_threads.md
+++ b/docs/src/examples/ex_pinning_julia_threads.md
@@ -54,4 +54,15 @@ pinthread(1,39)
 threadinfo(; color=false)
 ```
 
-If you want to pin the calling thread you can simply use [`pinthread(cpuid)`](@ref).
\ No newline at end of file
+If you want to pin the calling thread you can simply use [`pinthread(cpuid)`](@ref).
+
+## Environment variables
+
+**Pinning:**
+
+The following environment variables can be used to specify the desired pinning before starting Julia. Julia Threads will then automatically get pinned during the initialization of the ThreadPinning package, i.e. when calling `using ThreadPinning`.
+* `JULIA_PIN`: Can be set to any binding / pinning strategy symbol supported by [`pinthreads`](@ref), e.g. `JULIA_PIN=compact`.
+* `JULIA_PLACES` (optional): Can be set to any places symbol supported by [`pinthreads`](@ref), e.g. `JULIA_PLACES=sockets`.
+
+**Other:**
+* `JULIA_TP_AUTOUPDATE`: When set to `false`, ThreadPinning.jl won't query lscpu when `using` the package but will use the system information obtained during precompilation. This can drastically reduce the `using ThreadPinning` time but is only safe if the package is used on the same system used for precompilation (i.e. often not the case on clusters!).
\ No newline at end of file
diff --git a/src/ThreadPinning.jl b/src/ThreadPinning.jl
index b88a186..d5197aa 100644
--- a/src/ThreadPinning.jl
+++ b/src/ThreadPinning.jl
@@ -4,7 +4,7 @@ module ThreadPinning
 using Base.Threads: @threads, nthreads, threadid
 using Libdl
 using LinearAlgebra
-using Random
+import Random
 using DelimitedFiles
 
 # includes
@@ -14,6 +14,7 @@ include("lscpu_examples.jl")
 include("libs/libc.jl")
 include("libs/libuv.jl")
 include("libs/libpthread.jl")
+include("omp.jl")
 include("blas.jl")
 include("querying.jl")
 include("pinning.jl")
@@ -22,18 +23,44 @@ include("latency.jl")
 
 # initialization
 function __init__()
-    update_sysinfo!()
+    if lowercase(get(ENV, "JULIA_TP_AUTOUPDATE", "true")) != "false"
+        update_sysinfo!()
+    end
+
+    JULIA_PIN = get(ENV, "JULIA_PIN", nothing)
+    JULIA_PLACES = get(ENV, "JULIA_PLACES", nothing)
+    if !isnothing(JULIA_PIN)
+        try
+            binding = Symbol(lowercase(JULIA_PIN))
+            if !isnothing(JULIA_PLACES)
+                pinthreads(binding; places = Symbol(lowercase(JULIA_PLACES)))
+            else
+                pinthreads(binding)
+            end
+        catch err
+            @warn("Ignoring unsupported settings:", JULIA_PIN,
+                  JULIA_PLACES)
+        end
+    end
+    return nothing
 end
 
 # precompile
 import SnoopPrecompile
 SnoopPrecompile.@precompile_all_calls begin
     sysinfo()
-    threadinfo()
-    pinthreads(:compact)
+    # threadinfo()
     pinthread(0)
-    pinthreads(0:(Threads.nthreads() - 1))
-    pinthreads(collect(0:(Threads.nthreads() - 1)))
+    pinthreads(0:(nthreads() - 1))
+    pinthreads(collect(0:(nthreads() - 1)))
+    pinthreads(:compact)
+    pinthreads(:spread)
+    pinthreads(:random)
+    pinthreads(:current)
+    pinthreads(:compact; places=Cores())
+    pinthreads(:compact; places=CPUThreads())
+    pinthreads(:spread; places=NUMA())
+    pinthreads(:spread; places=Sockets())
     getcpuid()
     getcpuids()
     nsockets()
@@ -69,5 +96,13 @@ export threadinfo,
        ishyperthread,
        cpuids_per_socket,
        cpuids_per_numa,
-       cpuids_all
+       cpuids_all,
+       CPUThreads,
+       Cores,
+       Sockets,
+       NUMA,
+       CompactBind,
+       SpreadBind,
+       RandomBind,
+       CurrentBind
 end
diff --git a/src/omp.jl b/src/omp.jl
new file mode 100644
index 0000000..b524431
--- /dev/null
+++ b/src/omp.jl
@@ -0,0 +1,63 @@
+# OMP_PLACES
+function _omp_places_env_parse(places::AbstractString)::Vector{Vector{Int}}
+    if places == "threads"
+        return [cpuids_all()]
+    elseif places == "sockets"
+        return cpuids_per_socket()
+    elseif places == "cores"
+        return [filter(!ishyperthread, cpuids_all())]
+    end
+
+    # TODO: support "threads(3)" etc. limiting the number of places
+
+    if contains(places, ',')
+        curly_braces_contents = [last(x)
+                                 for x in split.(split(places, '}'), '{')
+                                 if !isempty(last(x))]
+        places_jl = _omp_curly_braces_content_to_jl.(curly_braces_contents)
+        return places_jl
+    end
+end
+
+function _omp_curly_braces_content_to_jl(content::AbstractString)
+    if contains(content, ':')
+        _omp_range_to_jl_array(content)
+    elseif contains(content, ',')
+        _omp_list_to_jl_array(content)
+    else
+        throw(ArgumentError("Input \"$content\" isn't in OMP-style."))
+    end
+end
+
+function _omp_list_to_jl_array(list::AbstractString)
+    # contains(list, ',') || throw(ArgumentError("Input list $list isn't OMP style."))
+    nums = parse.(Int, split(list, ','))
+    return nums
+end
+
+function _omp_range_to_jl_array(range::AbstractString)
+    # contains(range, ':') || throw(ArgumentError("Input range $range isn't OMP style."))
+    num_strs = split(range, ':')
+    if length(num_strs) == 2 && !contains(first(num_strs), '{')
+        nums = parse.(Int, num_strs)
+        return @inbounds collect(nums[1]:(nums[1] + nums[2] - 1))
+    else
+        throw(ArgumentError("Not a supported OMP-style range."))
+    end
+end
+
+# OMP_PROC_BIND
+function _omp_proc_bind_env_parse(bind::AbstractString)::Union{Nothing, Symbol}
+    bindl = lowercase(bind)
+    if bindl == "false"
+        return nothing # don't pin threads at all
+    elseif bindl == "master"
+        throw(ArgumentError("Binding option \"master\" isn't supported."))
+    elseif bindl == "close"
+        return :close
+    elseif bindl == "spread"
+        return :spread
+    else
+        throw(ArgumentError("Unknown binding option \"$bind\"."))
+    end
+end
diff --git a/src/pinning.jl b/src/pinning.jl
index a75fd13..a69c76d 100644
--- a/src/pinning.jl
+++ b/src/pinning.jl
@@ -1,3 +1,4 @@
+# "Low-level" pinthreads
 """
     pinthread(cpuid::Integer; warn::Bool = true)
 
@@ -42,68 +43,113 @@ function pinthreads(cpuids::AbstractVector{<:Integer}; warn::Bool = true)
     return nothing
 end
 
-"""
-    pinthreads(strategy::Symbol[; nthreads, warn, kwargs...])
-Pin the first `1:nthreads` Julia threads according to the given pinning `strategy`.
-Per default, `nthreads == Threads.nthreads()`
-
-Allowed strategies:
-* `:compact`: pins to the first `nthreads` cpu threads while trying to avoid using hyperthreads (i.e. moving to next socket before using hyperthreads). If `hyperthreads=true`, hyperthreads will be used before moving to the next socket, if necessary.
-* `:scatter` or `:spread` or `sockets`: pins to all available sockets in an alternating / round robin fashion.
-* `:numa`: pins to all available NUMA nodes in an alternating / round robin fashion.
-* `:random` or `:rand`: pins threads to random cpu threads (ensures that no cpu thread is double occupied). By default (`hyperthreads=false`), hyperthreads will be ignored.
-* `:firstn`: pins to the cpuids `0:nthreads-1`
-"""
-function pinthreads(strategy::Symbol; nthreads = Threads.nthreads(), warn::Bool = true,
-                    kwargs...)
-    warn && _check_environment()
-    cpuids = if strategy == :compact
-        _strategy_compact(; kwargs...)
-    elseif strategy in (:scatter, :spread, :sockets)
-        _strategy_scatter(; kwargs...)
-    elseif strategy == :numa
-        _strategy_numa(; kwargs...)
-    elseif strategy in (:rand, :random)
-        _strategy_random(; kwargs...)
-    elseif strategy == :firstn
-        _strategy_firstn(nthreads)
-    else
-        throw(ArgumentError("Unknown pinning strategy."))
-    end
-    pinthreads(@view(cpuids[1:nthreads]); warn = false)
+# Types
+abstract type Places end
+struct CPUThreads <: Places end
+struct Cores <: Places end
+struct NUMA <: Places end
+struct Sockets <: Places end
+
+abstract type PinningStrategy end
+struct CompactBind <: PinningStrategy end
+struct SpreadBind <: PinningStrategy end
+struct RandomBind <: PinningStrategy end
+struct CurrentBind <: PinningStrategy end
+
+# Places logic
+_places_symbol2singleton(s::Symbol) = _places_symbol2singleton(Val{s})
+function _places_symbol2singleton(::Type{Val{T}}) where {T}
+    throw(ArgumentError("Unknown places symbol."))
 end
+_places_symbol2singleton(::Type{Val{:threads}}) = CPUThreads()
+_places_symbol2singleton(::Type{Val{:cores}}) = Cores()
+_places_symbol2singleton(::Type{Val{:numa}}) = NUMA()
+_places_symbol2singleton(::Type{Val{:NUMA}}) = NUMA()
+_places_symbol2singleton(::Type{Val{:sockets}}) = Sockets()
 
-_strategy_firstn(nthreads) = return 0:(nthreads-1)
-function _strategy_random(; hyperthreads = false)
-    if !hyperthreads
-        cpuids = shuffle!(filter(!ishyperthread, cpuids_all()))
+getplaces_cpuids(s::Symbol) = getplaces_cpuids(_places_symbol2singleton(s))
+function getplaces_cpuids(::CPUThreads)
+    if hyperthreading_is_enabled()
+        [[cpuid]
+         for cpuid in interweave(filter(!ishyperthread, cpuids_all()),
+                                 filter(ishyperthread, cpuids_all()))]
     else
-        cpuids = shuffle(cpuids_all())
+        [[cpuid] for cpuid in cpuids_all()]
     end
-    return cpuids
 end
-function _strategy_compact(; hyperthreads = false)
-    if !hyperthreads
-        cpuids_noht = filter(!ishyperthread, cpuids_all())
-        cpuids_ht = filter(ishyperthread, cpuids_all())
-        cpuids = vcat(cpuids_noht, cpuids_ht)
+function getplaces_cpuids(::Cores)
+    if hyperthreading_is_enabled()
+        [[cpuid] for cpuid in filter(!ishyperthread, cpuids_all())] # should HT be entirely ignored here?
     else
-        cpuids = cpuids_all()
+        [[cpuid] for cpuid in cpuids_all()]
     end
-    return cpuids
 end
-function _strategy_scatter()
-    cpuids = interweave(cpuids_per_socket()...)
-    return cpuids
+getplaces_cpuids(::NUMA) = cpuids_per_numa()
+getplaces_cpuids(::Sockets) = cpuids_per_socket()
+getplaces_cpuids(v::AbstractVector{<:AbstractVector{<:Integer}}) = v
+
+_default_places(::PinningStrategy) = Cores() # fallback
+_default_places(::CompactBind) = Cores()
+_default_places(::SpreadBind) = Sockets()
+_default_places(::RandomBind) = CPUThreads()
+
+# Binding strategy logic
+_pinning_symbol2singleton(s::Symbol)::PinningStrategy = _pinning_symbol2singleton(Val{s})
+_pinning_symbol2singleton(::Type{Val{:compact}}) = CompactBind()
+_pinning_symbol2singleton(::Type{Val{:close}}) = CompactBind()
+_pinning_symbol2singleton(::Type{Val{:spread}}) = SpreadBind()
+_pinning_symbol2singleton(::Type{Val{:scatter}}) = SpreadBind()
+_pinning_symbol2singleton(::Type{Val{:random}}) = RandomBind()
+_pinning_symbol2singleton(::Type{Val{:current}}) = CurrentBind()
+
+function getcpuids_pinning(s::Symbol, places; kwargs...)
+    getcpuids_pinning(_pinning_symbol2singleton(s), places; kwargs...)
+end
+function getcpuids_pinning(::PinningStrategy, places; kwargs...)
+    throw(ArgumentError("Unknown pinning strategy."))
+end
+getcpuids_pinning(::CompactBind, places; kwargs...) = reduce(vcat, getplaces_cpuids(places))
+getcpuids_pinning(::SpreadBind, places; kwargs...) = interweave(getplaces_cpuids(places)...)
+function getcpuids_pinning(::RandomBind, places; kwargs...)
+    reduce(vcat, Random.shuffle(getplaces_cpuids(places)))
+end
+getcpuids_pinning(::CurrentBind, args...; kwargs...) = getcpuids()
+
+# High-level pinthreads
+"""
+    pinthreads(pinning[; places, nthreads, warn, kwargs...])
+Pins the first `1:nthreads` Julia threads according to the given `pinning` strategy to the given `places`.
+Per default, `nthreads == Threads.nthreads()` and a reasonable value for `places` is chosen based on `pinning`.
+
+**Pinning strategies** (`pinning`):
+* `:compact` or `:close`: pins to `places` one after another.
+* `:spread` or `scatter`: pins to `places` in an alternating / round robin fashion.
+* `:random`: shuffles the given `places` and then pins to them compactly.
+* `:current`: pins threads to the cpu threads where they are currently running (ignores `places`).
+
+**Places** (`places`):
+* `:cores` or `Cores()`: all the cores of the system
+* `:threads` or `CPUThreads()`: all the cpu threads of the system (equal to `:cores` if there is one cpu thread per core, e.g. no hyperthreading)
+* `:sockets` or `Sockets()`: the sockets of the system
+* `:numa` or `NUMA()`: the NUMA domains of the system
+* An `AbstractVector{<:AbstractVector{<:Integer}}` of cpu ids that defines the places explicitly
+"""
+function pinthreads(pinning::PinningStrategy;
+                    places::Union{Places, Symbol,
+                                  AbstractVector{<:AbstractVector{<:Integer}}} = _default_places(pinning),
+                    nthreads = Base.Threads.nthreads(), warn::Bool = true,
+                    kwargs...)
+    warn && _check_environment()
+    cpuids = getcpuids_pinning(pinning, places; kwargs...)
+    @views pinthreads(cpuids[1:nthreads]; warn = false)
 end
-function _strategy_numa()
-    cpuids = interweave(cpuids_per_numa()...)
-    return cpuids
+function pinthreads(pinning::Symbol; kwargs...)
+    pinthreads(_pinning_symbol2singleton(pinning); kwargs...)
 end
 
 # Potentially throw warnings if the environment is such that thread pinning might not work.
 function _check_environment()
-    if Threads.nthreads() > 1 && mkl_is_loaded() && mkl_get_dynamic() == 1
+    if Base.Threads.nthreads() > 1 && mkl_is_loaded() && mkl_get_dynamic() == 1
         @warn("Found MKL_DYNAMIC == true. Be aware that calling an MKL function can spoil the pinning of Julia threads! Use `ThreadPinning.mkl_set_dynamic(0)` to be safe. See https://discourse.julialang.org/t/julia-thread-affinity-not-persistent-when-calling-mkl-function/74560/3.")
     end
     return nothing
diff --git a/src/threadinfo.jl b/src/threadinfo.jl
index fa885f4..04e0ffa 100644
--- a/src/threadinfo.jl
+++ b/src/threadinfo.jl
@@ -11,7 +11,7 @@ Keyword arguments:
 """
 function threadinfo(; blas = false, hints = false, color = true, kwargs...)
     # general info
-    jlthreads = Threads.nthreads()
+    jlthreads = Base.Threads.nthreads()
     thread_cpuids = getcpuids()
     occupied_cputhreads = length(unique(thread_cpuids))
     cputhreads = Sys.CPU_THREADS
@@ -83,7 +83,7 @@ function _visualize_affinity(;
     ncpuids = Sys.CPU_THREADS
     cpuids_grouped = if groupby in (:sockets, :socket)
         cpuids_per_socket()
-    elseif groupby == :numa
+    elseif groupby in (:numa, :NUMA)
         cpuids_per_numa()
     else
         [cpuids_all()]
diff --git a/src/utility.jl b/src/utility.jl
index ecffeb7..212e21f 100644
--- a/src/utility.jl
+++ b/src/utility.jl
@@ -18,7 +18,7 @@ macro tspawnat(thrdid, expr)
     var = esc(Base.sync_varname)
     tid = esc(thrdid)
     quote
-        if $tid < 1 || $tid > Threads.nthreads()
+        if $tid < 1 || $tid > Base.Threads.nthreads()
             throw(AssertionError("@tspawnat thread assignment ($($tid)) must be between 1 and Threads.nthreads() (1:$(Threads.nthreads()))"))
         end
         let $(letargs...)


Test patch:
diff --git a/test/omp_test.jl b/test/omp_test.jl
new file mode 100644
index 0000000..5636700
--- /dev/null
+++ b/test/omp_test.jl
@@ -0,0 +1,50 @@
+using Test
+using ThreadPinning
+
+@testset "OMP_PLACES" begin
+    @testset "_omp_list_to_jl_array" begin
+        @test ThreadPinning._omp_list_to_jl_array("0,1,2,3") == [0,1,2,3]
+        @test ThreadPinning._omp_list_to_jl_array("0,4,2,10") == [0,4,2,10]
+    end
+
+    @testset "_omp_range_to_jl_array" begin
+        @test_throws ArgumentError ThreadPinning._omp_range_to_jl_array("0,1,2,3")
+        # NUM:NUM
+        @test ThreadPinning._omp_range_to_jl_array("0:4") == [0,1,2,3]
+        @test ThreadPinning._omp_range_to_jl_array("4:4") == [4,5,6,7]
+        @test ThreadPinning._omp_range_to_jl_array("12:4") == [12,13,14,15]
+        # NUM:NUM:NUM (not supported yet)
+        @test_broken ThreadPinning._omp_range_to_jl_array("12:4:2")
+        # {NUMS...}:NUM:NUM (not supported yet)
+        @test_broken ThreadPinning._omp_range_to_jl_array("{0:1}:8:32")
+    end
+
+    @testset "Testcases (high-level)" begin
+        @test ThreadPinning._omp_places_env_parse("threads") isa Vector{Vector{Int}}
+        @test ThreadPinning._omp_places_env_parse("threads") == [cpuids_all()]
+        @test ThreadPinning._omp_places_env_parse("sockets") isa Vector{Vector{Int}}
+        @test ThreadPinning._omp_places_env_parse("sockets") == cpuids_per_socket()
+        @test ThreadPinning._omp_places_env_parse("cores") isa Vector{Vector{Int}}
+        @test ThreadPinning._omp_places_env_parse("cores") == [filter(!ishyperthread, cpuids_all())]
+
+        # not implemented yet
+        @test_broken ThreadPinning._omp_places_env_parse("threads(10)") isa Vector{Vector{Int}}
+        @test_broken ThreadPinning._omp_places_env_parse("threads(10)") == [cpuids_all()[1:10]]
+        @test_broken ThreadPinning._omp_places_env_parse("sockets(1)") isa Vector{Vector{Int}}
+        @test_broken ThreadPinning._omp_places_env_parse("sockets(1)") == cpuids_per_socket()[1:1]
+        @test_broken ThreadPinning._omp_places_env_parse("cores(10)") isa Vector{Vector{Int}}
+        @test_broken ThreadPinning._omp_places_env_parse("cores(10)") == [filter(!ishyperthread, cpuids_all())[1:10]]
+    end
+
+    @testset "Testcases (specific lists)" begin
+        desired_result = [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]]
+        OMP_PLACES_strs = [
+            "{0,1,2,3},{4,5,6,7},{8,9,10,11},{12,13,14,15}",
+            "{0:4},{4:4},{8:4},{12:4}",
+            "{0,1,2,3},{4:4},{8,9,10,11},{12:4}",
+        ]
+        for places in OMP_PLACES_strs
+            @test ThreadPinning._omp_places_env_parse(places) == desired_result
+        end
+    end
+end
diff --git a/test/pinning_test.jl b/test/pinning_test.jl
index 5edb613..676c7a9 100644
--- a/test/pinning_test.jl
+++ b/test/pinning_test.jl
@@ -47,22 +47,71 @@ end
     end
 end
 
-@testset "Thread Pinning (compact)" begin
-    @assert getcpuids() != 0:(nthreads() - 1)
-    @test isnothing(pinthreads(:compact; nthreads = 2))
+@testset "Thread Pinning (compact)" begin for binding in (:compact, :close)
+    pinthreads(:random; places = :threads)
+    @test isnothing(pinthreads(binding; nthreads = 2))
     @test getcpuids()[1:2] == 0:1
-    @test isnothing(pinthreads(:compact))
+    @test isnothing(pinthreads(binding))
     @test getcpuids() == 0:(nthreads() - 1)
-end
+end end
 
-@testset "Thread Pinning (scatter)" begin
-    @test isnothing(pinthreads(:scatter))
+@testset "Thread Pinning (spread)" begin for binding in (:spread, :scatter)
+    pinthreads(:random; places = :threads)
+    @test isnothing(pinthreads(binding))
     cpuids_after = getcpuids()
     @test check_compact_within_socket(cpuids_after)
-end
+end end
 
-@testset "Thread Pinning (numa)" begin
-    @test isnothing(pinthreads(:numa))
+@testset "Thread Pinning (numa)" begin for places in (:numa, :NUMA)
+    pinthreads(:random; places = :threads)
+    @test isnothing(pinthreads(:compact; places))
     cpuids_after = getcpuids()
     @test check_compact_within_numa(cpuids_after)
+end end
+
+@testset "Thread Pinning (current)" begin
+    pinthreads(:random; places = :threads)
+    cpuids_before = getcpuids()
+    @test isnothing(pinthreads(:current))
+    cpuids_after = getcpuids()
+    @test cpuids_before == cpuids_after
+end
+
+@testset "Environment variables" begin
+    julia = Base.julia_cmd()
+    pkgdir = joinpath(@__DIR__, "..")
+
+    exec(s) = run(`$julia --project=$(pkgdir) -t 3 -e $s`).exitcode == 0
+    withenv("JULIA_PIN" => "compact") do
+        @test exec(`'using ThreadPinning, Test;
+            @test getcpuids() == cpuids_all()[1:Threads.nthreads()]'`)
+    end
+    withenv("JULIA_PIN" => "spread") do
+        @test exec(`'using ThreadPinning, Test;
+        function check_compact_within_socket(cpuids)
+            socket_cpuids = cpuids_per_socket()
+            for s in 1:nsockets()
+                cpuids_filtered = filter(x -> x in socket_cpuids[s], cpuids)
+                if cpuids_filtered != socket_cpuids[s][1:length(cpuids_filtered)]
+                    return false
+                end
+            end
+            return true
+        end
+        @test check_compact_within_socket(cpuids_all())'`)
+    end
+    withenv("JULIA_PIN" => "spread", "JULIA_PLACES" => "numa") do
+        @test exec(`'using ThreadPinning, Test;
+        function check_compact_within_numa(cpuids)
+            numa_cpuids = cpuids_per_numa()
+            for s in 1:nnuma()
+                cpuids_filtered = filter(x -> x in numa_cpuids[s], cpuids)
+                if cpuids_filtered != numa_cpuids[s][1:length(cpuids_filtered)]
+                    return false
+                end
+            end
+            return true
+        end
+        @test check_compact_within_numa(cpuids_all())'`)
+    end
 end
diff --git a/test/runtests.jl b/test/runtests.jl
index f903cc0..63ef139 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -9,4 +9,5 @@ Threads.nthreads() ≥ 2 ||
     @time @safetestset "Pinning" begin include("pinning_test.jl") end
     @time @safetestset "Querying" begin include("querying_test.jl") end
     @time @safetestset "threadinfo" begin include("threadinfo_test.jl") end
+    @time @safetestset "OMP" begin include("omp_test.jl") end
 end