Repository: ceph/ceph
Pull request #63137: crimson: simplify vstart crimson-osd reactor pinning
Author: login: athanatos
Labels: name: crimson
name: crimson backport tentacle
Created: 2025-05-06T16:15:13Z | Merged: 2025-05-12T17:08:06Z



<details>
<summary>Show available Jenkins commands</summary>

- `jenkins test classic perf` [Jenkins Job](https://jenkins.ceph.com/view/all/job/ceph-perf-classic/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-perf-pull-requests/config/definitions/ceph-perf-pull-requests.yml)
- `jenkins test crimson perf` [Jenkins Job](https://jenkins.ceph.com/view/all/job/ceph-perf-crimson/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-perf-pull-requests/config/definitions/ceph-perf-pull-requests.yml)
- `jenkins test signed` [Jenkins Job](https://jenkins.ceph.com/job/ceph-pr-commits/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-pr-commits/config/definitions/ceph-pr-commits.yml)
- `jenkins test make check` [Jenkins Job](https://jenkins.ceph.com/job/ceph-pull-requests/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-pull-requests/config/definitions/ceph-pull-requests.yml)
- `jenkins test make check arm64` [Jenkins Job](https://jenkins.ceph.com/job/ceph-pull-requests-arm64/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-pull-requests-arm64/config/definitions/ceph-pull-requests-arm64.yml)
- `jenkins test submodules` [Jenkins Job](https://jenkins.ceph.com/view/all/job/ceph-pr-submodules/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-pr-submodules/config/definitions/ceph-pr-commits.yml)
- `jenkins test dashboard` [Jenkins Job](https://jenkins.ceph.com/view/all/job/ceph-dashboard-pull-requests/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-dashboard-pull-requests/config/definitions/ceph-dashboard-pull-requests.yml)
- `jenkins test dashboard cephadm` [Jenkins Job](https://jenkins.ceph.com/view/all/job/ceph-dashboard-cephadm-e2e/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-dashboard-cephadm-e2e/config/definitions/ceph-dashboard-cephadm-e2e.yml)
- `jenkins test api` [Jenkins Job](https://jenkins.ceph.com/view/all/job/ceph-api/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-pr-api/config/definitions/ceph-pr-api.yml)
- `jenkins test docs` [ReadTheDocs](https://readthedocs.org/projects/ceph/) | [Github Workflow Definition](https://github.com/ceph/ceph/blob/main/.readthedocs.yml)
- `jenkins test ceph-volume all` [Jenkins Jobs](https://jenkins.ceph.com/view/ceph-volume%20PR/) | [Jenkins Jobs Definition](https://github.com/ceph/ceph-build/blob/main/ceph-volume-cephadm-prs/config/definitions/ceph-volume-pr.yml)
- `jenkins test windows` [Jenkins Job](https://jenkins.ceph.com/job/ceph-windows-pull-requests/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-windows-pull-requests/config/definitions/ceph-windows-pull-requests.yml)
- `jenkins test rook e2e` [Jenkins Job](https://jenkins.ceph.com/view/all/job/ceph-orchestrator-rook-e2e/) | [Jenkins Job Definition](https://github.com/ceph/ceph-build/blob/main/ceph-rook-e2e/config/definitions/ceph-orchestrator-rook-e2e.yml)
</details>



Diff:
diff --git a/src/tools/contrib/assign_crimson_cores.py b/src/tools/contrib/assign_crimson_cores.py
new file mode 100644
index 0000000000000..ce4cd3529f959
--- /dev/null
+++ b/src/tools/contrib/assign_crimson_cores.py
@@ -0,0 +1,491 @@
+#!env python3
+import argparse
+import doctest
+import functools
+import itertools
+import logging
+import json
+import os
+import subprocess
+import re
+import sys
+
+root_logger = logging.getLogger(__name__)
+
+
+def get_taskset_output(fname):
+    """
+    Returns contents of fname, or output of taskset -acp
+    if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return f.read()
+    else:
+        return str(
+            subprocess.check_output(
+                ['taskset', '-acp', str(os.getpid())]
+            ), 'utf-8')
+
+
+def taskset_output_to_allowed_cpus(tsout):
+    """
+    Parses taskset output like
+
+    pid 3560663's current affinity list: 0-63\n
+
+    into {0,1,...,63}
+
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-3"))
+    [0, 1, 2, 3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 3"))
+    [3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8"))
+    [0, 1, 2, 3, 4, 5, 8]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8,10-13"))
+    [0, 1, 2, 3, 4, 5, 8, 10, 11, 12, 13]
+    """
+    tsout = re.sub("pid.*list: ", "", tsout)
+    tsout.strip()
+
+    # at this point, tsout should be just the comma delimited ranges
+    ranges = tsout.split(',')
+
+    def parse_range(r):
+        if '-' in r:
+            bounds = r.split('-')
+            if len(bounds) != 2:
+                raise Exception("Invalid taskset string")
+            return set(range(int(bounds[0]), int(bounds[1]) + 1))
+        else:
+            return {int(r)}
+    return functools.reduce(lambda x, y: x | y, map(parse_range, ranges), set())
+
+
+def get_lscpu_json(fname):
+    """
+    Loads the contents of fname as json, or the output
+    of lscpu -e --json if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return json.load(f)
+    else:
+        return json.loads(
+            subprocess.check_output(
+                ['lscpu', '-e', '--json']
+            ))
+
+
+def lscpu_json_to_cpuinfo(lscpu_info, allowed_cpus):
+    """
+    Converts json output of lscpu -e --json to CPUInfo filtering
+    for allowed_cpus
+    """
+    # [CPU(cpu, node, core)]
+    cpus = [
+        CPUInfo.CPU(int(cpu['cpu']), int(cpu['node']), int(cpu['core']))
+        for cpu in lscpu_info['cpus']
+    ]
+
+    if allowed_cpus:
+        cpus = list(filter(
+            lambda x: x.cpu in allowed_cpus,
+            cpus))
+    return CPUInfo(cpus)
+
+
+def to_range_str(s):
+    """
+    Returns comma delimited range representing s
+
+    >>> to_range_str({100})
+    '100'
+    >>> to_range_str({1,3,5,7})
+    '1,3,5,7'
+    >>> to_range_str({1,2,3})
+    '1-3'
+    >>> to_range_str({1,4,5,6,7,10})
+    '1,4-7,10'
+    >>> to_range_str({1,3,5,7,8,9})
+    '1,3,5,7-9'
+    """
+    ranges = []
+    start = None
+    last = None
+    for i in sorted(s):
+        if start is None:
+            assert(last is None)
+            start = i
+            last = i
+            continue
+        if i == last + 1:
+            last = i
+            continue
+        ranges.append((start, last))
+        start = i
+        last = i
+    ranges.append((start, last))
+
+    def to_range(item):
+        start, last = item
+        if start == last:
+            return str(start)
+        else:
+            return f"{start}-{last}"
+    return ",".join(map(to_range, ranges))
+
+
+class CPUInfo:
+    class CPU:
+        def __init__(self, cpu: int, node: int, core : int):
+            # cpu id
+            self.cpu = cpu
+            # numa node id
+            self.node = node
+            # physical core id
+            self.core = core
+
+    def __init__(self, cpus):
+        self.__cpus = cpus
+
+    def get_nodes(self):
+        return sorted({x.node for x in self.__cpus})
+
+    def get_cores_for_node(self, node):
+        """
+        Generates (core_id, [cpu_id]) for each core in the given node
+
+        >>> test_cpuinfo_nodes_interleaved(2,4,2).get_cores_for_node(1)
+        [(1, [1, 9]), (3, [3, 11]), (5, [5, 13]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_separate(2,4,2).get_cores_for_node(1)
+        [(4, [4, 12]), (5, [5, 13]), (6, [6, 14]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_separate(1,4,2).get_cores_for_node(0)
+        [(0, [0, 4]), (1, [1, 5]), (2, [2, 6]), (3, [3, 7])]
+        """
+        ret = {}
+        for cpu in self.__cpus:
+            if cpu.node != node:
+                continue
+            if cpu.core not in ret:
+                ret[cpu.core] = []
+            ret[cpu.core].append(cpu.cpu)
+        return [(core, sorted(cpus)) for core, cpus in sorted(ret.items())]
+
+    def get_cores_by_node(self):
+        """
+        Generates [(node, (core_id, [cpu_id]))] ordered by node, core_id
+
+        >>> test_cpuinfo_nodes_separate(2,2,2).get_cores_by_node()
+        [(0, [(0, [0, 4]), (1, [1, 5])]), (1, [(2, [2, 6]), (3, [3, 7])])]
+        >>> test_cpuinfo_nodes_separate(1,4,1).get_cores_by_node()
+        [(0, [(0, [0]), (1, [1]), (2, [2]), (3, [3])])]
+        """
+        return [(node, self.get_cores_for_node(node)) for node in self.get_nodes()]
+
+    def get_cores_by_node_interleaved(self):
+        """
+        Generates (core_id, [cpu_id]) ordered by core_id, interleaved by
+        node
+
+        >>> list(itertools.islice(test_cpuinfo_nodes_separate(1,4,1).get_cores_by_node_interleaved(), 6))
+        [(0, [0]), (1, [1]), (2, [2]), (3, [3])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_separate(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (8, [8, 24]), (1, [1, 17]), (9, [9, 25])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_interleaved(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (1, [1, 17]), (2, [2, 18]), (3, [3, 19])]
+        """
+        cores_by_node = self.get_cores_by_node()
+        idx = 0
+        while len(cores_by_node) > 0:
+            node, cores = cores_by_node[idx % len(cores_by_node)]
+            assert(len(cores) > 0)
+            yield cores.pop(0)
+            if len(cores) == 0:
+                cores_by_node.pop(idx % len(cores_by_node))
+            else:
+                idx += 1
+
+
+def test_cpuinfo_nodes_interleaved(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that successive cpus are on successive
+    nodes.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id % num_nodes
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == (num_nodes * cores_per_node * cpus_per_core))
+    return CPUInfo(cpus)
+
+
+def test_cpuinfo_nodes_separate(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that cpu ids are grouped by node.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id // cores_per_node
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == num_cpus)
+    return CPUInfo(cpus)
+
+
+class AllocationParams:
+    def __init__(self, num_cpus : int, physical_only : bool):
+        self.num_cpus = num_cpus
+        self.physical_only = physical_only
+
+
+class Allocation:
+    def __init__(self, cpus):
+        self.cpus = cpus
+
+    def __str__(self):
+        return to_range_str(self.cpus)
+
+    def __repr__(self):
+        return f"allocation({self})"
+
+
+def balance_by_process(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are within a single numa node.
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,2,8,10)], [allocation(1,3,9,11)], [allocation(4,6,12,14)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0,2,4,6)], [allocation(1,3,5,7)], [allocation(8,10,12,14)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_separate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_separate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_separate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(4-5,12-13)], [allocation(2-3,10-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_separate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(8-11)], [allocation(4-7)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0,2,32,34), allocation(4,6,8,10)], [allocation(1,3,33,35), allocation(5,7,9,11)], [allocation(12,14,44,46), allocation(16,18,20,22)]]
+    """
+    cores_by_node = cpu_info.get_cores_by_node()
+    ret = [[] for i in range(num_proc)]
+    idx = 0
+    for proc in range(num_proc):
+        node, cores = cores_by_node[idx % len(cores_by_node)]
+        for param in params:
+            cpus = set()
+            while len(cpus) < param.num_cpus:
+                if len(cores) == 0:
+                    raise Exception("Unable to allocate by proc")
+                core, core_cpus = cores.pop(0)
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+        idx += 1
+    return ret
+
+
+def balance_by_socket(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are distributed across all sockets.
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_separate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_separate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_separate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,4,8,12)], [allocation(1,5,9,13)], [allocation(2,6,10,14)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_separate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0-1,32-33), allocation(2-5)], [allocation(6-7,38-39), allocation(8-11)], [allocation(12-13,44-45), allocation(14-17)]]
+    """
+    cpu_stream = cpu_info.get_cores_by_node_interleaved()
+    def get_cpu():
+        try:
+            return cpu_stream.__next__()
+        except StopIteration:
+            raise Exception("Unable to allocate by socket")
+
+    ret = [[] for i in range(num_proc)]
+    for proc in range(num_proc):
+        for param in params:
+            cpus = set()
+            while (len(cpus) < param.num_cpus):
+                core_id, core_cpus = get_cpu()
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+    return ret
+
+
+def get_balance_algorithm(arg : str):
+    balance_algorithms = {
+        'osd': balance_by_process,
+        'socket': balance_by_socket
+    }
+    if arg not in balance_algorithms :
+        algs = ", ". join(balance_algorithm_dict.keys())
+        raise Exception(
+            f"balance_algorithm must be one of {algs}"
+        )
+    return balance_algorithms[arg]
+
+
+def main(argv):
+    examples = """
+    Examples:
+    # Produce logical core mappings for use with crimson-osd
+    %prog [-u <lscpu.json>] [-t <allowed_cpus>] [-b <osd|socket>] [-v]
+          [-o <num_osd>] [-r <num_reactors_per_osd>]
+          [--physical-only-seastar] [--physical-only-alienstore]
+
+    Note that allocations may exceed requested cpus if logical siblings are
+    included and request is not a multiple of logical cpus per core.
+    """
+
+    # skip argument parsing for tests
+    if '-x' in argv:
+        doctest.testmod()
+        return
+
+    logger = root_logger.getChild("main")
+
+    parser = argparse.ArgumentParser(
+        description="Creates cpu mappings appropriate for use with crimson-osd",
+        epilog=examples,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument(
+        "-o",
+        "--num_osd",
+        type=int,
+        required=True,
+        help="Number of OSDs",
+    )
+    parser.add_argument(
+        "-u",
+        "--lscpu",
+        type=str,
+        required=False,
+        help="Input file: .json file produced by lscpu -e --json",
+        default=None,
+    )
+    parser.add_argument(
+        "-t",
+        "--taskset",
+        type=str,
+        required=False,
+        help="Allowed CPUs (comma separated ranges format)",
+        default=None,
+    )
+    parser.add_argument(
+        "-r",
+        "--num-reactors",
+        type=int,
+        required=True,
+        help="Number of Seastar reactors per OSD"
+    )
+    parser.add_argument(
+        "-p",
+        "--physical-only-seastar",
+        action="store_true",
+        help="Only use one logical cpu per physical core for seastar reactors",
+        default=False,
+    )
+    parser.add_argument(
+        "-a",
+        "--num-alienstore-cores",
+        type=int,
+        default=0,
+        help="Number of alienstore cores per OSD"
+    )
+    parser.add_argument(
+        "-k",
+        "--physical-only-alienstore",
+        action="store_true",
+        help="Only use one logical cpu per physical core for alienstore",
+        default=False,
+    )
+    parser.add_argument(
+        "-b",
+        "--balance-algorithm",
+        type=str,
+        required=True,
+        help="CPU balance algorithm: osd, socket (NUMA)",
+    )
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        action="store_true",
+        help="True to enable verbose logging mode",
+        default=False,
+    )
+
+    options = parser.parse_args(argv)
+
+    if options.verbose:
+        logLevel = logging.DEBUG
+    else:
+        logLevel = logging.INFO
+
+    logging.basicConfig(level=logLevel, stream=sys.stderr)
+
+    logger.debug(f"Got options: {options}")
+
+    params = [AllocationParams(
+        options.num_reactors,
+        options.physical_only_seastar)]
+    if options.num_alienstore_cores > 0:
+        params.append(AllocationParams(
+            options.num_alienstore_cores,
+            options.physical_only_alienstore))
+    cpuinfo = lscpu_json_to_cpuinfo(
+        get_lscpu_json(options.lscpu),
+        taskset_output_to_allowed_cpus(get_taskset_output(options.taskset))
+    )
+    osd_mappings = get_balance_algorithm(options.balance_algorithm)(
+        cpuinfo, options.num_osd, params
+    )
+    for idx in range(len(params)):
+        for mappings in osd_mappings:
+            print(mappings[idx])
+
+
+if __name__ == "__main__":
+    main(sys.argv[1:])
diff --git a/src/tools/contrib/balance_cpu.py b/src/tools/contrib/balance_cpu.py
deleted file mode 100644
index 0f7d4b7e3f1ae..0000000000000
--- a/src/tools/contrib/balance_cpu.py
+++ /dev/null
@@ -1,643 +0,0 @@
-#!/usr/bin/python
-"""
-This script gets the output from lscpu and produces a list of CPU uids
-corresponding to physical cores, intended to use to allocate Seastar reactors
-in a balanced way across sockets.
-
-Two strategies of balancing reactors over CPU cores:
-
-1) OSD based: all the reactors of each OSD run in the same CPU NUMA socket (default),
-2) Socket based: reactors for the same OSD are distributed evenly across CPU NUMA sockets.
-
-Some auxiliaries:
-- given a taskset cpu_set bitmask, identify those active physical CPU core ids and their
-  HT siblings,
-- for a gfiven OSD id, identify the corresponding CPU core ids to set.
-- convert a (decimal) comma separated intervals into a cpu_set bitmask
-
-Apply bitwise operator over each bytes variables:
-result=bytes(map (lambda a,b: a ^ b, bytes_all_cpu, bytes_fio_cpu))
-
-Given the list extracted from lscpu, apply the cpu_set bitmask from the taskset argument,
-hence disabling some core ids. For each OSD, produce the corresponding bitmask.
-"""
-
-import argparse
-import logging
-import sys
-import os
-import re
-import tempfile
-from pprint import pformat
-# from typing import Dict, List, Any
-
-from lscpu import LsCpuJson
-
-__author__ = "Jose J Palacios-Perez"
-
-logger = logging.getLogger(__name__)
-
-
-# Some generic bitwise functions to use from the taskset data
-def get_bit(value, bit_index):
-    """Get a power of 2 if the bit is on, 0 otherwise"""
-    return value & (1 << bit_index)
-
-
-def get_normalized_bit(value, bit_index):
-    """Return 1/0 whenever the bit is on"""
-    return (value >> bit_index) & 1
-
-
-def set_bit(value, bit_index):
-    """As it says on the tin"""
-    return value | (1 << bit_index)
-
-
-def clear_bit(value, bit_index):
-    """As it says on the tin"""
-    return value & ~(1 << bit_index)
-
-
-# Generic functions to query whether a CPU id is enabled/available or not
-def is_cpu_avail(bytes_mask, cpuid):
-    """
-    Return true if the cpuid is on
-    CPU id 0 is at the last end of the bytes_mask, the max_cpu is at bit 0
-    """
-    try:
-        return get_normalized_bit(bytes_mask[-1 - (cpuid // 8)], cpuid % 8)
-    except IndexError:
-        return False
-
-
-def set_cpu(bytes_mask, cpuid):
-    """Set cpuid on bytes_mask"""
-    try:
-        bytes_mask[-1 - (cpuid // 8)] = set_bit(bytes_mask[-1 - (cpuid // 8)], cpuid % 8)
-    except IndexError:
-        pass
-    return bytes_mask
-
-
-def get_range(bytes_mask, start, length):
-    """
-    Given a bytes_mask, return a new bytes_mask with the range of CPU ids
-    starting from start and of length, skipping those that are not available
-    """
-    result = bytearray(b"\x00" * len(bytes_mask))
-    max_cpu = 8 * len(bytes_mask)
-    while length > 0 and start < max_cpu:
-        if is_cpu_avail(bytes_mask, start):
-            set_cpu(result, start)
-            length -= 1
-        start += 1
-
-    return result
-
-
-def set_range(bytes_mask, start, end):
-    """
-    Set a range of CPU ids in bytes_mask from start to end
-    """
-    ba = bytearray(bytes_mask)
-    for i in range(start, end):
-        set_cpu(ba, i)
-    return ba
-
-
-def set_all_ht_siblings(bytes_mask):
-    """
-    Set all the HT sibling of the enabled physical CPU ids specified in bytes_mask.
-
-    Physical cores are in the range [-half_length_bytes_mask:]
-    HT siblings are in the range [0:half_length_bytes_mask-1]
-
-    Notes:
-    result=bytes(map (lambda a,b: a | b, bytes_ht, bytes_phys))
-    # result=bytes(map (lambda a,b: a | b, result[0:half_indx], bytes_mask[-half_indx:]))
-    # partial = [a | b for a, b in zip(empty[0:half_indx], bytes_mask[-half_indx:])]
-    # result = bytes( partial + bytes_mask[-half_indx:] )
-    # result = bytearray(b"\x00" * len(bytes_mask))
-    """
-    result = bytearray(bytes_mask)
-    half_indx = len(bytes_mask) // 2
-    for i in range(0, half_indx):
-        result[i] |= bytes_mask[half_indx + i]
-    return result
-
-
-def count_bits(bytes_mask: bytearray) -> int:
-    """
-    Using Python 3.9 way
-    Python 3.10: i.bit_count()
-    """
-    count = 0
-    for x in bytes_mask:
-        count += bin(x).count("1")
-    return count
-
-
-def count_phys_cpus(bytes_mask: bytearray) -> int:
-    """
-    Count the number of physical CPU from the bitmask
-    """
-    count = 0
-    half_indx = len(bytes_mask) // 2
-    count = count_bits(bytes_mask[half_indx:])
-    return count
-
-
-def is_hexadecimal_str(s: str) -> bool:
-    try:
-        int(s, 16)
-        return True
-    except ValueError:
-        return False
-
-
-# Defaults to declare, which are values that can be given as options for the script
-NUM_OSD = 8
-NUM_REACTORS = 3
-
-
-class CpuCoreAllocator(object):
-    """
-    Process a sequence of CPU core ids to be used for the allocation of Seastar reactors
-
-    # lscpu --json
-    {
-    "lscpu": [
-      {
-        d: { "field": "CPU(s):", "data": "112",}
-        d: {'field': 'NUMA node(s):', 'data': '2'}
-        d: {'field': 'NUMA node0 CPU(s):', 'data': '0-27,56-83'}
-        d: {'field': 'NUMA node1 CPU(s):', 'data': '28-55,84-111'}
-      }
-      :
-    }
-    """
-
-    def __init__(
-        self,
-        lscpu: str = "",
-        num_osd: int = NUM_OSD,
-        num_react: int = NUM_REACTORS,
-        hex_or_range_str: str = "",
-        out_hex: bool = False,
-    ) -> None:
-        """
-        This class expects the output from lscpu --json, from there
-        it works out a list of physical CPU uids to allocate Seastar reactors
-        """
-        self.num_osd = num_osd
-        self.num_react = num_react
-        self.out_hex = out_hex
-        self.bytes_avail_cpus = bytes([])
-        self.hex_or_range_str = hex_or_range_str
-        self.lscpu = LsCpuJson(lscpu)
-        assert self.lscpu, f"Invalid {lscpu}"
-        # Output to produce: either hex bitmask CPU set or decimal ranges
-        self.osds_cpu_out = {"dec_ranges": {}, "hex_cpu_mask": {}}
-
-    def set_cpu_default(self) -> None:
-        """
-        From lscpu we set the max num CPUs value to indicate how many hex digits
-        a valid taskset string should have
-        """
-        # Number of hex digits required for the cpu_set bitmask
-        self.num_hex_digits = self.lscpu.get_num_logical_cpus() // 8
-        # Default bitmask: all CPUs available
-        self.ALL_CPUS = "ff" * self.num_hex_digits
-        self.bytes_all_cpu = bytes.fromhex(self.ALL_CPUS)
-
-    def parse_taskset_arg(self, cpu_range: str) -> str:
-        """
-        The taskset arg can be an hexstring describing a cpu_set bitmask, or can be
-        decimal ranges comma separated. This method parses the second case.
-        - split the ',' tuples (or singletons)
-        extract the decimal values -- validate they are within max CPU uid. Produce a
-        valid hexstring cpu_set bitmask.
-        # result_ba = bytearray(b"\x00" * len(self.bytes_all_cpu))
-        # cpu_list_int = list(range(start, end + 1))
-        # cpu_list_int = [start]
-        # cpu_set.update(set(cpu_list_int))
-        # Convert the set to an hex string for a cpu_set bitmask
-        # for item in cpu_set:
-        #    set_cpu(result_ba, item)
-        # Compare both approaches match:
-        # logging.debug(f"result_ba:{result_ba}, bytes_mask:{bytes_mask}")
-        """
-        cpu_list_str = cpu_range.split(",")
-        regex = re.compile(r"(\d+)([-](\d+))?")
-        bytes_mask = bytearray(b"\x00" * len(self.bytes_all_cpu))
-        for item in cpu_list_str:
-            m = regex.search(item)
-            if m:
-                start = int(m.group(1))
-                if m.group(2):
-                    end = int(m.group(3))
-                    bytes_mask = set_range(bytes_mask, start, end)
-                else:
-                    bytes_mask = set_cpu(bytes_mask, start)
-        return bytes(bytes_mask).hex()
-
-    def set_available_cpus(self):
-        """
-        Set the instance attribute self.bytes_avail_cpus
-        If valid taskset hex_cpu_mask, use it, otherwise use all CPUs
-        """
-        # Validate the hex_string/bytes size for the cpuset bitmask
-        if self.hex_cpu_mask:
-            try:
-                self.bytes_avail_cpus = bytes.fromhex(self.hex_cpu_mask)
-            except ValueError:
-                print(f"Ignoring invalid hex string, using default {self.ALL_CPUS}")
-                logger.error(f"Invalid taskset arg: {self.hex_cpu_mask} ")
-                self.bytes_avail_cpus = self.bytes_all_cpu
-            assert self.num_hex_digits >= len(
-                self.bytes_avail_cpus
-            ), "Invalid taskset hexstring size"
-
-    def validate_cpu_for_osd(self):
-        """
-        Validate whether there are enough CPU cores for the required OSD
-        Count the number of bits from the physical section of the bytes_avail_cpus
-
-        Note: we could use up to the maximum possible num of OSD instead of the number asked.
-        """
-        total_phys_cores = self.lscpu.get_total_physical()
-        self.num_avail_phys_cores = count_phys_cpus(bytearray(self.bytes_avail_cpus))
-
-        logger.debug(
-            f"total_phys_cores: {total_phys_cores}, avail_phys_cores: {self.num_avail_phys_cores}"
-        )
-        assert (
-            total_phys_cores >= self.num_avail_phys_cores
-        ), "Invalid available physical CPU cores"
-        max_osd_num = self.num_avail_phys_cores // self.num_react
-        assert max_osd_num > self.num_osd, "Not enough physical CPU cores"
-
-
-    def setup(self):
-        """
-        Preparation and validation of available CPU ids
-        """
-        self.lscpu.load_json()
-        self.lscpu.get_ranges()
-        self.set_cpu_default()
-        if is_hexadecimal_str(self.hex_or_range_str):
-            self.hex_cpu_mask = self.hex_or_range_str
-        else:
-            self.hex_cpu_mask = self.parse_taskset_arg(self.hex_or_range_str)
-        self.set_available_cpus()
-        logger.debug(f"self.bytes_avail_cpus: {self.bytes_avail_cpus}")
-        self.validate_cpu_for_osd()
-
-
-    def bitmask_to_range(self, bytes_mask) -> str:
-        """
-        Produce a list of decimal ranges from the bitmask cpuset
-        """
-        lista = []
-        start = -1
-        end = start
-        i = 0
-        # Do we need to check max_cpu < self.lscpu.get_num_logical_cpus():
-        max_cpu = len(bytes_mask) * 8
-        logger.debug(f"max_cpu : {max_cpu}")
-        while i<max_cpu:
-            flag = is_cpu_avail(bytes_mask, i)
-            if flag:
-                if start == -1:
-                    start = i
-                logger.debug(f"i: {i}, lista:{pformat(lista)}")
-                while is_cpu_avail(bytes_mask,i) and i<max_cpu:
-                    end = i
-                    i += 1
-                if start == end:
-                    lista.append(f"{start}")
-                else:
-                    lista.append(f"{start}-{end}")
-                start = -1
-            else:
-                logger.debug(f"not i: {i}, lista:{pformat(lista)}")
-                while not is_cpu_avail(bytes_mask, i) and i<max_cpu:
-                    i += 1
-        return ",".join(lista)
-
-
-    def set_osd_cpuset(self, osd, cpuset_ba:bytes) -> None:
-        """
-        Updates the internal attributes to trace the CPUs assigned to the OSD process
-        """
-        osd_cpu_s = bytes(cpuset_ba).hex()
-        # Update the bitset mask:
-        if osd in self.osds_cpu_out["hex_cpu_mask"]:
-            self.osds_cpu_out["hex_cpu_mask"][osd] += f",{osd_cpu_s}"
-        else:
-            self.osds_cpu_out["hex_cpu_mask"].update({osd: osd_cpu_s})
-
-        osd_cpu_str = self.bitmask_to_range(cpuset_ba)
-        self.osds_cpu_out["dec_ranges"].update({osd: osd_cpu_str})
-        logger.debug(f"self.osds_cpu_out: {pformat(self.osds_cpu_out)}")
-
-
-    def do_distrib_socket_based(self):
-        """
-        Distribution criteria: the reactors of each OSD are distributed across the available
-        NUMA sockets evenly.
-        Each OSD uses step cores from each NUMA socket. Each socket is a pair of ranges (_start,_end)
-        for physical and HT. On each allocation we update the physical_start, so the next iteration
-        picks the CPU uid accordingly.
-        Produces a bitmask cpuset and list of cpu id ranges to use for the ceph config set CLI.
-
-        This method and next definitely can be refactored, possibly by defining a dictionary with callbacks
-        for each stage where differ, both use the same general algorithm.
-        """
-        # Init: common to both strategies
-        control = []
-        num_sockets = self.lscpu.get_num_sockets()
-
-        # Each OSD uses num reactor//sockets cores
-        step = self.num_react // num_sockets
-        reminder = self.num_react % num_sockets
-
-        logger.debug(f"do_distrib_socket_based: step:{step}")
-
-        # Copy the original physical ranges to the control dict
-        for socket in self.lscpu.get_sockets():
-            control.append(socket)
-
-        # This byte array will be transformed for each OSD
-        cpu_avail_ba = bytearray(self.bytes_avail_cpus)
-        avail_s = bytes(cpu_avail_ba).hex()
-        # This dict would hold a bitsetmask in hex per OSD
-        osds_ba = {}
-        # Traverse the OSD to produce an allocation
-        for osd in range(self.num_osd):
-            for socket in control:
-                _start = socket["physical_start"]
-                _step = step
-                # If there is a reminder, use a round-robin technique so all
-                # sockets are candidate for it
-                _candidate = osd % num_sockets
-                _so_id = socket["socket"]
-                if _candidate == _so_id:
-                    _step += reminder
-                _end = socket["physical_start"] + _step
-                # For cephadm, construct a dictionary for these intervals
-                logger.debug(
-                    f"osd: {osd}, socket:{_so_id}, _start:{_start}, _end:{_end - 1}"
-                )
-                # Verify this range is valid, otherwise shift as appropriate
-                cpuset_ba = get_range(cpu_avail_ba, _start, _step)
-                # Associate their HT siblings of this range
-                cpuset_ba = set_all_ht_siblings(cpuset_ba)
-                # Update the list of bitmask of this OSD
-                if osd in osds_ba:
-                    merged = bytes(map(lambda a, b: a | b, osds_ba[osd], cpuset_ba))
-                    osds_ba[osd] = merged
-                else:
-                    osds_ba.update({osd: cpuset_ba})
-                # Disable this OSD bitmaskset from the cpu_avail_ba
-                cpu_avail_ba = bytes(map(lambda a, b: a & ~b, cpu_avail_ba, cpuset_ba))
-                osd_cpu_s = bytes(cpuset_ba).hex()
-                osd_cpu_s = bytes(osds_ba[osd]).hex()
-                avail_s = bytes(cpu_avail_ba).hex()
-                logger.debug(f"-- OSD: {osd}: {osd_cpu_s}, avail:{avail_s}")
-                # Update the bitset mask
-                self.set_osd_cpuset(osd, osds_ba[osd])
-
-                if _end <= socket["physical_end"]:
-                    socket["physical_start"] = _end
-                    _ht_start = socket["ht_sibling_start"]
-                    _ht_end = socket["ht_sibling_start"] + step
-                    plist = list(
-                        range(
-                            _ht_start,
-                            _ht_end,
-                            1,
-                        )
-                    )
-                    logger.debug(f"plist: {plist}")
-                    socket["ht_sibling_start"] += _step
-                else:
-                    # bail out
-                    _sops = socket["physical_start"] + step
-                    logger.debug(f"out of range: {_sops}")
-                    break
-        # Set the reminder available CPU
-        self.set_osd_cpuset('available', bytes(cpu_avail_ba) )
-        self.osds_ba = osds_ba
-
-    def do_distrib_osd_based(self):
-        """
-        Given a number of Seastar reactor threads and number of OSD,
-        distributes all the reactors of the same OSD in the same NUMA socket
-        using only physical core CPUs.
-        Produces a list of ranges to use for the ceph config set CLI.
-        """
-        control = []
-        # Each OSD uses num reactor cores from the same NUMA socket
-        num_sockets = self.lscpu.get_num_sockets()
-        step = self.num_react
-
-        # Copy the original physical ranges to the control dict
-        for socket in self.lscpu.get_sockets():
-            control.append(socket)
-
-        # This byte array will be transformed for each OSD
-        cpu_avail_ba = bytearray(self.bytes_avail_cpus)
-        avail_s = bytes(cpu_avail_ba).hex()
-        logger.debug(f"cpu_avail_ba : {avail_s}")
-        # This dict would hold a bitsetmask per OSD
-        osds_ba = {}
-        # Traverse the OSD to produce an allocation
-        # even OSD num uses socket0, odd OSD number uses socket 1
-        for osd in range(self.num_osd):
-            #osds = []  # List of ranges as string
-            _so_id = osd % num_sockets
-            socket = control[_so_id]
-            _start = socket["physical_start"]
-            _end = socket["physical_start"] + step
-            # For cephadm, construct a dictionary for these intervals
-            logger.debug(
-                f"osd: {osd}, socket:{_so_id}, _start:{_start}, _end:{_end - 1}"
-            )
-            # Verify this range is valid, skipping unavailable CPU ids as appropriate
-            cpuset_ba = get_range(cpu_avail_ba, _start, step)
-            # Associate their HT siblings of this range -- what if some of these are disabled?
-            cpuset_ba = set_all_ht_siblings(cpuset_ba)
-            # Update the list of bitmask of this OSD
-            if osd in osds_ba:
-                merged = bytes(map(lambda a, b: a | b, osds_ba[osd], cpuset_ba))
-                osds_ba[osd] = merged
-            else:
-                osds_ba.update({osd: cpuset_ba})
-            #osds.append(f"{_start}-{_end - 1}")
-            # Disable this OSD bitmaskset from the cpu_avail_ba
-            cpu_avail_ba = bytes(map(lambda a, b: a & ~b, cpu_avail_ba, cpuset_ba))
-            osd_cpu_s = bytes(cpuset_ba).hex()
-            avail_s = bytes(cpu_avail_ba).hex()
-            logger.debug(f"-- OSD: {osd}: {osd_cpu_s}, avail:{avail_s}")
-            # Update the bitset mask
-            self.set_osd_cpuset(osd, osds_ba[osd])
-
-            if _end <= socket["physical_end"]:
-                socket["physical_start"] = _end
-                _ht_start = socket["ht_sibling_start"]
-                _ht_end = socket["ht_sibling_start"] + step
-                plist = list(
-                    range(
-                        _ht_start,
-                        _ht_end,
-                        1,
-                    )
-                )
-                logger.debug(f"plist: {plist}")
-                socket["ht_sibling_start"] += step
-            else:
-                # bail out
-                _sops = socket["physical_start"] + step
-                logger.debug(f"Out of range: {_sops}")
-                break
-        # Set the reminder available CPU
-        self.set_osd_cpuset('available', bytes(cpu_avail_ba) )
-        # Set the following to exercise the unit tests
-        self.osds_ba = osds_ba
-
-    def output_cpusets(self):
-        """
-        Generic print of the cpuset to use per OSD and the remaining list of CPU available
-        to use for everything else, eg Alien threads
-
-        # Convert a bytesarrys back to hex string
-        # hex_string = "".join("%02x" % b for b in array_alpha)
-        # print(bytes(bytes_array).hex())
-        """
-        # Output: either hex or decimal ranges
-        if self.out_hex:
-            for cpuset in self.osds_cpu_out["hex_cpu_mask"].values():
-                # one line per OSD-- ensure its sorted, last one must be the available
-                print(cpuset)
-        else:
-            for cpuset in self.osds_cpu_out["dec_ranges"].values():
-                print(cpuset)
-            #print(" ".join(map(str, self._to_disable)))
-
-        logger.debug(f"osds_cpu_out: {pformat(self.osds_cpu_out)}")
-
-
-    def run(self, distribute_strat):
-        """
-        Load the .json from lscpu, get the ranges of CPU cores per socket,
-        produce the corresponding balance, print the balance as a list intended to be
-        consumed by vstart.sh -- a dictionary will be used for cephadm.
-        """
-        self.setup()
-        if distribute_strat == "socket":
-            self.do_distrib_socket_based()
-        else:
-            self.do_distrib_osd_based()
-
-        self.output_cpusets()
-
-
-def main(argv):
-    examples = """
-    Examples:
-    # Produce a balanced CPU distribution of physical CPU cores intended for the Seastar
-        reactor threads
-        %prog [-u <lscpu.json>|-t <taskset_mask>] [-b <osd|socket>] [-d<dir>] [-v]
-              [-o <num_OSDs>] [-r <num_reactors>]
-
-    # such a list can be used for vstart.sh/cephadm to issue ceph conf set commands.
-    """
-    parser = argparse.ArgumentParser(
-        description="""This tool is used to produce CPU core balanced allocation""",
-        epilog=examples,
-        formatter_class=argparse.RawDescriptionHelpFormatter,
-    )
-    parser.add_argument(
-        "-o",
-        "--num_osd",
-        type=int,
-        required=False,
-        help="Number of OSDs",
-        default=NUM_OSD,
-    )
-    parser.add_argument(
-        "-u",
-        "--lscpu",
-        type=str,
-        help="Input file: .json file produced by lscpu --json",
-        default=None,
-    )
-    parser.add_argument(
-        "-t",
-        "--taskset",
-        type=str,
-        help="The taskset argument of the parent process (eg. vstart)",
-        default=None,
-    )
-    parser.add_argument(
-        "-r",
-        "--num_reactor",  # value of --crimson-smp
-        type=int,
-        required=False,
-        help="Number of Seastar reactors",
-        default=NUM_REACTORS,
-    )
-    parser.add_argument(
-        "-d", "--directory", type=str, help="Directory to examine", default="./"
-    )
-    parser.add_argument(
-        "-b",
-        "--balance",
-        type=str,
-        required=False,
-        help="CPU balance strategy: osd (default), socket (NUMA)",
-        default=False,
-    )
-    parser.add_argument(
-        "-v",
-        "--verbose",
-        action="store_true",
-        help="True to enable verbose logging mode",
-        default=False,
-    )
-    parser.add_argument(
-        "-x",
-        "--hexcpuset",
-        action="store_true",
-        help="True to enable hexadecimal cpuset bitmask output",
-        default=False,
-    )
-
-    # parser.set_defaults(numosd=1)
-    options = parser.parse_args(argv)
-
-    if options.verbose:
-        logLevel = logging.DEBUG
-    else:
-        logLevel = logging.INFO
-
-    with tempfile.NamedTemporaryFile(dir="/tmp", delete=False) as tmpfile:
-        logging.basicConfig(filename=tmpfile.name, encoding="utf-8", level=logLevel)
-
-    logger.debug(f"Got options: {options}")
-    os.chdir(options.directory)
-
-    cpu_cores = CpuCoreAllocator(
-        options.lscpu,
-        options.num_osd,
-        options.num_reactor,
-        options.taskset,
-        options.hexcpuset,
-    )
-    cpu_cores.run(options.balance)
-
-
-if __name__ == "__main__":
-    main(sys.argv[1:])
diff --git a/src/tools/contrib/lscpu.py b/src/tools/contrib/lscpu.py
deleted file mode 100644
index 32dd2d96d0160..0000000000000
--- a/src/tools/contrib/lscpu.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/python
-"""
-This module gets the output from lscpu and produces a list of CPU uids
-corresponding to physical cores.
-"""
-
-# import logging
-import os
-import re
-import json
-# import tempfile
-# import pprint
-
-__author__ = "Jose J Palacios-Perez"
-
-# logger = logging.getLogger(__name__)
-
-
-class LsCpuJson(object):
-    """
-    Process a sequence of CPU core ids
-
-    # lscpu --json
-    {
-    "lscpu": [
-      {
-        d: { "field": "CPU(s):", "data": "112"}
-        d: {"field": "Core(s) per socket:", "data": "28"}
-        d: {'field': 'NUMA node(s):', 'data': '2'}
-        d: {'field': 'NUMA node0 CPU(s):', 'data': '0-27,56-83'}
-        d: {'field': 'NUMA node1 CPU(s):', 'data': '28-55,84-111'}
-      }
-      :
-    }
-    """
-
-    def __init__(self, json_file: str):
-        """
-        This class expects the output from lscpu --json
-        """
-        self.json_file = json_file
-        self._dict = {}
-        self.socket_lst = {
-            "num_sockets": 0,
-            "num_logical_cpus": 0,
-            "num_cores_per_socket": 0,
-            # or more general, an array, index is the socket number
-            "sockets": [],
-        }
-
-    def load_json(self):
-        """
-        Load the lscpu --json output
-        """
-        json_file = self.json_file
-        with open(json_file, "r") as json_data:
-            # check for empty file
-            f_info = os.fstat(json_data.fileno())
-            if f_info.st_size == 0:
-                print(f"JSON input file {json_file} is empty")
-                return  # Should assert
-            self._dict = json.load(json_data)
-            json_data.close()
-        # logger.debug(f"_dict: {self._dict}")
-
-    def get_num_sockets(self):
-        """
-        Accessor
-        """
-        return self.socket_lst["num_sockets"]
-
-    def get_physical_start(self, sindex):
-        """
-        Accessor: cpu core id start physical
-        """
-        return self.socket_lst["sockets"][sindex]["physical_start"]
-
-    def get_ht_start(self, sindex):
-        """
-        Accessor: cpu core id start ht-sibling
-        """
-        return self.socket_lst["sockets"][sindex]["ht_sibling_start"]
-
-    def get_num_physical(self):
-        """
-        Accessor: num physical cpu core ids
-        """
-        return self.socket_lst["num_cores_per_socket"]
-
-    def get_num_logical_cpus(self):
-        """
-        Accessor: num CPU ids
-        """
-        return self.socket_lst["num_logical_cpus"]
-
-    def get_total_physical(self):
-        """
-        Accessor: sum of the physical cores for all sockets
-        """
-        return self.get_num_sockets() * self.get_num_physical()
-        
-
-    def get_socket(self, cpuid: int):
-        """
-        Accessor: given cpuid returns which socket number and
-        whether is a physical (True) or ht-sibling (False)
-        """
-        for _i, s in enumerate(self.socket_lst["sockets"]):
-            if s["physical_start"] <= cpuid and cpuid <= s["physical_end"]:
-                return (_i, True)
-            if s["ht_sibling_start"] <= cpuid and cpuid <= s["ht_sibling_end"]:
-                return (_i, False)
-
-    def get_ranges(self):
-        """
-        Parse the .json from lscpu
-        (we might extend this to parse either version: normal or .json)
-        """
-        ncpu_re = re.compile(r"^CPU\(s\):$")
-        numa_re = re.compile(r"NUMA node\(s\):")
-        node_re = re.compile(r"NUMA node(\d+) CPU\(s\):")
-        ranges_re = re.compile(r"(\d+)-(\d+),(\d+)-(\d+)")
-        cores_re = re.compile(r"^Core\(s\) per socket:$")
-        socket_lst = self.socket_lst
-        for d in self._dict["lscpu"]:
-            # logger.debug(f"d: {d}")
-            m = numa_re.search(d["field"])
-            if m:
-                socket_lst["num_sockets"] = int(d["data"])
-                continue
-            m = node_re.search(d["field"])
-            if m:
-                socket = m.group(1)
-                m = ranges_re.search(d["data"])
-                if m:
-                    drange = {
-                        "socket": int(socket),
-                        "physical_start": int(m.group(1)),
-                        "physical_end": int(m.group(2)),
-                        "ht_sibling_start": int(m.group(3)),
-                        "ht_sibling_end": int(m.group(4)),
-                    }
-                    socket_lst["sockets"].append(drange)
-                    continue
-            m = ncpu_re.search(d["field"])
-            if m:
-                socket_lst["num_logical_cpus"] = int(d["data"])
-                continue
-            m = cores_re.search(d["field"]) 
-            if m:
-                socket_lst["num_cores_per_socket"] = int(d["data"])
-        # logger.debug(f"result: {socket_lst}")
-        assert self.socket_lst["num_sockets"] > 0, "Failed to parse lscpu"
-
-    def get_sockets(self):
-        """
-        Return the socket_lst["sockets"]
-        """
-        return self.socket_lst["sockets"]
diff --git a/src/vstart.sh b/src/vstart.sh
index adeeab9bb9a7a..ea3577bf4af86 100755
--- a/src/vstart.sh
+++ b/src/vstart.sh
@@ -275,7 +275,9 @@ options:
 	--seastore-secondary-devs-type: device type of all secondary blockdevs. HDD, SSD(default), ZNS or RANDOM_BLOCK_SSD
 	--crimson-smp: number of cores to use for crimson
 	--crimson-alien-num-threads: number of alien-tp threads
-	--crimson-alien-num-cores: number of cores to use for alien-tp
+	--crimson-reactor-physical-only: use only one cpu per physical core for seastar reactors
+	--crimson-alien-num-cores: number of cpus to use for alien threads
+	--crimson-alienstore-physical-only: use only one cpu per physical core for alienstore
 	--crimson-balance-cpu: distribute the Seastar reactors uniformly across OSDs (osd) or NUMA (socket)
 	--osds-per-host: populate crush_location as each host holds the specified number of osds if set
 	--require-osd-and-client-version: if supplied, do set-require-min-compat-client and require-osd-release to specified value
@@ -350,40 +352,38 @@ parse_secondary_devs() {
 
 # Auxiliar function to prepare the CPU cores to pin Seastar reactors
 prep_balance_cpu() {
-    local crimson_smp=$1
-    local balance_strategy=$2
-    local in_file_name="/tmp/numa_args_${balance_strategy}.out"
-    local out_file_name="/tmp/numa_nodes.json"
-    local log_file_name="/tmp/numa_bal_${balance_strategy}.log"
-    local cmd
-
-    # Check the table is empty
-    if [ "${#cpu_table[@]}" -eq 0 ]; then
-        # Ensure the file with the CPU mappings exist
-        if [ ! -f ${in_file_name} ]; then
-            debug echo "lscpu --json > ${out_file_name}"
-            lscpu --json > ${out_file_name}
-            MY_CPUS=$(taskset -acp $$ | awk -F : '{print $2}')
-            cmd="python3 ${CEPH_DIR}/../src/tools/contrib/balance_cpu.py -o ${CEPH_NUM_OSD}\
-                -r ${crimson_smp}  -b ${balance_strategy} -u ${out_file_name} -t ${MY_CPUS} > ${in_file_name}"
-            debug echo "$cmd"
-            eval "$cmd" >> ${log_file_name}
-        fi
+    if [ -z $crimson_balance_cpu ] || [ "${crimson_balance_cpu}" == "none" ] ; then
+        echo "Not assigning cpus for crimson"
+        return
+    fi
 
-        readarray -t cpu_table < ${in_file_name}
-        # Check the table is not empty, bail out otherwise
-        if [ "${#cpu_table[@]}" -ne 0 ]; then
-            debug echo "CPU table not empty with ${#cpu_table[@]} entries"
-        else
-            debug echo "CPU table empty, bailing out. Check  ${log_file_name}"
-        fi
+    cmd="python3 ${CEPH_DIR}/../src/tools/contrib/assign_crimson_cores.py"
+    cmd+=" -o ${CEPH_NUM_OSD} -r ${crimson_smp} -a ${crimson_alien_num_cores}"
+    cmd+=" -b ${crimson_balance_cpu}"
+    if [ ${crimson_reactor_physical_only} != 0 ]; then
+        cmd+=" --physical-only-seastar"
+    fi
+    if [ ${crimson_alienstore_physical_only} != 0 ]; then
+        cmd+=" --physical-only-alienstore"
+    fi
+
+    echo $cmd
+    readarray -t cpu_table < <($cmd)
+    # Check the table is not empty, bail out otherwise
+    if [ "${#cpu_table[@]}" -ne 0 ]; then
+        debug echo "CPU table not empty with ${#cpu_table[@]} entries"
+    else
+        debug echo "CPU table empty, bailing out."
+        exit 1
     fi
 }
 
 # Default values for the crimson options
 crimson_smp=1
 crimson_alien_num_threads=0
+crimson_reactor_physical_only=0
 crimson_alien_num_cores=0
+crimson_alienstore_physical_only=0
 crimson_balance_cpu="" # "osd", "socket"
 
 while [ $# -ge 1 ]; do
@@ -620,10 +620,16 @@ case $1 in
         crimson_alien_num_threads=$2
         shift
         ;;
+    --crimson-reactor-physical-only)
+        crimson_reactor_physical_only=1
+        ;;
     --crimson-alien-num-cores)
         crimson_alien_num_cores=$2
         shift
         ;;
+    --crimson-alienstore-physical-only)
+        crimson_alienstore_physical_only=1
+        ;;
     --crimson-balance-cpu)
         crimson_balance_cpu=$2
         shift
@@ -1197,10 +1203,32 @@ start_cephexporter() {
 
 do_balance_cpu() {
     local osd=$1
+    local alienstore_idx=$(( osd + CEPH_NUM_OSD ))
+
+    local reactor_interval=${cpu_table[${osd}]}
+    if ! [ "${reactor_interval}" == "" ]; then
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_cpu_cores ${reactor_interval}"
+        echo $cmd
+        $cmd
+    else
+        echo "No cpu_table entry for osd $osd, setting crimson_seastar_num_reactors"
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_num_threads $crimson_smp"
+        echo $cmd
+        $cmd
+        return
+    fi
+
+
+    local alienstore_interval=${cpu_table[${alienstore_idx}]}
+    if [ ! "${alienstore_interval}" == "" ]; then
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_alien_thread_cpu_cores ${alienstore_interval}"
+        echo $cmd
+        $cmd
+    else
+        echo "No alienstore cpu_table entry for osd $osd"
+        return
+    fi
 
-    interval=${cpu_table[${osd}]}
-    echo "$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_cpu_cores $interval"
-    $CEPH_BIN/ceph -c $conf_fn config set "osd.$osd" crimson_seastar_cpu_cores "$interval"
 }
 
 start_osd() {
@@ -1215,22 +1243,14 @@ start_osd() {
     fi
     local osds_wait
     # If the type of OSD is Crimson and the option to balance the Seastar reactors is true
-	if [ "$ceph_osd" == "crimson-osd" ] && [ ! -z "$crimson_balance_cpu" ]; then
+	if [ "$ceph_osd" == "crimson-osd" ]; then
         debug echo "Preparing balance CPU for Crimson"
-        prep_balance_cpu $crimson_smp $crimson_balance_cpu
+        prep_balance_cpu
     fi
     for osd in `seq $start $end`
     do
 	if [ "$ceph_osd" == "crimson-osd" ]; then
-        if [ ! -z "$crimson_balance_cpu" ]; then
-            do_balance_cpu $osd
-        else
-            bottom_cpu=$(( osd * crimson_smp ))
-            top_cpu=$(( bottom_cpu + crimson_smp - 1 ))
-            # set exclusive CPU nodes for each osd
-            echo "$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_cpu_cores $bottom_cpu-$top_cpu"
-            $CEPH_BIN/ceph -c $conf_fn config set "osd.$osd" crimson_seastar_cpu_cores "$bottom_cpu-$top_cpu"
-        fi
+        do_balance_cpu $osd
     fi
 	if [ "$new" -eq 1 -o $inc_osd_num -gt 0 ]; then
             wconf <<EOF
@@ -1759,38 +1779,9 @@ if [ "$ceph_osd" == "crimson-osd" ]; then
         extra_seastar_args=" --trace"
     fi
     if [ "$objectstore" == "bluestore" ]; then
-        # This condition verifies the number of logical CPU cores
-        if [ "$(expr $(nproc) - 1)" -gt "$(($CEPH_NUM_OSD * crimson_smp))" ]; then
-            if [ ! -z "$crimson_balance_cpu" ]; then
-                debug echo "Preparing balance CPU for Crimson"
-                prep_balance_cpu $crimson_smp $crimson_balance_cpu
-                available_cpus="${cpu_table[-1]}"
-                echo "crimson_alien_thread_cpu_cores: '$available_cpus'"
-                $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_thread_cpu_cores "$available_cpus"
-            else
-                if [ $crimson_alien_num_cores -gt 0 ]; then
-                    alien_bottom_cpu=$(($CEPH_NUM_OSD * crimson_smp))
-                    alien_top_cpu=$(( alien_bottom_cpu + crimson_alien_num_cores - 1 ))
-                    # Ensure top value within range:
-                    if [ "$(($alien_top_cpu))" -gt "$(expr $(nproc) - 1)" ]; then
-                        alien_top_cpu=$(expr $(nproc) - 1)
-                    fi
-                    echo "crimson_alien_thread_cpu_cores: $alien_bottom_cpu-$alien_top_cpu"
-                    # This is a (logical) processor id range, it could be refined to encompass only physical processor ids
-                    # (equivalently, ignore hyperthreading sibling processor ids)
-                    $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_thread_cpu_cores "$alien_bottom_cpu-$alien_top_cpu"
-                else
-                    # This is the legacy default case
-                    echo "crimson_alien_thread_cpu_cores:" $(($CEPH_NUM_OSD * crimson_smp))-"$(expr $(nproc) - 1)"
-                    $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_thread_cpu_cores $(($CEPH_NUM_OSD * crimson_smp))-"$(expr $(nproc) - 1)"
-                fi
-            fi
-            if [ $crimson_alien_num_threads -gt 0 ]; then
-                echo "$CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads $crimson_alien_num_threads"
-                $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads "$crimson_alien_num_threads"
-            fi
-        else
-          echo "No alien thread cpu core isolation"
+        if [ $crimson_alien_num_threads -gt 0 ]; then
+            echo "$CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads $crimson_alien_num_threads"
+            $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads "$crimson_alien_num_threads"
         fi
     fi
 fi


Reviews:
user: login: perezjosibm
body: Just a typo, and a couple of minor suggestions please, thanks!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:25:49Z
user: login: athanatos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:54:18Z
user: login: athanatos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:57:12Z
user: login: athanatos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:02:31Z
user: login: athanatos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:17:03Z
user: login: athanatos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:20:34Z
user: login: perezjosibm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T11:57:41Z
user: login: perezjosibm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T11:59:11Z
user: login: perezjosibm
body: LGTM, thanks!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T12:00:23Z
user: login: Matan-B
body: This seem to address https://tracker.ceph.com/issues/66159, I've updated it so we can backport this easily - can you please add a "fixes"? 

edit: discussed offline, the script is useful today for testing/verifying new allocation strategies - and is not yet proven (on a specific machine) to have any meaningful impact. Yet, we want to be able to easily mesaure and test this is this might change.

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T09:32:19Z
user: login: Matan-B
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:33:01Z
user: login: Matan-B
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:33:36Z
user: login: Matan-B
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:34:47Z
user: login: Matan-B
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:35:30Z
user: login: Matan-B
body: 2 comments from previous round might be worth addressing. Other than that lgtm!

It seems that doc/dev/crimson/BalanceCPUCrimson.md might need an update after this PR. We can address this out of scope of this PR.

note: we should verify that seastar's resource.cc doesn't provide similar strategies since it might.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:39:58Z
user: login: athanatos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:06:13Z
user: login: athanatos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:08:02Z

Review comments:
diff_hunk: @@ -350,40 +351,37 @@ parse_secondary_devs() {
 
 # Auxiliar function to prepare the CPU cores to pin Seastar reactors
 prep_balance_cpu() {
-    local crimson_smp=$1
-    local balance_strategy=$2
-    local in_file_name="/tmp/numa_args_${balance_strategy}.out"
-    local out_file_name="/tmp/numa_nodes.json"
-    local log_file_name="/tmp/numa_bal_${balance_strategy}.log"
-    local cmd
-
-    # Check the table is empty
-    if [ "${#cpu_table[@]}" -eq 0 ]; then
-        # Ensure the file with the CPU mappings exist
-        if [ ! -f ${in_file_name} ]; then
-            debug echo "lscpu --json > ${out_file_name}"
-            lscpu --json > ${out_file_name}
-            MY_CPUS=$(taskset -acp $$ | awk -F : '{print $2}')
path: src/vstart.sh
user: login: perezjosibm
body: We used this code to get the affinity of the running vstast.sh process, and use that as the allowed set of CPU ids to use. Main reason is that didn't have a separate machine to run the IO exerciser (eg. FIO), so it had to be run  in the same machine as the OSDs. This part of the code works a bit like "reserving" a set of valid CPU ids to use by FIO at the time of the cluster creation, for example, the following is a snippet for a [script driving a test plan](https://github.com/perezjosibm/ceph-aprg/blob/main/bin/run_balanced_osd.sh) (iterating over number of OSDs, reactor, etc)

```
VSTART_CPU_CORES="0-27"
FIO_CPU_CORES="28-55,84-111" # inc HT
:
cmd="MDS=0 MON=1 OSD=${NUM_OSD} MGR=1 taskset -ac '${VSTART_CPU_CORES}' /ceph/src/vstart.sh\
 --new -x --localhost --without-dashboard --redirect-output ${crimson_be_table[${OSD_TYPE}]}\
 --crimson ${bal_ops_table[${BAL_KEY}]}"
:

```
that is why we passed such affinity as an argument to the Python script. I guess is a trade off between the flexibility to have two sets of CPU ids to use by the OSDs, which needs to be mutually exclusive to the set of CPU ids used by FIO.
created_at: 2025-05-06T17:25:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1760,37 +1782,9 @@ if [ "$ceph_osd" == "crimson-osd" ]; then
     fi
     if [ "$objectstore" == "bluestore" ]; then
         # This condition verifies the number of logical CPU cores
-        if [ "$(expr $(nproc) - 1)" -gt "$(($CEPH_NUM_OSD * crimson_smp))" ]; then
-            if [ ! -z "$crimson_balance_cpu" ]; then
-                debug echo "Preparing balance CPU for Crimson"
-                prep_balance_cpu $crimson_smp $crimson_balance_cpu
-                available_cpus="${cpu_table[-1]}"
-                echo "crimson_alien_thread_cpu_cores: '$available_cpus'"
-                $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_thread_cpu_cores "$available_cpus"
-            else
-                if [ $crimson_alien_num_cores -gt 0 ]; then
-                    alien_bottom_cpu=$(($CEPH_NUM_OSD * crimson_smp))
-                    alien_top_cpu=$(( alien_bottom_cpu + crimson_alien_num_cores - 1 ))
-                    # Ensure top value within range:
-                    if [ "$(($alien_top_cpu))" -gt "$(expr $(nproc) - 1)" ]; then
-                        alien_top_cpu=$(expr $(nproc) - 1)
-                    fi
-                    echo "crimson_alien_thread_cpu_cores: $alien_bottom_cpu-$alien_top_cpu"
-                    # This is a (logical) processor id range, it could be refined to encompass only physical processor ids
-                    # (equivalently, ignore hyperthreading sibling processor ids)
-                    $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_thread_cpu_cores "$alien_bottom_cpu-$alien_top_cpu"
-                else
-                    # This is the legacy default case
-                    echo "crimson_alien_thread_cpu_cores:" $(($CEPH_NUM_OSD * crimson_smp))-"$(expr $(nproc) - 1)"
-                    $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_thread_cpu_cores $(($CEPH_NUM_OSD * crimson_smp))-"$(expr $(nproc) - 1)"
-                fi
-            fi
-            if [ $crimson_alien_num_threads -gt 0 ]; then
-                echo "$CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads $crimson_alien_num_threads"
-                $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads "$crimson_alien_num_threads"
-            fi
-        else
-          echo "No alien thread cpu core isolation"
+        if [ $crimson_alien_num_threads -gt 0 ]; then
+            echo "$CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads $crimson_alien_num_threads"
+            $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads "$crimson_alien_num_threads"
path: src/vstart.sh
user: login: perezjosibm
body: This refactoring does look much better and simpler 👍 Assuming we definitely no longer need the ceph config options for Alienstore
created_at: 2025-05-06T17:40:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,491 @@
+#!env python3
+import argparse
+import doctest
+import functools
+import itertools
+import logging
+import json
+import os
+import subprocess
+import re
+import sys
+
+root_logger = logging.getLogger(__name__)
+
+
+def get_taskset_output(fname):
+    """
+    Returns contents of fname, or output of taskset -acp
+    if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return f.read()
+    else:
+        return str(
+            subprocess.check_output(
+                ['taskset', '-acp', str(os.getpid())]
+            ), 'utf-8')
+
+
+def taskset_output_to_allowed_cpus(tsout):
+    """
+    Parses taskset output like
+
+    pid 3560663's current affinity list: 0-63\n
+
+    into {0,1,...,63}
+
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-3"))
+    [0, 1, 2, 3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 3"))
+    [3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8"))
+    [0, 1, 2, 3, 4, 5, 8]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8,10-13"))
+    [0, 1, 2, 3, 4, 5, 8, 10, 11, 12, 13]
+    """
+    tsout = re.sub("pid.*list: ", "", tsout)
+    tsout.strip()
+
+    # at this point, tsout should be just the comma delimited ranges
+    ranges = tsout.split(',')
+
+    def parse_range(r):
+        if '-' in r:
+            bounds = r.split('-')
+            if len(bounds) != 2:
+                raise Exception("Invalid taskset string")
+            return set(range(int(bounds[0]), int(bounds[1]) + 1))
+        else:
+            return {int(r)}
+    return functools.reduce(lambda x, y: x | y, map(parse_range, ranges), set())
path: src/tools/contrib/assign_crimson_cores.py
user: login: perezjosibm
body: Nice!
created_at: 2025-05-06T17:50:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,491 @@
+#!env python3
+import argparse
+import doctest
+import functools
+import itertools
+import logging
+import json
+import os
+import subprocess
+import re
+import sys
+
+root_logger = logging.getLogger(__name__)
+
+
+def get_taskset_output(fname):
+    """
+    Returns contents of fname, or output of taskset -acp
+    if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return f.read()
+    else:
+        return str(
+            subprocess.check_output(
+                ['taskset', '-acp', str(os.getpid())]
+            ), 'utf-8')
+
+
+def taskset_output_to_allowed_cpus(tsout):
+    """
+    Parses taskset output like
+
+    pid 3560663's current affinity list: 0-63\n
+
+    into {0,1,...,63}
+
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-3"))
+    [0, 1, 2, 3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 3"))
+    [3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8"))
+    [0, 1, 2, 3, 4, 5, 8]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8,10-13"))
+    [0, 1, 2, 3, 4, 5, 8, 10, 11, 12, 13]
+    """
+    tsout = re.sub("pid.*list: ", "", tsout)
+    tsout.strip()
+
+    # at this point, tsout should be just the comma delimited ranges
+    ranges = tsout.split(',')
+
+    def parse_range(r):
+        if '-' in r:
+            bounds = r.split('-')
+            if len(bounds) != 2:
+                raise Exception("Invalid taskset string")
+            return set(range(int(bounds[0]), int(bounds[1]) + 1))
+        else:
+            return {int(r)}
+    return functools.reduce(lambda x, y: x | y, map(parse_range, ranges), set())
+
+
+def get_lscpu_json(fname):
+    """
+    Loads the contents of fname as json, or the output
+    of lscpu -e --json if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return json.load(f)
+    else:
+        return json.loads(
+            subprocess.check_output(
+                ['lscpu', '-e', '--json']
+            ))
+
+
+def lscpu_json_to_cpuinfo(lscpu_info, allowed_cpus):
+    """
+    Converts json output of lscpu -e --json to CPUInfo filtering
+    for allowed_cpus
+    """
+    # [CPU(cpu, node, core)]
+    cpus = [
+        CPUInfo.CPU(int(cpu['cpu']), int(cpu['node']), int(cpu['core']))
+        for cpu in lscpu_info['cpus']
+    ]
+
+    if allowed_cpus:
+        cpus = list(filter(
+            lambda x: x.cpu in allowed_cpus,
+            cpus))
+    return CPUInfo(cpus)
+
+
+def to_range_str(s):
+    """
+    Returns comma delimited range representing s
+
+    >>> to_range_str({100})
+    '100'
+    >>> to_range_str({1,3,5,7})
+    '1,3,5,7'
+    >>> to_range_str({1,2,3})
+    '1-3'
+    >>> to_range_str({1,4,5,6,7,10})
+    '1,4-7,10'
+    >>> to_range_str({1,3,5,7,8,9})
+    '1,3,5,7-9'
+    """
+    ranges = []
+    start = None
+    last = None
+    for i in sorted(s):
+        if start is None:
+            assert(last is None)
+            start = i
+            last = i
+            continue
+        if i == last + 1:
+            last = i
+            continue
+        ranges.append((start, last))
+        start = i
+        last = i
+    ranges.append((start, last))
+
+    def to_range(item):
+        start, last = item
+        if start == last:
+            return str(start)
+        else:
+            return f"{start}-{last}"
+    return ",".join(map(to_range, ranges))
+
+
+class CPUInfo:
+    class CPU:
+        def __init__(self, cpu: int, node: int, core : int):
+            # cpu id
+            self.cpu = cpu
+            # numa node id
+            self.node = node
+            # physical core id
+            self.core = core
+
+    def __init__(self, cpus):
+        self.__cpus = cpus
+
+    def get_nodes(self):
+        return sorted({x.node for x in self.__cpus})
+
+    def get_cores_for_node(self, node):
+        """
+        Generates (core_id, [cpu_id]) for each core in the given node
+
+        >>> test_cpuinfo_nodes_interleaved(2,4,2).get_cores_for_node(1)
+        [(1, [1, 9]), (3, [3, 11]), (5, [5, 13]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(2,4,2).get_cores_for_node(1)
+        [(4, [4, 12]), (5, [5, 13]), (6, [6, 14]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(1,4,2).get_cores_for_node(0)
+        [(0, [0, 4]), (1, [1, 5]), (2, [2, 6]), (3, [3, 7])]
+        """
+        ret = {}
+        for cpu in self.__cpus:
+            if cpu.node != node:
+                continue
+            if cpu.core not in ret:
+                ret[cpu.core] = []
+            ret[cpu.core].append(cpu.cpu)
+        return [(core, sorted(cpus)) for core, cpus in sorted(ret.items())]
+
+    def get_cores_by_node(self):
+        """
+        Generates [(node, (core_id, [cpu_id]))] ordered by node, core_id
+
+        >>> test_cpuinfo_nodes_seperate(2,2,2).get_cores_by_node()
+        [(0, [(0, [0, 4]), (1, [1, 5])]), (1, [(2, [2, 6]), (3, [3, 7])])]
+        >>> test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node()
+        [(0, [(0, [0]), (1, [1]), (2, [2]), (3, [3])])]
+        """
+        return [(node, self.get_cores_for_node(node)) for node in self.get_nodes()]
+
+    def get_cores_by_node_interleaved(self):
+        """
+        Generates (core_id, [cpu_id]) ordered by core_id, interleaved by
+        node
+
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node_interleaved(), 6))
+        [(0, [0]), (1, [1]), (2, [2]), (3, [3])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (8, [8, 24]), (1, [1, 17]), (9, [9, 25])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_interleaved(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (1, [1, 17]), (2, [2, 18]), (3, [3, 19])]
+        """
+        cores_by_node = self.get_cores_by_node()
+        idx = 0
+        while len(cores_by_node) > 0:
+            node, cores = cores_by_node[idx % len(cores_by_node)]
+            assert(len(cores) > 0)
+            yield cores.pop(0)
+            if len(cores) == 0:
+                cores_by_node.pop(idx % len(cores_by_node))
+            else:
+                idx += 1
+
+
+def test_cpuinfo_nodes_interleaved(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that successive cpus are on successive
+    nodes.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id % num_nodes
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == (num_nodes * cores_per_node * cpus_per_core))
+    return CPUInfo(cpus)
+
+
+def test_cpuinfo_nodes_seperate(num_nodes, cores_per_node, cpus_per_core):
+    """
path: src/tools/contrib/assign_crimson_cores.py
user: login: perezjosibm
body: Typo : it should be "separate" 
created_at: 2025-05-06T18:05:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,491 @@
+#!env python3
+import argparse
+import doctest
+import functools
+import itertools
+import logging
+import json
+import os
+import subprocess
+import re
+import sys
+
+root_logger = logging.getLogger(__name__)
+
+
+def get_taskset_output(fname):
+    """
+    Returns contents of fname, or output of taskset -acp
+    if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return f.read()
+    else:
+        return str(
+            subprocess.check_output(
+                ['taskset', '-acp', str(os.getpid())]
+            ), 'utf-8')
+
+
+def taskset_output_to_allowed_cpus(tsout):
+    """
+    Parses taskset output like
+
+    pid 3560663's current affinity list: 0-63\n
+
+    into {0,1,...,63}
+
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-3"))
+    [0, 1, 2, 3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 3"))
+    [3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8"))
+    [0, 1, 2, 3, 4, 5, 8]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8,10-13"))
+    [0, 1, 2, 3, 4, 5, 8, 10, 11, 12, 13]
+    """
+    tsout = re.sub("pid.*list: ", "", tsout)
+    tsout.strip()
+
+    # at this point, tsout should be just the comma delimited ranges
+    ranges = tsout.split(',')
+
+    def parse_range(r):
+        if '-' in r:
+            bounds = r.split('-')
+            if len(bounds) != 2:
+                raise Exception("Invalid taskset string")
+            return set(range(int(bounds[0]), int(bounds[1]) + 1))
+        else:
+            return {int(r)}
+    return functools.reduce(lambda x, y: x | y, map(parse_range, ranges), set())
+
+
+def get_lscpu_json(fname):
+    """
+    Loads the contents of fname as json, or the output
+    of lscpu -e --json if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return json.load(f)
+    else:
+        return json.loads(
+            subprocess.check_output(
+                ['lscpu', '-e', '--json']
+            ))
+
+
+def lscpu_json_to_cpuinfo(lscpu_info, allowed_cpus):
+    """
+    Converts json output of lscpu -e --json to CPUInfo filtering
+    for allowed_cpus
+    """
+    # [CPU(cpu, node, core)]
+    cpus = [
+        CPUInfo.CPU(int(cpu['cpu']), int(cpu['node']), int(cpu['core']))
+        for cpu in lscpu_info['cpus']
+    ]
+
+    if allowed_cpus:
+        cpus = list(filter(
+            lambda x: x.cpu in allowed_cpus,
+            cpus))
+    return CPUInfo(cpus)
+
+
+def to_range_str(s):
+    """
+    Returns comma delimited range representing s
+
+    >>> to_range_str({100})
+    '100'
+    >>> to_range_str({1,3,5,7})
+    '1,3,5,7'
+    >>> to_range_str({1,2,3})
+    '1-3'
+    >>> to_range_str({1,4,5,6,7,10})
+    '1,4-7,10'
+    >>> to_range_str({1,3,5,7,8,9})
+    '1,3,5,7-9'
+    """
+    ranges = []
+    start = None
+    last = None
+    for i in sorted(s):
+        if start is None:
+            assert(last is None)
+            start = i
+            last = i
+            continue
+        if i == last + 1:
+            last = i
+            continue
+        ranges.append((start, last))
+        start = i
+        last = i
+    ranges.append((start, last))
+
+    def to_range(item):
+        start, last = item
+        if start == last:
+            return str(start)
+        else:
+            return f"{start}-{last}"
+    return ",".join(map(to_range, ranges))
+
+
+class CPUInfo:
+    class CPU:
+        def __init__(self, cpu: int, node: int, core : int):
+            # cpu id
+            self.cpu = cpu
+            # numa node id
+            self.node = node
+            # physical core id
+            self.core = core
+
+    def __init__(self, cpus):
+        self.__cpus = cpus
+
+    def get_nodes(self):
+        return sorted({x.node for x in self.__cpus})
+
+    def get_cores_for_node(self, node):
+        """
+        Generates (core_id, [cpu_id]) for each core in the given node
+
+        >>> test_cpuinfo_nodes_interleaved(2,4,2).get_cores_for_node(1)
+        [(1, [1, 9]), (3, [3, 11]), (5, [5, 13]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(2,4,2).get_cores_for_node(1)
+        [(4, [4, 12]), (5, [5, 13]), (6, [6, 14]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(1,4,2).get_cores_for_node(0)
+        [(0, [0, 4]), (1, [1, 5]), (2, [2, 6]), (3, [3, 7])]
+        """
+        ret = {}
+        for cpu in self.__cpus:
+            if cpu.node != node:
+                continue
+            if cpu.core not in ret:
+                ret[cpu.core] = []
+            ret[cpu.core].append(cpu.cpu)
+        return [(core, sorted(cpus)) for core, cpus in sorted(ret.items())]
+
+    def get_cores_by_node(self):
+        """
+        Generates [(node, (core_id, [cpu_id]))] ordered by node, core_id
+
+        >>> test_cpuinfo_nodes_seperate(2,2,2).get_cores_by_node()
+        [(0, [(0, [0, 4]), (1, [1, 5])]), (1, [(2, [2, 6]), (3, [3, 7])])]
+        >>> test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node()
+        [(0, [(0, [0]), (1, [1]), (2, [2]), (3, [3])])]
+        """
+        return [(node, self.get_cores_for_node(node)) for node in self.get_nodes()]
+
+    def get_cores_by_node_interleaved(self):
+        """
+        Generates (core_id, [cpu_id]) ordered by core_id, interleaved by
+        node
+
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node_interleaved(), 6))
+        [(0, [0]), (1, [1]), (2, [2]), (3, [3])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (8, [8, 24]), (1, [1, 17]), (9, [9, 25])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_interleaved(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (1, [1, 17]), (2, [2, 18]), (3, [3, 19])]
+        """
+        cores_by_node = self.get_cores_by_node()
+        idx = 0
+        while len(cores_by_node) > 0:
+            node, cores = cores_by_node[idx % len(cores_by_node)]
+            assert(len(cores) > 0)
+            yield cores.pop(0)
+            if len(cores) == 0:
+                cores_by_node.pop(idx % len(cores_by_node))
+            else:
+                idx += 1
+
+
+def test_cpuinfo_nodes_interleaved(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that successive cpus are on successive
+    nodes.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id % num_nodes
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == (num_nodes * cores_per_node * cpus_per_core))
+    return CPUInfo(cpus)
+
+
+def test_cpuinfo_nodes_seperate(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that cpu ids are grouped by node.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id // cores_per_node
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == num_cpus)
+    return CPUInfo(cpus)
+
+
+class AllocationParams:
+    def __init__(self, num_cpus : int, physical_only : bool):
+        self.num_cpus = num_cpus
+        self.physical_only = physical_only
+
+
+class Allocation:
+    def __init__(self, cpus):
+        self.cpus = cpus
+
+    def __str__(self):
+        return to_range_str(self.cpus)
+
+    def __repr__(self):
+        return f"allocation({self})"
+
+
+def balance_by_process(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are within a single numa node.
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,2,8,10)], [allocation(1,3,9,11)], [allocation(4,6,12,14)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0,2,4,6)], [allocation(1,3,5,7)], [allocation(8,10,12,14)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(4-5,12-13)], [allocation(2-3,10-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(8-11)], [allocation(4-7)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0,2,32,34), allocation(4,6,8,10)], [allocation(1,3,33,35), allocation(5,7,9,11)], [allocation(12,14,44,46), allocation(16,18,20,22)]]
+    """
+    cores_by_node = cpu_info.get_cores_by_node()
+    ret = [[] for i in range(num_proc)]
+    idx = 0
+    for proc in range(num_proc):
+        node, cores = cores_by_node[idx % len(cores_by_node)]
+        for param in params:
+            cpus = set()
+            while len(cpus) < param.num_cpus:
+                if len(cores) == 0:
+                    raise Exception("Unable to allocate by proc")
+                core, core_cpus = cores.pop(0)
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+        idx += 1
+    return ret
+
+
+def balance_by_socket(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are distributed across all sockets.
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,4,8,12)], [allocation(1,5,9,13)], [allocation(2,6,10,14)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0-1,32-33), allocation(2-5)], [allocation(6-7,38-39), allocation(8-11)], [allocation(12-13,44-45), allocation(14-17)]]
+    """
+    cpu_stream = cpu_info.get_cores_by_node_interleaved()
+    def get_cpu():
+        try:
+            return cpu_stream.__next__()
+        except StopIteration:
+            raise Exception("Unable to allocate by socket")
+
+    ret = [[] for i in range(num_proc)]
+    for proc in range(num_proc):
+        for param in params:
+            cpus = set()
+            while (len(cpus) < param.num_cpus):
+                core_id, core_cpus = get_cpu()
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+    return ret
+
+
+def get_balance_algorithm(arg : str):
+    balance_algorithms = {
+        'osd': balance_by_process,
+        'socket': balance_by_socket
+    }
+    if arg not in balance_algorithms :
+        algs = ", ". join(balance_algorithm_dict.keys())
+        raise Exception(
+            f"balance_algorithm must be one of {algs}"
+        )
+    return balance_algorithms[arg]
+
+
+def main(argv):
+    examples = """
+    Examples:
+    # Produce logical core mappings for use with crimson-osd
+    %prog [-u <lscpu.json>] [-t <allowed_cpus>] [-b <osd|socket>] [-v]
+          [-o <num_osd>] [-r <num_reactors_per_osd>]
+          [--physical-only-seastar] [--physical-only-alienstore]
+
+    Note that allocations may exceed requested cpus if logical siblings are
+    included and request is not a multiple of logical cpus per core.
+    """
+
+    # skip argument parsing for tests
+    if '-x' in argv:
+        doctest.testmod()
+        return
+
+    logger = root_logger.getChild("main")
path: src/tools/contrib/assign_crimson_cores.py
user: login: perezjosibm
body: Nice, I was not aware of this technique
created_at: 2025-05-06T18:27:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,491 @@
+#!env python3
+import argparse
+import doctest
+import functools
+import itertools
+import logging
+import json
+import os
+import subprocess
+import re
+import sys
+
+root_logger = logging.getLogger(__name__)
+
+
+def get_taskset_output(fname):
+    """
+    Returns contents of fname, or output of taskset -acp
+    if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return f.read()
+    else:
+        return str(
+            subprocess.check_output(
+                ['taskset', '-acp', str(os.getpid())]
+            ), 'utf-8')
+
+
+def taskset_output_to_allowed_cpus(tsout):
+    """
+    Parses taskset output like
+
+    pid 3560663's current affinity list: 0-63\n
+
+    into {0,1,...,63}
+
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-3"))
+    [0, 1, 2, 3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 3"))
+    [3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8"))
+    [0, 1, 2, 3, 4, 5, 8]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8,10-13"))
+    [0, 1, 2, 3, 4, 5, 8, 10, 11, 12, 13]
+    """
+    tsout = re.sub("pid.*list: ", "", tsout)
+    tsout.strip()
+
+    # at this point, tsout should be just the comma delimited ranges
+    ranges = tsout.split(',')
+
+    def parse_range(r):
+        if '-' in r:
+            bounds = r.split('-')
+            if len(bounds) != 2:
+                raise Exception("Invalid taskset string")
+            return set(range(int(bounds[0]), int(bounds[1]) + 1))
+        else:
+            return {int(r)}
+    return functools.reduce(lambda x, y: x | y, map(parse_range, ranges), set())
+
+
+def get_lscpu_json(fname):
+    """
+    Loads the contents of fname as json, or the output
+    of lscpu -e --json if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return json.load(f)
+    else:
+        return json.loads(
+            subprocess.check_output(
+                ['lscpu', '-e', '--json']
+            ))
+
+
+def lscpu_json_to_cpuinfo(lscpu_info, allowed_cpus):
+    """
+    Converts json output of lscpu -e --json to CPUInfo filtering
+    for allowed_cpus
+    """
+    # [CPU(cpu, node, core)]
+    cpus = [
+        CPUInfo.CPU(int(cpu['cpu']), int(cpu['node']), int(cpu['core']))
+        for cpu in lscpu_info['cpus']
+    ]
+
+    if allowed_cpus:
+        cpus = list(filter(
+            lambda x: x.cpu in allowed_cpus,
+            cpus))
+    return CPUInfo(cpus)
+
+
+def to_range_str(s):
+    """
+    Returns comma delimited range representing s
+
+    >>> to_range_str({100})
+    '100'
+    >>> to_range_str({1,3,5,7})
+    '1,3,5,7'
+    >>> to_range_str({1,2,3})
+    '1-3'
+    >>> to_range_str({1,4,5,6,7,10})
+    '1,4-7,10'
+    >>> to_range_str({1,3,5,7,8,9})
+    '1,3,5,7-9'
+    """
+    ranges = []
+    start = None
+    last = None
+    for i in sorted(s):
+        if start is None:
+            assert(last is None)
+            start = i
+            last = i
+            continue
+        if i == last + 1:
+            last = i
+            continue
+        ranges.append((start, last))
+        start = i
+        last = i
+    ranges.append((start, last))
+
+    def to_range(item):
+        start, last = item
+        if start == last:
+            return str(start)
+        else:
+            return f"{start}-{last}"
+    return ",".join(map(to_range, ranges))
+
+
+class CPUInfo:
+    class CPU:
+        def __init__(self, cpu: int, node: int, core : int):
+            # cpu id
+            self.cpu = cpu
+            # numa node id
+            self.node = node
+            # physical core id
+            self.core = core
+
+    def __init__(self, cpus):
+        self.__cpus = cpus
+
+    def get_nodes(self):
+        return sorted({x.node for x in self.__cpus})
+
+    def get_cores_for_node(self, node):
+        """
+        Generates (core_id, [cpu_id]) for each core in the given node
+
+        >>> test_cpuinfo_nodes_interleaved(2,4,2).get_cores_for_node(1)
+        [(1, [1, 9]), (3, [3, 11]), (5, [5, 13]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(2,4,2).get_cores_for_node(1)
+        [(4, [4, 12]), (5, [5, 13]), (6, [6, 14]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(1,4,2).get_cores_for_node(0)
+        [(0, [0, 4]), (1, [1, 5]), (2, [2, 6]), (3, [3, 7])]
+        """
+        ret = {}
+        for cpu in self.__cpus:
+            if cpu.node != node:
+                continue
+            if cpu.core not in ret:
+                ret[cpu.core] = []
+            ret[cpu.core].append(cpu.cpu)
+        return [(core, sorted(cpus)) for core, cpus in sorted(ret.items())]
+
+    def get_cores_by_node(self):
+        """
+        Generates [(node, (core_id, [cpu_id]))] ordered by node, core_id
+
+        >>> test_cpuinfo_nodes_seperate(2,2,2).get_cores_by_node()
+        [(0, [(0, [0, 4]), (1, [1, 5])]), (1, [(2, [2, 6]), (3, [3, 7])])]
+        >>> test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node()
+        [(0, [(0, [0]), (1, [1]), (2, [2]), (3, [3])])]
+        """
+        return [(node, self.get_cores_for_node(node)) for node in self.get_nodes()]
+
+    def get_cores_by_node_interleaved(self):
+        """
+        Generates (core_id, [cpu_id]) ordered by core_id, interleaved by
+        node
+
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node_interleaved(), 6))
+        [(0, [0]), (1, [1]), (2, [2]), (3, [3])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (8, [8, 24]), (1, [1, 17]), (9, [9, 25])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_interleaved(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (1, [1, 17]), (2, [2, 18]), (3, [3, 19])]
+        """
+        cores_by_node = self.get_cores_by_node()
+        idx = 0
+        while len(cores_by_node) > 0:
+            node, cores = cores_by_node[idx % len(cores_by_node)]
+            assert(len(cores) > 0)
+            yield cores.pop(0)
+            if len(cores) == 0:
+                cores_by_node.pop(idx % len(cores_by_node))
+            else:
+                idx += 1
+
+
+def test_cpuinfo_nodes_interleaved(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that successive cpus are on successive
+    nodes.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id % num_nodes
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == (num_nodes * cores_per_node * cpus_per_core))
+    return CPUInfo(cpus)
+
+
+def test_cpuinfo_nodes_seperate(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that cpu ids are grouped by node.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id // cores_per_node
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == num_cpus)
+    return CPUInfo(cpus)
+
+
+class AllocationParams:
+    def __init__(self, num_cpus : int, physical_only : bool):
+        self.num_cpus = num_cpus
+        self.physical_only = physical_only
+
+
+class Allocation:
+    def __init__(self, cpus):
+        self.cpus = cpus
+
+    def __str__(self):
+        return to_range_str(self.cpus)
+
+    def __repr__(self):
+        return f"allocation({self})"
+
+
+def balance_by_process(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are within a single numa node.
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,2,8,10)], [allocation(1,3,9,11)], [allocation(4,6,12,14)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0,2,4,6)], [allocation(1,3,5,7)], [allocation(8,10,12,14)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(4-5,12-13)], [allocation(2-3,10-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(8-11)], [allocation(4-7)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0,2,32,34), allocation(4,6,8,10)], [allocation(1,3,33,35), allocation(5,7,9,11)], [allocation(12,14,44,46), allocation(16,18,20,22)]]
+    """
+    cores_by_node = cpu_info.get_cores_by_node()
+    ret = [[] for i in range(num_proc)]
+    idx = 0
+    for proc in range(num_proc):
+        node, cores = cores_by_node[idx % len(cores_by_node)]
+        for param in params:
+            cpus = set()
+            while len(cpus) < param.num_cpus:
+                if len(cores) == 0:
+                    raise Exception("Unable to allocate by proc")
+                core, core_cpus = cores.pop(0)
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+        idx += 1
+    return ret
+
+
+def balance_by_socket(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are distributed across all sockets.
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,4,8,12)], [allocation(1,5,9,13)], [allocation(2,6,10,14)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0-1,32-33), allocation(2-5)], [allocation(6-7,38-39), allocation(8-11)], [allocation(12-13,44-45), allocation(14-17)]]
+    """
+    cpu_stream = cpu_info.get_cores_by_node_interleaved()
+    def get_cpu():
+        try:
+            return cpu_stream.__next__()
+        except StopIteration:
+            raise Exception("Unable to allocate by socket")
+
+    ret = [[] for i in range(num_proc)]
+    for proc in range(num_proc):
+        for param in params:
+            cpus = set()
+            while (len(cpus) < param.num_cpus):
+                core_id, core_cpus = get_cpu()
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+    return ret
+
+
+def get_balance_algorithm(arg : str):
+    balance_algorithms = {
+        'osd': balance_by_process,
+        'socket': balance_by_socket
+    }
+    if arg not in balance_algorithms :
+        algs = ", ". join(balance_algorithm_dict.keys())
+        raise Exception(
+            f"balance_algorithm must be one of {algs}"
+        )
+    return balance_algorithms[arg]
+
+
+def main(argv):
+    examples = """
+    Examples:
+    # Produce logical core mappings for use with crimson-osd
+    %prog [-u <lscpu.json>] [-t <allowed_cpus>] [-b <osd|socket>] [-v]
+          [-o <num_osd>] [-r <num_reactors_per_osd>]
+          [--physical-only-seastar] [--physical-only-alienstore]
+
+    Note that allocations may exceed requested cpus if logical siblings are
+    included and request is not a multiple of logical cpus per core.
+    """
+
+    # skip argument parsing for tests
+    if '-x' in argv:
+        doctest.testmod()
+        return
+
+    logger = root_logger.getChild("main")
+
+    parser = argparse.ArgumentParser(
+        description="Creates cpu mappings appropriate for use with crimson-osd",
+        epilog=examples,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument(
+        "-o",
+        "--num_osd",
+        type=int,
+        required=True,
+        help="Number of OSDs",
+    )
+    parser.add_argument(
+        "-u",
+        "--lscpu",
+        type=str,
+        required=False,
+        help="Input file: .json file produced by lscpu -e --json",
+        default=None,
+    )
+    parser.add_argument(
+        "-t",
+        "--taskset",
path: src/tools/contrib/assign_crimson_cores.py
user: login: perezjosibm
body: So we do support this argument, we might want to use it when invoking from vstart.sh, right?
created_at: 2025-05-06T18:28:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,491 @@
+#!env python3
+import argparse
+import doctest
+import functools
+import itertools
+import logging
+import json
+import os
+import subprocess
+import re
+import sys
+
+root_logger = logging.getLogger(__name__)
+
+
+def get_taskset_output(fname):
+    """
+    Returns contents of fname, or output of taskset -acp
+    if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return f.read()
+    else:
+        return str(
+            subprocess.check_output(
+                ['taskset', '-acp', str(os.getpid())]
+            ), 'utf-8')
+
+
+def taskset_output_to_allowed_cpus(tsout):
+    """
+    Parses taskset output like
+
+    pid 3560663's current affinity list: 0-63\n
+
+    into {0,1,...,63}
+
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-3"))
+    [0, 1, 2, 3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 3"))
+    [3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8"))
+    [0, 1, 2, 3, 4, 5, 8]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8,10-13"))
+    [0, 1, 2, 3, 4, 5, 8, 10, 11, 12, 13]
+    """
+    tsout = re.sub("pid.*list: ", "", tsout)
+    tsout.strip()
+
+    # at this point, tsout should be just the comma delimited ranges
+    ranges = tsout.split(',')
+
+    def parse_range(r):
+        if '-' in r:
+            bounds = r.split('-')
+            if len(bounds) != 2:
+                raise Exception("Invalid taskset string")
+            return set(range(int(bounds[0]), int(bounds[1]) + 1))
+        else:
+            return {int(r)}
+    return functools.reduce(lambda x, y: x | y, map(parse_range, ranges), set())
+
+
+def get_lscpu_json(fname):
+    """
+    Loads the contents of fname as json, or the output
+    of lscpu -e --json if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return json.load(f)
+    else:
+        return json.loads(
+            subprocess.check_output(
+                ['lscpu', '-e', '--json']
+            ))
+
+
+def lscpu_json_to_cpuinfo(lscpu_info, allowed_cpus):
+    """
+    Converts json output of lscpu -e --json to CPUInfo filtering
+    for allowed_cpus
+    """
+    # [CPU(cpu, node, core)]
+    cpus = [
+        CPUInfo.CPU(int(cpu['cpu']), int(cpu['node']), int(cpu['core']))
+        for cpu in lscpu_info['cpus']
+    ]
+
+    if allowed_cpus:
+        cpus = list(filter(
+            lambda x: x.cpu in allowed_cpus,
+            cpus))
+    return CPUInfo(cpus)
+
+
+def to_range_str(s):
+    """
+    Returns comma delimited range representing s
+
+    >>> to_range_str({100})
+    '100'
+    >>> to_range_str({1,3,5,7})
+    '1,3,5,7'
+    >>> to_range_str({1,2,3})
+    '1-3'
+    >>> to_range_str({1,4,5,6,7,10})
+    '1,4-7,10'
+    >>> to_range_str({1,3,5,7,8,9})
+    '1,3,5,7-9'
+    """
+    ranges = []
+    start = None
+    last = None
+    for i in sorted(s):
+        if start is None:
+            assert(last is None)
+            start = i
+            last = i
+            continue
+        if i == last + 1:
+            last = i
+            continue
+        ranges.append((start, last))
+        start = i
+        last = i
+    ranges.append((start, last))
+
+    def to_range(item):
+        start, last = item
+        if start == last:
+            return str(start)
+        else:
+            return f"{start}-{last}"
+    return ",".join(map(to_range, ranges))
+
+
+class CPUInfo:
+    class CPU:
+        def __init__(self, cpu: int, node: int, core : int):
+            # cpu id
+            self.cpu = cpu
+            # numa node id
+            self.node = node
+            # physical core id
+            self.core = core
+
+    def __init__(self, cpus):
+        self.__cpus = cpus
+
+    def get_nodes(self):
+        return sorted({x.node for x in self.__cpus})
+
+    def get_cores_for_node(self, node):
+        """
+        Generates (core_id, [cpu_id]) for each core in the given node
+
+        >>> test_cpuinfo_nodes_interleaved(2,4,2).get_cores_for_node(1)
+        [(1, [1, 9]), (3, [3, 11]), (5, [5, 13]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(2,4,2).get_cores_for_node(1)
+        [(4, [4, 12]), (5, [5, 13]), (6, [6, 14]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(1,4,2).get_cores_for_node(0)
+        [(0, [0, 4]), (1, [1, 5]), (2, [2, 6]), (3, [3, 7])]
+        """
+        ret = {}
+        for cpu in self.__cpus:
+            if cpu.node != node:
+                continue
+            if cpu.core not in ret:
+                ret[cpu.core] = []
+            ret[cpu.core].append(cpu.cpu)
+        return [(core, sorted(cpus)) for core, cpus in sorted(ret.items())]
+
+    def get_cores_by_node(self):
+        """
+        Generates [(node, (core_id, [cpu_id]))] ordered by node, core_id
+
+        >>> test_cpuinfo_nodes_seperate(2,2,2).get_cores_by_node()
+        [(0, [(0, [0, 4]), (1, [1, 5])]), (1, [(2, [2, 6]), (3, [3, 7])])]
+        >>> test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node()
+        [(0, [(0, [0]), (1, [1]), (2, [2]), (3, [3])])]
+        """
+        return [(node, self.get_cores_for_node(node)) for node in self.get_nodes()]
+
+    def get_cores_by_node_interleaved(self):
+        """
+        Generates (core_id, [cpu_id]) ordered by core_id, interleaved by
+        node
+
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node_interleaved(), 6))
+        [(0, [0]), (1, [1]), (2, [2]), (3, [3])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (8, [8, 24]), (1, [1, 17]), (9, [9, 25])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_interleaved(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (1, [1, 17]), (2, [2, 18]), (3, [3, 19])]
+        """
+        cores_by_node = self.get_cores_by_node()
+        idx = 0
+        while len(cores_by_node) > 0:
+            node, cores = cores_by_node[idx % len(cores_by_node)]
+            assert(len(cores) > 0)
+            yield cores.pop(0)
path: src/tools/contrib/assign_crimson_cores.py
user: login: perezjosibm
body: I see, that's why we can use the __next() method down below, neat
created_at: 2025-05-06T18:40:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,491 @@
+#!env python3
+import argparse
+import doctest
+import functools
+import itertools
+import logging
+import json
+import os
+import subprocess
+import re
+import sys
+
+root_logger = logging.getLogger(__name__)
+
+
+def get_taskset_output(fname):
+    """
+    Returns contents of fname, or output of taskset -acp
+    if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return f.read()
+    else:
+        return str(
+            subprocess.check_output(
+                ['taskset', '-acp', str(os.getpid())]
+            ), 'utf-8')
+
+
+def taskset_output_to_allowed_cpus(tsout):
+    """
+    Parses taskset output like
+
+    pid 3560663's current affinity list: 0-63\n
+
+    into {0,1,...,63}
+
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-3"))
+    [0, 1, 2, 3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 3"))
+    [3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8"))
+    [0, 1, 2, 3, 4, 5, 8]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8,10-13"))
+    [0, 1, 2, 3, 4, 5, 8, 10, 11, 12, 13]
+    """
+    tsout = re.sub("pid.*list: ", "", tsout)
+    tsout.strip()
+
+    # at this point, tsout should be just the comma delimited ranges
+    ranges = tsout.split(',')
+
+    def parse_range(r):
+        if '-' in r:
+            bounds = r.split('-')
+            if len(bounds) != 2:
+                raise Exception("Invalid taskset string")
+            return set(range(int(bounds[0]), int(bounds[1]) + 1))
+        else:
+            return {int(r)}
+    return functools.reduce(lambda x, y: x | y, map(parse_range, ranges), set())
+
+
+def get_lscpu_json(fname):
+    """
+    Loads the contents of fname as json, or the output
+    of lscpu -e --json if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return json.load(f)
+    else:
+        return json.loads(
+            subprocess.check_output(
+                ['lscpu', '-e', '--json']
+            ))
+
+
+def lscpu_json_to_cpuinfo(lscpu_info, allowed_cpus):
+    """
+    Converts json output of lscpu -e --json to CPUInfo filtering
+    for allowed_cpus
+    """
+    # [CPU(cpu, node, core)]
+    cpus = [
+        CPUInfo.CPU(int(cpu['cpu']), int(cpu['node']), int(cpu['core']))
+        for cpu in lscpu_info['cpus']
+    ]
+
+    if allowed_cpus:
+        cpus = list(filter(
+            lambda x: x.cpu in allowed_cpus,
+            cpus))
+    return CPUInfo(cpus)
+
+
+def to_range_str(s):
+    """
+    Returns comma delimited range representing s
+
+    >>> to_range_str({100})
+    '100'
+    >>> to_range_str({1,3,5,7})
+    '1,3,5,7'
+    >>> to_range_str({1,2,3})
+    '1-3'
+    >>> to_range_str({1,4,5,6,7,10})
+    '1,4-7,10'
+    >>> to_range_str({1,3,5,7,8,9})
+    '1,3,5,7-9'
+    """
+    ranges = []
+    start = None
+    last = None
+    for i in sorted(s):
+        if start is None:
+            assert(last is None)
+            start = i
+            last = i
+            continue
+        if i == last + 1:
+            last = i
+            continue
+        ranges.append((start, last))
+        start = i
+        last = i
+    ranges.append((start, last))
+
+    def to_range(item):
+        start, last = item
+        if start == last:
+            return str(start)
+        else:
+            return f"{start}-{last}"
+    return ",".join(map(to_range, ranges))
+
+
+class CPUInfo:
+    class CPU:
+        def __init__(self, cpu: int, node: int, core : int):
+            # cpu id
+            self.cpu = cpu
+            # numa node id
+            self.node = node
+            # physical core id
+            self.core = core
+
+    def __init__(self, cpus):
+        self.__cpus = cpus
+
+    def get_nodes(self):
+        return sorted({x.node for x in self.__cpus})
+
+    def get_cores_for_node(self, node):
+        """
+        Generates (core_id, [cpu_id]) for each core in the given node
+
+        >>> test_cpuinfo_nodes_interleaved(2,4,2).get_cores_for_node(1)
+        [(1, [1, 9]), (3, [3, 11]), (5, [5, 13]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(2,4,2).get_cores_for_node(1)
+        [(4, [4, 12]), (5, [5, 13]), (6, [6, 14]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(1,4,2).get_cores_for_node(0)
+        [(0, [0, 4]), (1, [1, 5]), (2, [2, 6]), (3, [3, 7])]
+        """
+        ret = {}
+        for cpu in self.__cpus:
+            if cpu.node != node:
+                continue
+            if cpu.core not in ret:
+                ret[cpu.core] = []
+            ret[cpu.core].append(cpu.cpu)
+        return [(core, sorted(cpus)) for core, cpus in sorted(ret.items())]
+
+    def get_cores_by_node(self):
+        """
+        Generates [(node, (core_id, [cpu_id]))] ordered by node, core_id
+
+        >>> test_cpuinfo_nodes_seperate(2,2,2).get_cores_by_node()
+        [(0, [(0, [0, 4]), (1, [1, 5])]), (1, [(2, [2, 6]), (3, [3, 7])])]
+        >>> test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node()
+        [(0, [(0, [0]), (1, [1]), (2, [2]), (3, [3])])]
+        """
+        return [(node, self.get_cores_for_node(node)) for node in self.get_nodes()]
+
+    def get_cores_by_node_interleaved(self):
+        """
+        Generates (core_id, [cpu_id]) ordered by core_id, interleaved by
+        node
+
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node_interleaved(), 6))
+        [(0, [0]), (1, [1]), (2, [2]), (3, [3])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (8, [8, 24]), (1, [1, 17]), (9, [9, 25])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_interleaved(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (1, [1, 17]), (2, [2, 18]), (3, [3, 19])]
+        """
+        cores_by_node = self.get_cores_by_node()
+        idx = 0
+        while len(cores_by_node) > 0:
+            node, cores = cores_by_node[idx % len(cores_by_node)]
+            assert(len(cores) > 0)
+            yield cores.pop(0)
+            if len(cores) == 0:
+                cores_by_node.pop(idx % len(cores_by_node))
+            else:
+                idx += 1
+
+
+def test_cpuinfo_nodes_interleaved(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that successive cpus are on successive
+    nodes.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id % num_nodes
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == (num_nodes * cores_per_node * cpus_per_core))
+    return CPUInfo(cpus)
+
+
+def test_cpuinfo_nodes_seperate(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that cpu ids are grouped by node.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id // cores_per_node
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == num_cpus)
+    return CPUInfo(cpus)
+
+
+class AllocationParams:
+    def __init__(self, num_cpus : int, physical_only : bool):
+        self.num_cpus = num_cpus
+        self.physical_only = physical_only
+
+
+class Allocation:
+    def __init__(self, cpus):
+        self.cpus = cpus
+
+    def __str__(self):
+        return to_range_str(self.cpus)
+
+    def __repr__(self):
+        return f"allocation({self})"
+
+
+def balance_by_process(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are within a single numa node.
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,2,8,10)], [allocation(1,3,9,11)], [allocation(4,6,12,14)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0,2,4,6)], [allocation(1,3,5,7)], [allocation(8,10,12,14)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(4-5,12-13)], [allocation(2-3,10-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(8-11)], [allocation(4-7)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0,2,32,34), allocation(4,6,8,10)], [allocation(1,3,33,35), allocation(5,7,9,11)], [allocation(12,14,44,46), allocation(16,18,20,22)]]
+    """
+    cores_by_node = cpu_info.get_cores_by_node()
+    ret = [[] for i in range(num_proc)]
+    idx = 0
+    for proc in range(num_proc):
+        node, cores = cores_by_node[idx % len(cores_by_node)]
+        for param in params:
+            cpus = set()
+            while len(cpus) < param.num_cpus:
+                if len(cores) == 0:
+                    raise Exception("Unable to allocate by proc")
+                core, core_cpus = cores.pop(0)
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+        idx += 1
+    return ret
+
+
+def balance_by_socket(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are distributed across all sockets.
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,4,8,12)], [allocation(1,5,9,13)], [allocation(2,6,10,14)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0-1,32-33), allocation(2-5)], [allocation(6-7,38-39), allocation(8-11)], [allocation(12-13,44-45), allocation(14-17)]]
+    """
+    cpu_stream = cpu_info.get_cores_by_node_interleaved()
+    def get_cpu():
+        try:
+            return cpu_stream.__next__()
+        except StopIteration:
+            raise Exception("Unable to allocate by socket")
+
+    ret = [[] for i in range(num_proc)]
+    for proc in range(num_proc):
+        for param in params:
+            cpus = set()
+            while (len(cpus) < param.num_cpus):
+                core_id, core_cpus = get_cpu()
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+    return ret
+
+
+def get_balance_algorithm(arg : str):
+    balance_algorithms = {
+        'osd': balance_by_process,
+        'socket': balance_by_socket
+    }
+    if arg not in balance_algorithms :
+        algs = ", ". join(balance_algorithm_dict.keys())
+        raise Exception(
+            f"balance_algorithm must be one of {algs}"
+        )
+    return balance_algorithms[arg]
+
+
+def main(argv):
+    examples = """
+    Examples:
+    # Produce logical core mappings for use with crimson-osd
+    %prog [-u <lscpu.json>] [-t <allowed_cpus>] [-b <osd|socket>] [-v]
+          [-o <num_osd>] [-r <num_reactors_per_osd>]
+          [--physical-only-seastar] [--physical-only-alienstore]
+
+    Note that allocations may exceed requested cpus if logical siblings are
+    included and request is not a multiple of logical cpus per core.
+    """
+
+    # skip argument parsing for tests
+    if '-x' in argv:
+        doctest.testmod()
+        return
+
+    logger = root_logger.getChild("main")
+
+    parser = argparse.ArgumentParser(
+        description="Creates cpu mappings appropriate for use with crimson-osd",
+        epilog=examples,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument(
+        "-o",
+        "--num_osd",
+        type=int,
+        required=True,
+        help="Number of OSDs",
+    )
+    parser.add_argument(
+        "-u",
+        "--lscpu",
+        type=str,
+        required=False,
+        help="Input file: .json file produced by lscpu -e --json",
+        default=None,
+    )
+    parser.add_argument(
+        "-t",
+        "--taskset",
+        type=str,
+        required=False,
+        help="Allowed CPUs (comma separated ranges format)",
+        default=None,
+    )
+    parser.add_argument(
+        "-r",
+        "--num-reactors",
+        type=int,
+        required=True,
+        help="Number of Seastar reactors per OSD"
+    )
+    parser.add_argument(
+        "-p",
+        "--physical-only-seastar",
+        action="store_true",
+        help="Only use one logical cpu per physical core for seastar reactors",
+        default=False,
+    )
+    parser.add_argument(
+        "-a",
+        "--num-alienstore-threads",
+        type=int,
+        default=0,
+        help="Number of alienstore cores per OSD"
path: src/tools/contrib/assign_crimson_cores.py
user: login: perezjosibm
body: Just a nit: we might prefer to use the name for this argument to be "--num-alienstore-cores" -- or cpus--, rather than threads? 
created_at: 2025-05-06T18:50:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,8 +274,9 @@ options:
 	--seastore-secondary-devs: comma-separated list of secondary blockdevs to use for seastore
 	--seastore-secondary-devs-type: device type of all secondary blockdevs. HDD, SSD(default), ZNS or RANDOM_BLOCK_SSD
 	--crimson-smp: number of cores to use for crimson
+	--crimson-reactor-physical-only: use only one cpu per physical core for seastar reactors
 	--crimson-alien-num-threads: number of alien-tp threads
-	--crimson-alien-num-cores: number of cores to use for alien-tp
+	--crimson-alienstore-physical-only: use only one cpu per physical core for alienstore
path: src/vstart.sh
user: login: perezjosibm
body: I no longer have the slack conversation, but iirc the best results by the team in India showed  that the number of alien threads needed to be four times that of the total of CPU cores used by the Bluestore backend, I think that is part of the reason why there were two parameters for alienstore (num of threads and num of CPU cores). 

For example, here is what I use last in my test driving script:
```
  if [ "$OSD_TYPE" == "blue" ]; then
                  NUM_ALIEN_THREADS=$(( 4 *NUM_OSD * NUM_REACTORS ))
                  title="${title} alien_num_threads=${NUM_ALIEN_THREADS}"
                  cmd="${cmd}  --crimson-alien-num-threads $NUM_ALIEN_THREADS"

```
The code in vstart used the remaining CPUs left once all OSDs reactors have been configured, within the number given by the above option. TBH I haven't worked out the minimal number of CPU cores for Alien threads that gives good performance.

Apologies if I'm wrong/confused.
created_at: 2025-05-06T19:07:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -350,40 +351,37 @@ parse_secondary_devs() {
 
 # Auxiliar function to prepare the CPU cores to pin Seastar reactors
 prep_balance_cpu() {
-    local crimson_smp=$1
-    local balance_strategy=$2
-    local in_file_name="/tmp/numa_args_${balance_strategy}.out"
-    local out_file_name="/tmp/numa_nodes.json"
-    local log_file_name="/tmp/numa_bal_${balance_strategy}.log"
-    local cmd
-
-    # Check the table is empty
-    if [ "${#cpu_table[@]}" -eq 0 ]; then
-        # Ensure the file with the CPU mappings exist
-        if [ ! -f ${in_file_name} ]; then
-            debug echo "lscpu --json > ${out_file_name}"
-            lscpu --json > ${out_file_name}
-            MY_CPUS=$(taskset -acp $$ | awk -F : '{print $2}')
path: src/vstart.sh
user: login: perezjosibm
body: I noticed that your new Python script `assign_crimson_cores.py` supports the allowed set (via parameter --t), can we please support it here? Thanks in advance
created_at: 2025-05-06T19:10:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -350,40 +351,37 @@ parse_secondary_devs() {
 
 # Auxiliar function to prepare the CPU cores to pin Seastar reactors
 prep_balance_cpu() {
-    local crimson_smp=$1
-    local balance_strategy=$2
-    local in_file_name="/tmp/numa_args_${balance_strategy}.out"
-    local out_file_name="/tmp/numa_nodes.json"
-    local log_file_name="/tmp/numa_bal_${balance_strategy}.log"
-    local cmd
-
-    # Check the table is empty
-    if [ "${#cpu_table[@]}" -eq 0 ]; then
-        # Ensure the file with the CPU mappings exist
-        if [ ! -f ${in_file_name} ]; then
-            debug echo "lscpu --json > ${out_file_name}"
-            lscpu --json > ${out_file_name}
-            MY_CPUS=$(taskset -acp $$ | awk -F : '{print $2}')
-            cmd="python3 ${CEPH_DIR}/../src/tools/contrib/balance_cpu.py -o ${CEPH_NUM_OSD}\
-                -r ${crimson_smp}  -b ${balance_strategy} -u ${out_file_name} -t ${MY_CPUS} > ${in_file_name}"
-            debug echo "$cmd"
-            eval "$cmd" >> ${log_file_name}
-        fi
+    if [ -z $crimson_balance_cpu ] || [ "${crimson_balance_cpu}" == "none" ] ; then
+        echo "Not assigning cpus for crimson"
+        return
+    fi
 
-        readarray -t cpu_table < ${in_file_name}
-        # Check the table is not empty, bail out otherwise
-        if [ "${#cpu_table[@]}" -ne 0 ]; then
-            debug echo "CPU table not empty with ${#cpu_table[@]} entries"
-        else
-            debug echo "CPU table empty, bailing out. Check  ${log_file_name}"
-        fi
+    cmd="python3 ${CEPH_DIR}/../src/tools/contrib/assign_crimson_cores.py"
+    cmd+=" -o ${CEPH_NUM_OSD} -r ${crimson_smp} -a ${crimson_alien_num_threads}"
path: src/vstart.sh
user: login: perezjosibm
body: Nit: for Alienstore, we are actually allocating CPUs, so I'd suggest to change the option name to `crimson_alien_num_cores` instead?
created_at: 2025-05-06T19:20:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,491 @@
+#!env python3
+import argparse
+import doctest
+import functools
+import itertools
+import logging
+import json
+import os
+import subprocess
+import re
+import sys
+
+root_logger = logging.getLogger(__name__)
+
+
+def get_taskset_output(fname):
+    """
+    Returns contents of fname, or output of taskset -acp
+    if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return f.read()
+    else:
+        return str(
+            subprocess.check_output(
+                ['taskset', '-acp', str(os.getpid())]
+            ), 'utf-8')
+
+
+def taskset_output_to_allowed_cpus(tsout):
+    """
+    Parses taskset output like
+
+    pid 3560663's current affinity list: 0-63\n
+
+    into {0,1,...,63}
+
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-3"))
+    [0, 1, 2, 3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 3"))
+    [3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8"))
+    [0, 1, 2, 3, 4, 5, 8]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8,10-13"))
+    [0, 1, 2, 3, 4, 5, 8, 10, 11, 12, 13]
+    """
+    tsout = re.sub("pid.*list: ", "", tsout)
+    tsout.strip()
+
+    # at this point, tsout should be just the comma delimited ranges
+    ranges = tsout.split(',')
+
+    def parse_range(r):
+        if '-' in r:
+            bounds = r.split('-')
+            if len(bounds) != 2:
+                raise Exception("Invalid taskset string")
+            return set(range(int(bounds[0]), int(bounds[1]) + 1))
+        else:
+            return {int(r)}
+    return functools.reduce(lambda x, y: x | y, map(parse_range, ranges), set())
+
+
+def get_lscpu_json(fname):
+    """
+    Loads the contents of fname as json, or the output
+    of lscpu -e --json if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return json.load(f)
+    else:
+        return json.loads(
+            subprocess.check_output(
+                ['lscpu', '-e', '--json']
+            ))
+
+
+def lscpu_json_to_cpuinfo(lscpu_info, allowed_cpus):
+    """
+    Converts json output of lscpu -e --json to CPUInfo filtering
+    for allowed_cpus
+    """
+    # [CPU(cpu, node, core)]
+    cpus = [
+        CPUInfo.CPU(int(cpu['cpu']), int(cpu['node']), int(cpu['core']))
+        for cpu in lscpu_info['cpus']
+    ]
+
+    if allowed_cpus:
+        cpus = list(filter(
+            lambda x: x.cpu in allowed_cpus,
+            cpus))
+    return CPUInfo(cpus)
+
+
+def to_range_str(s):
+    """
+    Returns comma delimited range representing s
+
+    >>> to_range_str({100})
+    '100'
+    >>> to_range_str({1,3,5,7})
+    '1,3,5,7'
+    >>> to_range_str({1,2,3})
+    '1-3'
+    >>> to_range_str({1,4,5,6,7,10})
+    '1,4-7,10'
+    >>> to_range_str({1,3,5,7,8,9})
+    '1,3,5,7-9'
+    """
+    ranges = []
+    start = None
+    last = None
+    for i in sorted(s):
+        if start is None:
+            assert(last is None)
+            start = i
+            last = i
+            continue
+        if i == last + 1:
+            last = i
+            continue
+        ranges.append((start, last))
+        start = i
+        last = i
+    ranges.append((start, last))
+
+    def to_range(item):
+        start, last = item
+        if start == last:
+            return str(start)
+        else:
+            return f"{start}-{last}"
+    return ",".join(map(to_range, ranges))
+
+
+class CPUInfo:
+    class CPU:
+        def __init__(self, cpu: int, node: int, core : int):
+            # cpu id
+            self.cpu = cpu
+            # numa node id
+            self.node = node
+            # physical core id
+            self.core = core
+
+    def __init__(self, cpus):
+        self.__cpus = cpus
+
+    def get_nodes(self):
+        return sorted({x.node for x in self.__cpus})
+
+    def get_cores_for_node(self, node):
+        """
+        Generates (core_id, [cpu_id]) for each core in the given node
+
+        >>> test_cpuinfo_nodes_interleaved(2,4,2).get_cores_for_node(1)
+        [(1, [1, 9]), (3, [3, 11]), (5, [5, 13]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(2,4,2).get_cores_for_node(1)
+        [(4, [4, 12]), (5, [5, 13]), (6, [6, 14]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(1,4,2).get_cores_for_node(0)
+        [(0, [0, 4]), (1, [1, 5]), (2, [2, 6]), (3, [3, 7])]
+        """
+        ret = {}
+        for cpu in self.__cpus:
+            if cpu.node != node:
+                continue
+            if cpu.core not in ret:
+                ret[cpu.core] = []
+            ret[cpu.core].append(cpu.cpu)
+        return [(core, sorted(cpus)) for core, cpus in sorted(ret.items())]
+
+    def get_cores_by_node(self):
+        """
+        Generates [(node, (core_id, [cpu_id]))] ordered by node, core_id
+
+        >>> test_cpuinfo_nodes_seperate(2,2,2).get_cores_by_node()
+        [(0, [(0, [0, 4]), (1, [1, 5])]), (1, [(2, [2, 6]), (3, [3, 7])])]
+        >>> test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node()
+        [(0, [(0, [0]), (1, [1]), (2, [2]), (3, [3])])]
+        """
+        return [(node, self.get_cores_for_node(node)) for node in self.get_nodes()]
+
+    def get_cores_by_node_interleaved(self):
+        """
+        Generates (core_id, [cpu_id]) ordered by core_id, interleaved by
+        node
+
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node_interleaved(), 6))
+        [(0, [0]), (1, [1]), (2, [2]), (3, [3])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (8, [8, 24]), (1, [1, 17]), (9, [9, 25])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_interleaved(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (1, [1, 17]), (2, [2, 18]), (3, [3, 19])]
+        """
+        cores_by_node = self.get_cores_by_node()
+        idx = 0
+        while len(cores_by_node) > 0:
+            node, cores = cores_by_node[idx % len(cores_by_node)]
+            assert(len(cores) > 0)
+            yield cores.pop(0)
+            if len(cores) == 0:
+                cores_by_node.pop(idx % len(cores_by_node))
+            else:
+                idx += 1
+
+
+def test_cpuinfo_nodes_interleaved(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that successive cpus are on successive
+    nodes.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id % num_nodes
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == (num_nodes * cores_per_node * cpus_per_core))
+    return CPUInfo(cpus)
+
+
+def test_cpuinfo_nodes_seperate(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that cpu ids are grouped by node.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id // cores_per_node
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == num_cpus)
+    return CPUInfo(cpus)
+
+
+class AllocationParams:
+    def __init__(self, num_cpus : int, physical_only : bool):
+        self.num_cpus = num_cpus
+        self.physical_only = physical_only
+
+
+class Allocation:
+    def __init__(self, cpus):
+        self.cpus = cpus
+
+    def __str__(self):
+        return to_range_str(self.cpus)
+
+    def __repr__(self):
+        return f"allocation({self})"
+
+
+def balance_by_process(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are within a single numa node.
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,2,8,10)], [allocation(1,3,9,11)], [allocation(4,6,12,14)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0,2,4,6)], [allocation(1,3,5,7)], [allocation(8,10,12,14)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(4-5,12-13)], [allocation(2-3,10-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(8-11)], [allocation(4-7)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0,2,32,34), allocation(4,6,8,10)], [allocation(1,3,33,35), allocation(5,7,9,11)], [allocation(12,14,44,46), allocation(16,18,20,22)]]
+    """
+    cores_by_node = cpu_info.get_cores_by_node()
+    ret = [[] for i in range(num_proc)]
+    idx = 0
+    for proc in range(num_proc):
+        node, cores = cores_by_node[idx % len(cores_by_node)]
+        for param in params:
+            cpus = set()
+            while len(cpus) < param.num_cpus:
+                if len(cores) == 0:
+                    raise Exception("Unable to allocate by proc")
+                core, core_cpus = cores.pop(0)
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+        idx += 1
+    return ret
+
+
+def balance_by_socket(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are distributed across all sockets.
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,4,8,12)], [allocation(1,5,9,13)], [allocation(2,6,10,14)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0-1,32-33), allocation(2-5)], [allocation(6-7,38-39), allocation(8-11)], [allocation(12-13,44-45), allocation(14-17)]]
+    """
+    cpu_stream = cpu_info.get_cores_by_node_interleaved()
+    def get_cpu():
+        try:
+            return cpu_stream.__next__()
+        except StopIteration:
+            raise Exception("Unable to allocate by socket")
+
+    ret = [[] for i in range(num_proc)]
+    for proc in range(num_proc):
+        for param in params:
+            cpus = set()
+            while (len(cpus) < param.num_cpus):
+                core_id, core_cpus = get_cpu()
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+    return ret
+
+
+def get_balance_algorithm(arg : str):
+    balance_algorithms = {
+        'osd': balance_by_process,
+        'socket': balance_by_socket
+    }
+    if arg not in balance_algorithms :
+        algs = ", ". join(balance_algorithm_dict.keys())
+        raise Exception(
+            f"balance_algorithm must be one of {algs}"
+        )
+    return balance_algorithms[arg]
+
+
+def main(argv):
+    examples = """
+    Examples:
+    # Produce logical core mappings for use with crimson-osd
+    %prog [-u <lscpu.json>] [-t <allowed_cpus>] [-b <osd|socket>] [-v]
+          [-o <num_osd>] [-r <num_reactors_per_osd>]
+          [--physical-only-seastar] [--physical-only-alienstore]
+
+    Note that allocations may exceed requested cpus if logical siblings are
+    included and request is not a multiple of logical cpus per core.
+    """
+
+    # skip argument parsing for tests
+    if '-x' in argv:
+        doctest.testmod()
+        return
+
+    logger = root_logger.getChild("main")
+
+    parser = argparse.ArgumentParser(
+        description="Creates cpu mappings appropriate for use with crimson-osd",
+        epilog=examples,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument(
+        "-o",
+        "--num_osd",
+        type=int,
+        required=True,
+        help="Number of OSDs",
+    )
+    parser.add_argument(
+        "-u",
+        "--lscpu",
+        type=str,
+        required=False,
+        help="Input file: .json file produced by lscpu -e --json",
+        default=None,
+    )
+    parser.add_argument(
+        "-t",
+        "--taskset",
path: src/tools/contrib/assign_crimson_cores.py
user: login: athanatos
body: I don't see any reason for vstart not to let this script to it internally.  I left the argument simply to allow a user to take outputs from another host, but that doesn't really apply to vstart.
created_at: 2025-05-06T19:54:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -350,40 +351,37 @@ parse_secondary_devs() {
 
 # Auxiliar function to prepare the CPU cores to pin Seastar reactors
 prep_balance_cpu() {
-    local crimson_smp=$1
-    local balance_strategy=$2
-    local in_file_name="/tmp/numa_args_${balance_strategy}.out"
-    local out_file_name="/tmp/numa_nodes.json"
-    local log_file_name="/tmp/numa_bal_${balance_strategy}.log"
-    local cmd
-
-    # Check the table is empty
-    if [ "${#cpu_table[@]}" -eq 0 ]; then
-        # Ensure the file with the CPU mappings exist
-        if [ ! -f ${in_file_name} ]; then
-            debug echo "lscpu --json > ${out_file_name}"
-            lscpu --json > ${out_file_name}
-            MY_CPUS=$(taskset -acp $$ | awk -F : '{print $2}')
path: src/vstart.sh
user: login: athanatos
body: assign_crimson_cores.py simply calls taskset internally.

```
    cpuinfo = lscpu_json_to_cpuinfo(
        get_lscpu_json(options.lscpu),
        taskset_output_to_allowed_cpus(get_taskset_output(options.taskset))
    )
```

The cpuinfo there is already filtered by the taskset output from ```taskset_output_to_allowed_cpus(get_taskset_output(options.taskset))```.
created_at: 2025-05-06T19:57:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,8 +274,9 @@ options:
 	--seastore-secondary-devs: comma-separated list of secondary blockdevs to use for seastore
 	--seastore-secondary-devs-type: device type of all secondary blockdevs. HDD, SSD(default), ZNS or RANDOM_BLOCK_SSD
 	--crimson-smp: number of cores to use for crimson
+	--crimson-reactor-physical-only: use only one cpu per physical core for seastar reactors
 	--crimson-alien-num-threads: number of alien-tp threads
-	--crimson-alien-num-cores: number of cores to use for alien-tp
+	--crimson-alienstore-physical-only: use only one cpu per physical core for alienstore
path: src/vstart.sh
user: login: athanatos
body: Ah, I was sloppy and misunderstood that distinction:

```
-            if [ $crimson_alien_num_threads -gt 0 ]; then
-                echo "$CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads $crimson_alien_num_threads"
-                $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads "$crimson_alien_num_threads"
-            fi
```

I'll add it back in.
created_at: 2025-05-06T20:02:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,491 @@
+#!env python3
+import argparse
+import doctest
+import functools
+import itertools
+import logging
+import json
+import os
+import subprocess
+import re
+import sys
+
+root_logger = logging.getLogger(__name__)
+
+
+def get_taskset_output(fname):
+    """
+    Returns contents of fname, or output of taskset -acp
+    if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return f.read()
+    else:
+        return str(
+            subprocess.check_output(
+                ['taskset', '-acp', str(os.getpid())]
+            ), 'utf-8')
+
+
+def taskset_output_to_allowed_cpus(tsout):
+    """
+    Parses taskset output like
+
+    pid 3560663's current affinity list: 0-63\n
+
+    into {0,1,...,63}
+
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-3"))
+    [0, 1, 2, 3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 3"))
+    [3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8"))
+    [0, 1, 2, 3, 4, 5, 8]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8,10-13"))
+    [0, 1, 2, 3, 4, 5, 8, 10, 11, 12, 13]
+    """
+    tsout = re.sub("pid.*list: ", "", tsout)
+    tsout.strip()
+
+    # at this point, tsout should be just the comma delimited ranges
+    ranges = tsout.split(',')
+
+    def parse_range(r):
+        if '-' in r:
+            bounds = r.split('-')
+            if len(bounds) != 2:
+                raise Exception("Invalid taskset string")
+            return set(range(int(bounds[0]), int(bounds[1]) + 1))
+        else:
+            return {int(r)}
+    return functools.reduce(lambda x, y: x | y, map(parse_range, ranges), set())
+
+
+def get_lscpu_json(fname):
+    """
+    Loads the contents of fname as json, or the output
+    of lscpu -e --json if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return json.load(f)
+    else:
+        return json.loads(
+            subprocess.check_output(
+                ['lscpu', '-e', '--json']
+            ))
+
+
+def lscpu_json_to_cpuinfo(lscpu_info, allowed_cpus):
+    """
+    Converts json output of lscpu -e --json to CPUInfo filtering
+    for allowed_cpus
+    """
+    # [CPU(cpu, node, core)]
+    cpus = [
+        CPUInfo.CPU(int(cpu['cpu']), int(cpu['node']), int(cpu['core']))
+        for cpu in lscpu_info['cpus']
+    ]
+
+    if allowed_cpus:
+        cpus = list(filter(
+            lambda x: x.cpu in allowed_cpus,
+            cpus))
+    return CPUInfo(cpus)
+
+
+def to_range_str(s):
+    """
+    Returns comma delimited range representing s
+
+    >>> to_range_str({100})
+    '100'
+    >>> to_range_str({1,3,5,7})
+    '1,3,5,7'
+    >>> to_range_str({1,2,3})
+    '1-3'
+    >>> to_range_str({1,4,5,6,7,10})
+    '1,4-7,10'
+    >>> to_range_str({1,3,5,7,8,9})
+    '1,3,5,7-9'
+    """
+    ranges = []
+    start = None
+    last = None
+    for i in sorted(s):
+        if start is None:
+            assert(last is None)
+            start = i
+            last = i
+            continue
+        if i == last + 1:
+            last = i
+            continue
+        ranges.append((start, last))
+        start = i
+        last = i
+    ranges.append((start, last))
+
+    def to_range(item):
+        start, last = item
+        if start == last:
+            return str(start)
+        else:
+            return f"{start}-{last}"
+    return ",".join(map(to_range, ranges))
+
+
+class CPUInfo:
+    class CPU:
+        def __init__(self, cpu: int, node: int, core : int):
+            # cpu id
+            self.cpu = cpu
+            # numa node id
+            self.node = node
+            # physical core id
+            self.core = core
+
+    def __init__(self, cpus):
+        self.__cpus = cpus
+
+    def get_nodes(self):
+        return sorted({x.node for x in self.__cpus})
+
+    def get_cores_for_node(self, node):
+        """
+        Generates (core_id, [cpu_id]) for each core in the given node
+
+        >>> test_cpuinfo_nodes_interleaved(2,4,2).get_cores_for_node(1)
+        [(1, [1, 9]), (3, [3, 11]), (5, [5, 13]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(2,4,2).get_cores_for_node(1)
+        [(4, [4, 12]), (5, [5, 13]), (6, [6, 14]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(1,4,2).get_cores_for_node(0)
+        [(0, [0, 4]), (1, [1, 5]), (2, [2, 6]), (3, [3, 7])]
+        """
+        ret = {}
+        for cpu in self.__cpus:
+            if cpu.node != node:
+                continue
+            if cpu.core not in ret:
+                ret[cpu.core] = []
+            ret[cpu.core].append(cpu.cpu)
+        return [(core, sorted(cpus)) for core, cpus in sorted(ret.items())]
+
+    def get_cores_by_node(self):
+        """
+        Generates [(node, (core_id, [cpu_id]))] ordered by node, core_id
+
+        >>> test_cpuinfo_nodes_seperate(2,2,2).get_cores_by_node()
+        [(0, [(0, [0, 4]), (1, [1, 5])]), (1, [(2, [2, 6]), (3, [3, 7])])]
+        >>> test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node()
+        [(0, [(0, [0]), (1, [1]), (2, [2]), (3, [3])])]
+        """
+        return [(node, self.get_cores_for_node(node)) for node in self.get_nodes()]
+
+    def get_cores_by_node_interleaved(self):
+        """
+        Generates (core_id, [cpu_id]) ordered by core_id, interleaved by
+        node
+
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node_interleaved(), 6))
+        [(0, [0]), (1, [1]), (2, [2]), (3, [3])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (8, [8, 24]), (1, [1, 17]), (9, [9, 25])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_interleaved(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (1, [1, 17]), (2, [2, 18]), (3, [3, 19])]
+        """
+        cores_by_node = self.get_cores_by_node()
+        idx = 0
+        while len(cores_by_node) > 0:
+            node, cores = cores_by_node[idx % len(cores_by_node)]
+            assert(len(cores) > 0)
+            yield cores.pop(0)
+            if len(cores) == 0:
+                cores_by_node.pop(idx % len(cores_by_node))
+            else:
+                idx += 1
+
+
+def test_cpuinfo_nodes_interleaved(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that successive cpus are on successive
+    nodes.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id % num_nodes
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == (num_nodes * cores_per_node * cpus_per_core))
+    return CPUInfo(cpus)
+
+
+def test_cpuinfo_nodes_seperate(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that cpu ids are grouped by node.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id // cores_per_node
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == num_cpus)
+    return CPUInfo(cpus)
+
+
+class AllocationParams:
+    def __init__(self, num_cpus : int, physical_only : bool):
+        self.num_cpus = num_cpus
+        self.physical_only = physical_only
+
+
+class Allocation:
+    def __init__(self, cpus):
+        self.cpus = cpus
+
+    def __str__(self):
+        return to_range_str(self.cpus)
+
+    def __repr__(self):
+        return f"allocation({self})"
+
+
+def balance_by_process(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are within a single numa node.
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,2,8,10)], [allocation(1,3,9,11)], [allocation(4,6,12,14)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0,2,4,6)], [allocation(1,3,5,7)], [allocation(8,10,12,14)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(4-5,12-13)], [allocation(2-3,10-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(8-11)], [allocation(4-7)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0,2,32,34), allocation(4,6,8,10)], [allocation(1,3,33,35), allocation(5,7,9,11)], [allocation(12,14,44,46), allocation(16,18,20,22)]]
+    """
+    cores_by_node = cpu_info.get_cores_by_node()
+    ret = [[] for i in range(num_proc)]
+    idx = 0
+    for proc in range(num_proc):
+        node, cores = cores_by_node[idx % len(cores_by_node)]
+        for param in params:
+            cpus = set()
+            while len(cpus) < param.num_cpus:
+                if len(cores) == 0:
+                    raise Exception("Unable to allocate by proc")
+                core, core_cpus = cores.pop(0)
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+        idx += 1
+    return ret
+
+
+def balance_by_socket(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are distributed across all sockets.
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,4,8,12)], [allocation(1,5,9,13)], [allocation(2,6,10,14)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0-1,32-33), allocation(2-5)], [allocation(6-7,38-39), allocation(8-11)], [allocation(12-13,44-45), allocation(14-17)]]
+    """
+    cpu_stream = cpu_info.get_cores_by_node_interleaved()
+    def get_cpu():
+        try:
+            return cpu_stream.__next__()
+        except StopIteration:
+            raise Exception("Unable to allocate by socket")
+
+    ret = [[] for i in range(num_proc)]
+    for proc in range(num_proc):
+        for param in params:
+            cpus = set()
+            while (len(cpus) < param.num_cpus):
+                core_id, core_cpus = get_cpu()
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+    return ret
+
+
+def get_balance_algorithm(arg : str):
+    balance_algorithms = {
+        'osd': balance_by_process,
+        'socket': balance_by_socket
+    }
+    if arg not in balance_algorithms :
+        algs = ", ". join(balance_algorithm_dict.keys())
+        raise Exception(
+            f"balance_algorithm must be one of {algs}"
+        )
+    return balance_algorithms[arg]
+
+
+def main(argv):
+    examples = """
+    Examples:
+    # Produce logical core mappings for use with crimson-osd
+    %prog [-u <lscpu.json>] [-t <allowed_cpus>] [-b <osd|socket>] [-v]
+          [-o <num_osd>] [-r <num_reactors_per_osd>]
+          [--physical-only-seastar] [--physical-only-alienstore]
+
+    Note that allocations may exceed requested cpus if logical siblings are
+    included and request is not a multiple of logical cpus per core.
+    """
+
+    # skip argument parsing for tests
+    if '-x' in argv:
+        doctest.testmod()
+        return
+
+    logger = root_logger.getChild("main")
+
+    parser = argparse.ArgumentParser(
+        description="Creates cpu mappings appropriate for use with crimson-osd",
+        epilog=examples,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument(
+        "-o",
+        "--num_osd",
+        type=int,
+        required=True,
+        help="Number of OSDs",
+    )
+    parser.add_argument(
+        "-u",
+        "--lscpu",
+        type=str,
+        required=False,
+        help="Input file: .json file produced by lscpu -e --json",
+        default=None,
+    )
+    parser.add_argument(
+        "-t",
+        "--taskset",
path: src/tools/contrib/assign_crimson_cores.py
user: login: athanatos
body: To clarify, if the argument is omitted this script calls taskset internally:

```
    cpuinfo = lscpu_json_to_cpuinfo(
        get_lscpu_json(options.lscpu),
        taskset_output_to_allowed_cpus(get_taskset_output(options.taskset))
    )
```
created_at: 2025-05-06T20:17:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1760,37 +1782,9 @@ if [ "$ceph_osd" == "crimson-osd" ]; then
     fi
     if [ "$objectstore" == "bluestore" ]; then
         # This condition verifies the number of logical CPU cores
-        if [ "$(expr $(nproc) - 1)" -gt "$(($CEPH_NUM_OSD * crimson_smp))" ]; then
-            if [ ! -z "$crimson_balance_cpu" ]; then
-                debug echo "Preparing balance CPU for Crimson"
-                prep_balance_cpu $crimson_smp $crimson_balance_cpu
-                available_cpus="${cpu_table[-1]}"
-                echo "crimson_alien_thread_cpu_cores: '$available_cpus'"
-                $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_thread_cpu_cores "$available_cpus"
-            else
-                if [ $crimson_alien_num_cores -gt 0 ]; then
-                    alien_bottom_cpu=$(($CEPH_NUM_OSD * crimson_smp))
-                    alien_top_cpu=$(( alien_bottom_cpu + crimson_alien_num_cores - 1 ))
-                    # Ensure top value within range:
-                    if [ "$(($alien_top_cpu))" -gt "$(expr $(nproc) - 1)" ]; then
-                        alien_top_cpu=$(expr $(nproc) - 1)
-                    fi
-                    echo "crimson_alien_thread_cpu_cores: $alien_bottom_cpu-$alien_top_cpu"
-                    # This is a (logical) processor id range, it could be refined to encompass only physical processor ids
-                    # (equivalently, ignore hyperthreading sibling processor ids)
-                    $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_thread_cpu_cores "$alien_bottom_cpu-$alien_top_cpu"
-                else
-                    # This is the legacy default case
-                    echo "crimson_alien_thread_cpu_cores:" $(($CEPH_NUM_OSD * crimson_smp))-"$(expr $(nproc) - 1)"
-                    $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_thread_cpu_cores $(($CEPH_NUM_OSD * crimson_smp))-"$(expr $(nproc) - 1)"
-                fi
-            fi
-            if [ $crimson_alien_num_threads -gt 0 ]; then
-                echo "$CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads $crimson_alien_num_threads"
-                $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads "$crimson_alien_num_threads"
-            fi
-        else
-          echo "No alien thread cpu core isolation"
+        if [ $crimson_alien_num_threads -gt 0 ]; then
+            echo "$CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads $crimson_alien_num_threads"
+            $CEPH_BIN/ceph -c $conf_fn config set osd crimson_alien_op_num_threads "$crimson_alien_num_threads"
path: src/vstart.sh
user: login: athanatos
body: Added back distinct alien-threads and alien-cores options.
created_at: 2025-05-06T20:20:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,491 @@
+#!env python3
+import argparse
+import doctest
+import functools
+import itertools
+import logging
+import json
+import os
+import subprocess
+import re
+import sys
+
+root_logger = logging.getLogger(__name__)
+
+
+def get_taskset_output(fname):
+    """
+    Returns contents of fname, or output of taskset -acp
+    if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return f.read()
+    else:
+        return str(
+            subprocess.check_output(
+                ['taskset', '-acp', str(os.getpid())]
+            ), 'utf-8')
+
+
+def taskset_output_to_allowed_cpus(tsout):
+    """
+    Parses taskset output like
+
+    pid 3560663's current affinity list: 0-63\n
+
+    into {0,1,...,63}
+
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-3"))
+    [0, 1, 2, 3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 3"))
+    [3]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8"))
+    [0, 1, 2, 3, 4, 5, 8]
+    >>> sorted(taskset_output_to_allowed_cpus("pid 3560663's current affinity list: 0-5,8,10-13"))
+    [0, 1, 2, 3, 4, 5, 8, 10, 11, 12, 13]
+    """
+    tsout = re.sub("pid.*list: ", "", tsout)
+    tsout.strip()
+
+    # at this point, tsout should be just the comma delimited ranges
+    ranges = tsout.split(',')
+
+    def parse_range(r):
+        if '-' in r:
+            bounds = r.split('-')
+            if len(bounds) != 2:
+                raise Exception("Invalid taskset string")
+            return set(range(int(bounds[0]), int(bounds[1]) + 1))
+        else:
+            return {int(r)}
+    return functools.reduce(lambda x, y: x | y, map(parse_range, ranges), set())
+
+
+def get_lscpu_json(fname):
+    """
+    Loads the contents of fname as json, or the output
+    of lscpu -e --json if fname is None
+    """
+    if fname:
+        with open(fname, 'r') as f:
+            return json.load(f)
+    else:
+        return json.loads(
+            subprocess.check_output(
+                ['lscpu', '-e', '--json']
+            ))
+
+
+def lscpu_json_to_cpuinfo(lscpu_info, allowed_cpus):
+    """
+    Converts json output of lscpu -e --json to CPUInfo filtering
+    for allowed_cpus
+    """
+    # [CPU(cpu, node, core)]
+    cpus = [
+        CPUInfo.CPU(int(cpu['cpu']), int(cpu['node']), int(cpu['core']))
+        for cpu in lscpu_info['cpus']
+    ]
+
+    if allowed_cpus:
+        cpus = list(filter(
+            lambda x: x.cpu in allowed_cpus,
+            cpus))
+    return CPUInfo(cpus)
+
+
+def to_range_str(s):
+    """
+    Returns comma delimited range representing s
+
+    >>> to_range_str({100})
+    '100'
+    >>> to_range_str({1,3,5,7})
+    '1,3,5,7'
+    >>> to_range_str({1,2,3})
+    '1-3'
+    >>> to_range_str({1,4,5,6,7,10})
+    '1,4-7,10'
+    >>> to_range_str({1,3,5,7,8,9})
+    '1,3,5,7-9'
+    """
+    ranges = []
+    start = None
+    last = None
+    for i in sorted(s):
+        if start is None:
+            assert(last is None)
+            start = i
+            last = i
+            continue
+        if i == last + 1:
+            last = i
+            continue
+        ranges.append((start, last))
+        start = i
+        last = i
+    ranges.append((start, last))
+
+    def to_range(item):
+        start, last = item
+        if start == last:
+            return str(start)
+        else:
+            return f"{start}-{last}"
+    return ",".join(map(to_range, ranges))
+
+
+class CPUInfo:
+    class CPU:
+        def __init__(self, cpu: int, node: int, core : int):
+            # cpu id
+            self.cpu = cpu
+            # numa node id
+            self.node = node
+            # physical core id
+            self.core = core
+
+    def __init__(self, cpus):
+        self.__cpus = cpus
+
+    def get_nodes(self):
+        return sorted({x.node for x in self.__cpus})
+
+    def get_cores_for_node(self, node):
+        """
+        Generates (core_id, [cpu_id]) for each core in the given node
+
+        >>> test_cpuinfo_nodes_interleaved(2,4,2).get_cores_for_node(1)
+        [(1, [1, 9]), (3, [3, 11]), (5, [5, 13]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(2,4,2).get_cores_for_node(1)
+        [(4, [4, 12]), (5, [5, 13]), (6, [6, 14]), (7, [7, 15])]
+        >>> test_cpuinfo_nodes_seperate(1,4,2).get_cores_for_node(0)
+        [(0, [0, 4]), (1, [1, 5]), (2, [2, 6]), (3, [3, 7])]
+        """
+        ret = {}
+        for cpu in self.__cpus:
+            if cpu.node != node:
+                continue
+            if cpu.core not in ret:
+                ret[cpu.core] = []
+            ret[cpu.core].append(cpu.cpu)
+        return [(core, sorted(cpus)) for core, cpus in sorted(ret.items())]
+
+    def get_cores_by_node(self):
+        """
+        Generates [(node, (core_id, [cpu_id]))] ordered by node, core_id
+
+        >>> test_cpuinfo_nodes_seperate(2,2,2).get_cores_by_node()
+        [(0, [(0, [0, 4]), (1, [1, 5])]), (1, [(2, [2, 6]), (3, [3, 7])])]
+        >>> test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node()
+        [(0, [(0, [0]), (1, [1]), (2, [2]), (3, [3])])]
+        """
+        return [(node, self.get_cores_for_node(node)) for node in self.get_nodes()]
+
+    def get_cores_by_node_interleaved(self):
+        """
+        Generates (core_id, [cpu_id]) ordered by core_id, interleaved by
+        node
+
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(1,4,1).get_cores_by_node_interleaved(), 6))
+        [(0, [0]), (1, [1]), (2, [2]), (3, [3])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_seperate(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (8, [8, 24]), (1, [1, 17]), (9, [9, 25])]
+        >>> list(itertools.islice(test_cpuinfo_nodes_interleaved(2,8,2).get_cores_by_node_interleaved(), 4))
+        [(0, [0, 16]), (1, [1, 17]), (2, [2, 18]), (3, [3, 19])]
+        """
+        cores_by_node = self.get_cores_by_node()
+        idx = 0
+        while len(cores_by_node) > 0:
+            node, cores = cores_by_node[idx % len(cores_by_node)]
+            assert(len(cores) > 0)
+            yield cores.pop(0)
+            if len(cores) == 0:
+                cores_by_node.pop(idx % len(cores_by_node))
+            else:
+                idx += 1
+
+
+def test_cpuinfo_nodes_interleaved(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that successive cpus are on successive
+    nodes.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id % num_nodes
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == (num_nodes * cores_per_node * cpus_per_core))
+    return CPUInfo(cpus)
+
+
+def test_cpuinfo_nodes_seperate(num_nodes, cores_per_node, cpus_per_core):
+    """
+    Generates test CPUInfo such that cpu ids are grouped by node.
+    """
+    num_cpus = num_nodes * cores_per_node * cpus_per_core
+    num_cores = num_nodes * cores_per_node
+    cpus = []
+    for core_id in range(num_cores):
+        node_id = core_id // cores_per_node
+        for cpu_id in range(core_id, num_cpus, num_cores):
+            cpus.append(CPUInfo.CPU(cpu_id, node_id, core_id))
+    assert(len(cpus) == num_cpus)
+    return CPUInfo(cpus)
+
+
+class AllocationParams:
+    def __init__(self, num_cpus : int, physical_only : bool):
+        self.num_cpus = num_cpus
+        self.physical_only = physical_only
+
+
+class Allocation:
+    def __init__(self, cpus):
+        self.cpus = cpus
+
+    def __str__(self):
+        return to_range_str(self.cpus)
+
+    def __repr__(self):
+        return f"allocation({self})"
+
+
+def balance_by_process(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are within a single numa node.
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,2,8,10)], [allocation(1,3,9,11)], [allocation(4,6,12,14)]]
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0,2,4,6)], [allocation(1,3,5,7)], [allocation(8,10,12,14)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(4-5,12-13)], [allocation(2-3,10-11)]]
+    >>> balance_by_process(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(8-11)], [allocation(4-7)]]
+
+    >>> balance_by_process(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0,2,32,34), allocation(4,6,8,10)], [allocation(1,3,33,35), allocation(5,7,9,11)], [allocation(12,14,44,46), allocation(16,18,20,22)]]
+    """
+    cores_by_node = cpu_info.get_cores_by_node()
+    ret = [[] for i in range(num_proc)]
+    idx = 0
+    for proc in range(num_proc):
+        node, cores = cores_by_node[idx % len(cores_by_node)]
+        for param in params:
+            cpus = set()
+            while len(cpus) < param.num_cpus:
+                if len(cores) == 0:
+                    raise Exception("Unable to allocate by proc")
+                core, core_cpus = cores.pop(0)
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+        idx += 1
+    return ret
+
+
+def balance_by_socket(
+        cpu_info : CPUInfo,
+        num_proc : int,
+        params : list[AllocationParams]) -> list[list[Allocation]]:
+    """
+    Allocates cpus for each param to each process such that each process's
+    cpus are distributed across all sockets.
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,8,2), 3, [AllocationParams(4, False)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(1,16,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-3)], [allocation(4-7)], [allocation(8-11)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,4,2), 3, [AllocationParams(4, False)])
+    [[allocation(0,4,8,12)], [allocation(1,5,9,13)], [allocation(2,6,10,14)]]
+    >>> balance_by_socket(test_cpuinfo_nodes_seperate(2,8,2), 3, [AllocationParams(4, True)])
+    [[allocation(0-1,8-9)], [allocation(2-3,10-11)], [allocation(4-5,12-13)]]
+
+    >>> balance_by_socket(test_cpuinfo_nodes_interleaved(2,16,2), 3, [AllocationParams(4, False), AllocationParams(4, True)])
+    [[allocation(0-1,32-33), allocation(2-5)], [allocation(6-7,38-39), allocation(8-11)], [allocation(12-13,44-45), allocation(14-17)]]
+    """
+    cpu_stream = cpu_info.get_cores_by_node_interleaved()
+    def get_cpu():
+        try:
+            return cpu_stream.__next__()
+        except StopIteration:
+            raise Exception("Unable to allocate by socket")
+
+    ret = [[] for i in range(num_proc)]
+    for proc in range(num_proc):
+        for param in params:
+            cpus = set()
+            while (len(cpus) < param.num_cpus):
+                core_id, core_cpus = get_cpu()
+                if param.physical_only:
+                    cpus.add(core_cpus[0])
+                else:
+                    cpus.update(core_cpus)
+            ret[proc].append(Allocation(cpus))
+    return ret
+
+
+def get_balance_algorithm(arg : str):
+    balance_algorithms = {
+        'osd': balance_by_process,
+        'socket': balance_by_socket
+    }
+    if arg not in balance_algorithms :
+        algs = ", ". join(balance_algorithm_dict.keys())
+        raise Exception(
+            f"balance_algorithm must be one of {algs}"
+        )
+    return balance_algorithms[arg]
+
+
+def main(argv):
+    examples = """
+    Examples:
+    # Produce logical core mappings for use with crimson-osd
+    %prog [-u <lscpu.json>] [-t <allowed_cpus>] [-b <osd|socket>] [-v]
+          [-o <num_osd>] [-r <num_reactors_per_osd>]
+          [--physical-only-seastar] [--physical-only-alienstore]
+
+    Note that allocations may exceed requested cpus if logical siblings are
+    included and request is not a multiple of logical cpus per core.
+    """
+
+    # skip argument parsing for tests
+    if '-x' in argv:
+        doctest.testmod()
+        return
+
+    logger = root_logger.getChild("main")
+
+    parser = argparse.ArgumentParser(
+        description="Creates cpu mappings appropriate for use with crimson-osd",
+        epilog=examples,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument(
+        "-o",
+        "--num_osd",
+        type=int,
+        required=True,
+        help="Number of OSDs",
+    )
+    parser.add_argument(
+        "-u",
+        "--lscpu",
+        type=str,
+        required=False,
+        help="Input file: .json file produced by lscpu -e --json",
+        default=None,
+    )
+    parser.add_argument(
+        "-t",
+        "--taskset",
path: src/tools/contrib/assign_crimson_cores.py
user: login: perezjosibm
body: Got it, since the affinity is inherited from vstart, when running taskset as you said it will get the set of valid CPUs implicitly, 👍  
`['taskset', '-acp', str(os.getpid())]`
created_at: 2025-05-07T11:57:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -350,40 +351,37 @@ parse_secondary_devs() {
 
 # Auxiliar function to prepare the CPU cores to pin Seastar reactors
 prep_balance_cpu() {
-    local crimson_smp=$1
-    local balance_strategy=$2
-    local in_file_name="/tmp/numa_args_${balance_strategy}.out"
-    local out_file_name="/tmp/numa_nodes.json"
-    local log_file_name="/tmp/numa_bal_${balance_strategy}.log"
-    local cmd
-
-    # Check the table is empty
-    if [ "${#cpu_table[@]}" -eq 0 ]; then
-        # Ensure the file with the CPU mappings exist
-        if [ ! -f ${in_file_name} ]; then
-            debug echo "lscpu --json > ${out_file_name}"
-            lscpu --json > ${out_file_name}
-            MY_CPUS=$(taskset -acp $$ | awk -F : '{print $2}')
path: src/vstart.sh
user: login: perezjosibm
body: Got it, thanks!
created_at: 2025-05-07T11:59:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,7 +275,9 @@ options:
 	--seastore-secondary-devs-type: device type of all secondary blockdevs. HDD, SSD(default), ZNS or RANDOM_BLOCK_SSD
 	--crimson-smp: number of cores to use for crimson
 	--crimson-alien-num-threads: number of alien-tp threads
-	--crimson-alien-num-cores: number of cores to use for alien-tp
+	--crimson-reactor-physical-only: use only one cpu per physical core for seastar reactors
+	--crimson-alien-num-cores: number of alien-tp threads
path: src/vstart.sh
user: login: Matan-B
body: wrong description, Number of cores that alien-tp threads will use.


created_at: 2025-05-08T08:40:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,7 +275,9 @@ options:
 	--seastore-secondary-devs-type: device type of all secondary blockdevs. HDD, SSD(default), ZNS or RANDOM_BLOCK_SSD
 	--crimson-smp: number of cores to use for crimson
 	--crimson-alien-num-threads: number of alien-tp threads
-	--crimson-alien-num-cores: number of cores to use for alien-tp
+	--crimson-reactor-physical-only: use only one cpu per physical core for seastar reactors
+	--crimson-alien-num-cores: number of alien-tp threads
path: src/vstart.sh
user: login: Matan-B
body: Might be easier to read if this option is under `--crimson-alien-num-threads`:

```
	--crimson-alien-num-threads: number of alien-tp threads
	--crimson-alien-num-cores: number of cores that alien-tp threads will use

created_at: 2025-05-08T08:43:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,7 +275,9 @@ options:
 	--seastore-secondary-devs-type: device type of all secondary blockdevs. HDD, SSD(default), ZNS or RANDOM_BLOCK_SSD
 	--crimson-smp: number of cores to use for crimson
 	--crimson-alien-num-threads: number of alien-tp threads
-	--crimson-alien-num-cores: number of cores to use for alien-tp
+	--crimson-reactor-physical-only: use only one cpu per physical core for seastar reactors
path: src/vstart.sh
user: login: Matan-B
body: I'm afraid "use only one cpu per physical core" is hard to understand without context, what do you think about:
"avoid using logical cores for seastar reactors"

I understand this description is probably less accurate but might be easier to understand(?). I'm ok with also leaving as is.

(Same for crimson-alienstore-physical-only)
created_at: 2025-05-08T08:50:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1197,10 +1203,32 @@ start_cephexporter() {
 
 do_balance_cpu() {
     local osd=$1
+    local alienstore_idx=$(( osd + CEPH_NUM_OSD ))
+
+    local reactor_interval=${cpu_table[${osd}]}
+    if ! [ "${reactor_interval}" == "" ]; then
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_cpu_cores ${reactor_interval}"
+        echo $cmd
+        $cmd
+    else
+        echo "No cpu_table entry for osd $osd, setting crimson_seastar_num_reactors"
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_num_threads $crimson_smp"
+        echo $cmd
+        $cmd
+        return
path: src/vstart.sh
user: login: Matan-B
body: If we are using `do_balance_cpu` it means that crimson_balance_cpu was set.
Letting a single OSD use `crimson_smp` without taking the `reactor_interval` will ruin the purpose of balancing cpus IIUC.
My main concern is, for example, a parsing error will result in few osds using crimson_smp and other using the balanced reactor_interval.
I think that is would be more consistent and less undefined behavior prone to crash here.
Alternatively fallback to an all crimson_smp allocation - but I do lean towards aborting here (as these are vstart clusters)
created_at: 2025-05-08T09:03:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1197,10 +1203,32 @@ start_cephexporter() {
 
 do_balance_cpu() {
     local osd=$1
+    local alienstore_idx=$(( osd + CEPH_NUM_OSD ))
+
+    local reactor_interval=${cpu_table[${osd}]}
+    if ! [ "${reactor_interval}" == "" ]; then
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_cpu_cores ${reactor_interval}"
+        echo $cmd
+        $cmd
+    else
+        echo "No cpu_table entry for osd $osd, setting crimson_seastar_num_reactors"
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_num_threads $crimson_smp"
+        echo $cmd
+        $cmd
+        return
+    fi
+
+
+    local alienstore_interval=${cpu_table[${alienstore_idx}]}
+    if [ ! "${alienstore_interval}" == "" ]; then
path: src/vstart.sh
user: login: Matan-B
body:  `if [ "$objectstore" == "bluestore" ]; then`
Do we want to include this bit only for bluestore vstart clusters? same as we do later on for crimson_alien_num_threads later on


created_at: 2025-05-08T09:05:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1197,10 +1203,32 @@ start_cephexporter() {
 
 do_balance_cpu() {
     local osd=$1
+    local alienstore_idx=$(( osd + CEPH_NUM_OSD ))
+
+    local reactor_interval=${cpu_table[${osd}]}
+    if ! [ "${reactor_interval}" == "" ]; then
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_cpu_cores ${reactor_interval}"
+        echo $cmd
+        $cmd
+    else
+        echo "No cpu_table entry for osd $osd, setting crimson_seastar_num_reactors"
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_num_threads $crimson_smp"
+        echo $cmd
+        $cmd
+        return
+    fi
+
+
+    local alienstore_interval=${cpu_table[${alienstore_idx}]}
+    if [ ! "${alienstore_interval}" == "" ]; then
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_alien_thread_cpu_cores ${alienstore_interval}"
+        echo $cmd
+        $cmd
+    else
+        echo "No alienstore cpu_table entry for osd $osd"
+        return
+    fi
path: src/vstart.sh
user: login: Matan-B
body: Same here as with my previous comment above, I think that we must by consistent across all osds. If one had  `alienstore_interval` all the others should too. 
created_at: 2025-05-08T09:09:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -350,40 +352,38 @@ parse_secondary_devs() {
 
 # Auxiliar function to prepare the CPU cores to pin Seastar reactors
 prep_balance_cpu() {
-    local crimson_smp=$1
-    local balance_strategy=$2
-    local in_file_name="/tmp/numa_args_${balance_strategy}.out"
-    local out_file_name="/tmp/numa_nodes.json"
-    local log_file_name="/tmp/numa_bal_${balance_strategy}.log"
-    local cmd
-
-    # Check the table is empty
-    if [ "${#cpu_table[@]}" -eq 0 ]; then
-        # Ensure the file with the CPU mappings exist
-        if [ ! -f ${in_file_name} ]; then
-            debug echo "lscpu --json > ${out_file_name}"
-            lscpu --json > ${out_file_name}
-            MY_CPUS=$(taskset -acp $$ | awk -F : '{print $2}')
-            cmd="python3 ${CEPH_DIR}/../src/tools/contrib/balance_cpu.py -o ${CEPH_NUM_OSD}\
-                -r ${crimson_smp}  -b ${balance_strategy} -u ${out_file_name} -t ${MY_CPUS} > ${in_file_name}"
-            debug echo "$cmd"
-            eval "$cmd" >> ${log_file_name}
-        fi
+    if [ -z $crimson_balance_cpu ] || [ "${crimson_balance_cpu}" == "none" ] ; then
path: src/vstart.sh
user: login: Matan-B
body: nit: I think we should fail here on any entry which is not `osd` or `socket` (instead of only on "none").

"none" and "0" and mistakes like "osdsockettt" should all lead to not assigning cpus.

Otherwise we would try to assign cpus with faulty `-b` args.
created_at: 2025-05-08T09:15:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1215,9 +1243,9 @@ start_osd() {
     fi
     local osds_wait
     # If the type of OSD is Crimson and the option to balance the Seastar reactors is true
-	if [ "$ceph_osd" == "crimson-osd" ] && [ ! -z "$crimson_balance_cpu" ]; then
+	if [ "$ceph_osd" == "crimson-osd" ]; then
         debug echo "Preparing balance CPU for Crimson"
-        prep_balance_cpu $crimson_smp $crimson_balance_cpu
+        prep_balance_cpu
path: src/vstart.sh
user: login: Matan-B
body: We should move the `-z $crimson_balance_cpu ]` check (from within `prep_balance_cpu`) into here.
Otherwise this should be named `maybe_prep_balance_cpu`
created_at: 2025-05-08T09:17:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1215,9 +1243,9 @@ start_osd() {
     fi
     local osds_wait
     # If the type of OSD is Crimson and the option to balance the Seastar reactors is true
-	if [ "$ceph_osd" == "crimson-osd" ] && [ ! -z "$crimson_balance_cpu" ]; then
+	if [ "$ceph_osd" == "crimson-osd" ]; then
         debug echo "Preparing balance CPU for Crimson"
-        prep_balance_cpu $crimson_smp $crimson_balance_cpu
+        prep_balance_cpu
path: src/vstart.sh
user: login: Matan-B
body: ~Moreover, if balancing is **not**  set (disabled) - I think that we should fallback to `crimson_seastar_num_threads` (different from `crimson_alien_op_num_threads`).~

~Meaning, if no specific balancing strategy is used, which is expected for dev work/ simple debug clusters - 
Let's use `crimson_seastar_num_threads` to use the non-pinning strategy.~

~This is actually _required_ by our bootstrap, if `crimson_seastar_cpu_cores` is not used - `crimson_seastar_num_threads` must be set - otherwise the osd won't come up.~

edit: crimson_seastar_num_threads is used as a fallback
created_at: 2025-05-08T09:25:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,7 +275,9 @@ options:
 	--seastore-secondary-devs-type: device type of all secondary blockdevs. HDD, SSD(default), ZNS or RANDOM_BLOCK_SSD
 	--crimson-smp: number of cores to use for crimson
 	--crimson-alien-num-threads: number of alien-tp threads
-	--crimson-alien-num-cores: number of cores to use for alien-tp
+	--crimson-reactor-physical-only: use only one cpu per physical core for seastar reactors
path: src/vstart.sh
user: login: Matan-B
body: discussed offline
created_at: 2025-05-08T15:33:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1215,9 +1243,9 @@ start_osd() {
     fi
     local osds_wait
     # If the type of OSD is Crimson and the option to balance the Seastar reactors is true
-	if [ "$ceph_osd" == "crimson-osd" ] && [ ! -z "$crimson_balance_cpu" ]; then
+	if [ "$ceph_osd" == "crimson-osd" ]; then
         debug echo "Preparing balance CPU for Crimson"
-        prep_balance_cpu $crimson_smp $crimson_balance_cpu
+        prep_balance_cpu
path: src/vstart.sh
user: login: Matan-B
body: discussed offline
created_at: 2025-05-08T15:33:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1197,10 +1203,32 @@ start_cephexporter() {
 
 do_balance_cpu() {
     local osd=$1
+    local alienstore_idx=$(( osd + CEPH_NUM_OSD ))
+
+    local reactor_interval=${cpu_table[${osd}]}
+    if ! [ "${reactor_interval}" == "" ]; then
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_cpu_cores ${reactor_interval}"
+        echo $cmd
+        $cmd
+    else
+        echo "No cpu_table entry for osd $osd, setting crimson_seastar_num_reactors"
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_num_threads $crimson_smp"
+        echo $cmd
+        $cmd
+        return
path: src/vstart.sh
user: login: Matan-B
body: The fallback option (crimson_seastar_num_threads) is used and we, for now, assume the script is woirking well for all osds.

discussed offline

created_at: 2025-05-08T15:34:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1197,10 +1203,32 @@ start_cephexporter() {
 
 do_balance_cpu() {
     local osd=$1
+    local alienstore_idx=$(( osd + CEPH_NUM_OSD ))
+
+    local reactor_interval=${cpu_table[${osd}]}
+    if ! [ "${reactor_interval}" == "" ]; then
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_cpu_cores ${reactor_interval}"
+        echo $cmd
+        $cmd
+    else
+        echo "No cpu_table entry for osd $osd, setting crimson_seastar_num_reactors"
+        local cmd="$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_num_threads $crimson_smp"
+        echo $cmd
+        $cmd
+        return
+    fi
+
+
+    local alienstore_interval=${cpu_table[${alienstore_idx}]}
+    if [ ! "${alienstore_interval}" == "" ]; then
path: src/vstart.sh
user: login: Matan-B
body: discussed offline - setting alienstore_interval for no bluestore clusters will be non-harmful

created_at: 2025-05-08T15:35:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,7 +275,9 @@ options:
 	--seastore-secondary-devs-type: device type of all secondary blockdevs. HDD, SSD(default), ZNS or RANDOM_BLOCK_SSD
 	--crimson-smp: number of cores to use for crimson
 	--crimson-alien-num-threads: number of alien-tp threads
-	--crimson-alien-num-cores: number of cores to use for alien-tp
+	--crimson-reactor-physical-only: use only one cpu per physical core for seastar reactors
+	--crimson-alien-num-cores: number of alien-tp threads
path: src/vstart.sh
user: login: athanatos
body:  Good catch, fixed.
created_at: 2025-05-08T16:06:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -350,40 +352,38 @@ parse_secondary_devs() {
 
 # Auxiliar function to prepare the CPU cores to pin Seastar reactors
 prep_balance_cpu() {
-    local crimson_smp=$1
-    local balance_strategy=$2
-    local in_file_name="/tmp/numa_args_${balance_strategy}.out"
-    local out_file_name="/tmp/numa_nodes.json"
-    local log_file_name="/tmp/numa_bal_${balance_strategy}.log"
-    local cmd
-
-    # Check the table is empty
-    if [ "${#cpu_table[@]}" -eq 0 ]; then
-        # Ensure the file with the CPU mappings exist
-        if [ ! -f ${in_file_name} ]; then
-            debug echo "lscpu --json > ${out_file_name}"
-            lscpu --json > ${out_file_name}
-            MY_CPUS=$(taskset -acp $$ | awk -F : '{print $2}')
-            cmd="python3 ${CEPH_DIR}/../src/tools/contrib/balance_cpu.py -o ${CEPH_NUM_OSD}\
-                -r ${crimson_smp}  -b ${balance_strategy} -u ${out_file_name} -t ${MY_CPUS} > ${in_file_name}"
-            debug echo "$cmd"
-            eval "$cmd" >> ${log_file_name}
-        fi
+    if [ -z $crimson_balance_cpu ] || [ "${crimson_balance_cpu}" == "none" ] ; then
path: src/vstart.sh
user: login: athanatos
body: It'll cause the underlying script to fail and the following check for an empty array will fail as well.  Doing it this way means that adding new strategies won't require changes to vstart, just the python script.
created_at: 2025-05-08T16:08:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: athanatos
created_at: 2025-05-06T18:44:11Z
body: jenkins test make check
author_association: CONTRIBUTOR
user: login: athanatos
created_at: 2025-05-06T18:44:17Z
body: jenkins test make check arm64
author_association: CONTRIBUTOR
user: login: athanatos
created_at: 2025-05-07T20:21:09Z
body: @Matan-B Does this seem reasonable?
author_association: CONTRIBUTOR
user: login: Matan-B
created_at: 2025-05-08T09:40:12Z
body: Even though there's no src change here, we should backport this into T so we can test the script with actual deployments as well in the next tech-preview release. 
author_association: CONTRIBUTOR
user: login: athanatos
created_at: 2025-05-08T17:55:05Z
body: jenkins test make check
author_association: CONTRIBUTOR
user: login: athanatos
created_at: 2025-05-08T21:51:09Z
body: I apparently hadn't actually removed the sequential pinning default from the reactors, only from alienstore.  It should actually default to simply setting crimson_seastar_cpu_cores now:

```
diff --git a/src/vstart.sh b/src/vstart.sh
index 1970b9725b0..ea3577bf4af 100755
--- a/src/vstart.sh
+++ b/src/vstart.sh
@@ -1250,15 +1250,7 @@ start_osd() {
     for osd in `seq $start $end`
     do
        if [ "$ceph_osd" == "crimson-osd" ]; then
-        if [ ! -z "$crimson_balance_cpu" ]; then
-            do_balance_cpu $osd
-        else
-            bottom_cpu=$(( osd * crimson_smp ))
-            top_cpu=$(( bottom_cpu + crimson_smp - 1 ))
-            # set exclusive CPU nodes for each osd
-            echo "$CEPH_BIN/ceph -c $conf_fn config set osd.$osd crimson_seastar_cpu_cores $bottom_cpu-$top_cpu"
-            $CEPH_BIN/ceph -c $conf_fn config set "osd.$osd" crimson_seastar_cpu_cores "$bottom_cpu-$top_cpu"
-        fi
+        do_balance_cpu $osd
     fi
        if [ "$new" -eq 1 -o $inc_osd_num -gt 0 ]; then
             wconf <<EOF
```
author_association: CONTRIBUTOR
user: login: athanatos
created_at: 2025-05-09T00:11:44Z
body: jenkins test api
author_association: CONTRIBUTOR
user: login: athanatos
created_at: 2025-05-09T01:15:33Z
body: Updated the fixes line to reflect bug https://tracker.ceph.com/issues/71096 rather than https://tracker.ceph.com/issues/66159 -- see comment on https://tracker.ceph.com/issues/66159 for rationale.
author_association: CONTRIBUTOR
user: login: Matan-B
created_at: 2025-05-11T07:39:49Z
body: jenkins test make check
author_association: CONTRIBUTOR
user: login: Matan-B
created_at: 2025-05-11T07:47:17Z
body: jenkins test make check arm64
author_association: CONTRIBUTOR
user: login: athanatos
created_at: 2025-05-12T17:08:17Z
body: 51/51 passed https://pulpito-ng.ceph.com/runs/sjust-2025-05-09_20:34:47-crimson-rados-wip-sjust-crimson-testing-2025-05-09-1746810464-distro-default-gibba
author_association: CONTRIBUTOR