CPU pool plan: hybrid CPUs with more E-cores than P-cores default to the P-cores but the host's plus half of the E-cores (Core Ultra 7 265: 12-16 workers +12.6% decode over 19; 24 workers -44%)

This commit is contained in:
Niko1221 committed 2026-10-18 13:18:38 +02:00
1 parent 036c8f6ec3
commit bec4c838ab
5 files changed
+43 -3

No files matched your search

+13
View File
@@ -37,6 +37,12 @@ struct PoolPlanInput {
/// thread's own core is counted in it. One entry on a single-socket CPU. Empty: the topology is not known.
std::vector<int> cores_per_package;
double cpu_quota = 0; ///< CPUs a container quota allows (cgroup cpu.max); 0: no quota
/// Physical performance / efficiency cores of a hybrid CPU (both 0 on an ordinary one). With more E-cores than
/// P-cores the default is the P-cores but the host's plus half of the E-cores (#642: i9-14900KF 8P+16E, 15 workers
/// 165 / 116 tok/s against 106 / 84 with all 23; Core Ultra 7 265, 8P+12E, RTX 3060 decode at 4K: 12-16 workers
/// +12.6% over the 19 of one-per-core, 24 workers -44%: notes-152-users.md).
int p_cores = 0;
int e_cores = 0;
};
struct PoolPlan {
@@ -72,6 +78,13 @@ inline PoolPlan plan_pool_workers(const PoolPlanInput& in) {
n -= kPoolReserve;
if (p.topology_known && p.packages == 1) p.limit = "physical cores, one kept free";
}
if (in.p_cores > 0 && in.e_cores > in.p_cores) { // a hybrid CPU with more E-cores than P-cores
const int h = std::max(1, in.p_cores - 1 + in.e_cores / 2);
if (h < n) {
n = h;
p.limit = "the performance cores and half of the efficiency cores";
}
}
if (in.cpu_quota > 0.0) {
const int q = std::max(1, (int) in.cpu_quota - 1);
if (q < n) {
+5 -2
View File
@@ -1004,7 +1004,7 @@ POOL_RESERVE = 1
VISION_THREADS_CAP = 16
def plan_pool_workers(logical: int, cores_per_package=None, quota: float = 0.0) -> int:
def plan_pool_workers(logical: int, cores_per_package=None, quota: float = 0.0, hybrid=None) -> int:
"""The engine's default pool worker count for a CPU of `logical` threads whose physical cores per socket are
`cores_per_package` (the host thread's socket first; None/empty: the layout is not known) and a container CPU
quota (0: none)."""
@@ -1016,6 +1016,8 @@ def plan_pool_workers(logical: int, cores_per_package=None, quota: float = 0.0)
n = max(1, local - 1) # the host thread has the first core
if n >= POOL_RESERVE_FROM:
n -= POOL_RESERVE
if hybrid and hybrid[0] > 0 and hybrid[1] > hybrid[0]: # (P-cores, E-cores): the P-cores but the host's + half the E-cores
n = min(n, max(1, hybrid[0] - 1 + hybrid[1] // 2))
if quota > 0:
n = min(n, max(1, int(quota) - 1))
return max(1, min(n, POOL_WORKERS_CAP))
@@ -1037,8 +1039,9 @@ def pool_workers_note(args, layout=None) -> list[str]:
asked = int(args[args.index("--pool-workers") + 1])
except (IndexError, ValueError):
return []
hybrid = cpu_cores() if layout is None else None
logical, cores = layout or cpu_core_layout()
rec = plan_pool_workers(logical, cores)
rec = plan_pool_workers(logical, cores, hybrid=hybrid)
if asked <= rec:
return []
why = ("more than this PC has CPU threads" if asked + 1 > logical else
+1
View File
@@ -187,6 +187,7 @@ PoolPlanInput pool_plan_input(const CpuTopology& topo) {
PoolPlanInput in;
in.logical = topo.logical > 0 ? topo.logical : (int) std::thread::hardware_concurrency();
in.cpu_quota = topo.cpu_quota;
if (topo.is_hybrid) { in.p_cores = topo.p_cores; in.e_cores = topo.e_cores; }
if (!topo.layout_known) return in; // cores_per_package stays empty
// the cores per package, the host package first; the host core counts in its package
std::vector<std::pair<int, int>> counts; // (package, cores)
+18 -1
View File
@@ -83,6 +83,23 @@ int main() {
expect(cpu::plan_pool_workers(unk).workers < 255, "unknown 256 -> never 255");
expect(cpu::plan_pool_workers(cpu_of(8, {})).workers == 7, "unknown, 8 logical -> 7 workers");
// hybrid CPUs: with more E-cores than P-cores, the P-cores but the host's plus half of the E-cores
{
auto h = cpu_of(20, {20});
h.p_cores = 8;
h.e_cores = 12;
line("hybrid 8P+12E (Core Ultra 7 265)", h);
expect(cpu::plan_pool_workers(h).workers == 13, "8P+12E -> 13 workers (measured: 12-16 beat the 19 of one per core)");
auto h2 = cpu_of(32, {24});
h2.p_cores = 8;
h2.e_cores = 16;
expect(cpu::plan_pool_workers(h2).workers == 15, "8P+16E -> 15 workers (#642)");
auto h3 = cpu_of(24, {16});
h3.p_cores = 8;
h3.e_cores = 8;
expect(cpu::plan_pool_workers(h3).workers == 15, "8P+8E: not more E-cores than P-cores, every core but the host's");
}
// a container quota
const auto q = cpu_of(128, {64}, 4.0);
line("64c, container quota 4 CPUs", q);
@@ -106,7 +123,7 @@ int main() {
"300 workers on 256 threads: more than the CPU threads");
expect(cpu::pool_workers_warning(30, xeon, cpu::plan_pool_workers(xeon)).find("socket") != std::string::npos,
"30 workers on 2x20c: crosses the socket");
expect(!cpu::pool_workers_warning(30, c64, cpu::plan_pool_workers(c64)).empty(), "30 workers on 64c is above the cap: warns");
expect(!cpu::pool_workers_warning(40, c64, cpu::plan_pool_workers(c64)).empty(), "40 workers on 64c is above the cap: warns");
expect(cpu::pool_workers_warning(8, q, cpu::plan_pool_workers(q)).find("container") != std::string::npos,
"8 workers under a 4-CPU quota: names the container limit");
+6
View File
@@ -50,6 +50,12 @@ class DefaultWorkers(unittest.TestCase):
self.assertEqual(setup.plan_pool_workers(48, [6, 18]), 5)
self.assertEqual(setup.plan_pool_workers(1, [1]), 1)
def test_hybrid(self):
self.assertEqual(setup.plan_pool_workers(20, [20], hybrid=(8, 12)), 13) # Core Ultra 7 265 (measured)
self.assertEqual(setup.plan_pool_workers(32, [24], hybrid=(8, 16)), 15) # i9-14900KF (#642)
self.assertEqual(setup.plan_pool_workers(24, [16], hybrid=(8, 8)), 15) # not more E-cores than P-cores
self.assertEqual(setup.hybrid_pool_workers((8, 12)), 13) # setup's own rule agrees
def test_unknown_layout_and_quota(self):
self.assertLessEqual(setup.plan_pool_workers(256, []), setup.POOL_WORKERS_CAP)
self.assertLess(setup.plan_pool_workers(256, None), 255)