Merge pull request #943 from LibreQoE/928_ecores

ISSUE #928: Exclude efficiency cores from CPU mapping
This commit is contained in:
Robert Chacón authored and GitHub committed 2026-02-26 15:50:56 -05:00
commit 8f4d55cd36
12 files changed
+417 -20

No files matched your search

+5 -1
View File
@@ -35,6 +35,7 @@ from liblqos_python import is_lqosd_alive, clear_ip_mappings, delete_ip_mapping,
on_a_stick, get_tree_weights, get_weights, is_network_flat, get_libreqos_directory, enable_insight_topology, \
is_insight_enabled, \
fast_queues_fq_codel, \
shaping_cpu_count, \
Bakery
# Optional: urgent issue submission (available in newer liblqos_python)
@@ -189,7 +190,10 @@ def findQueuesAvailable(interfaceName):
else:
queuesAvailable = queues_available_override()
print(f"Interface {interfaceName} NIC queues (Override):\t\t\t" + str(queuesAvailable))
cpuCount = multiprocessing.cpu_count()
try:
cpuCount = shaping_cpu_count()
except Exception:
cpuCount = multiprocessing.cpu_count()
print("CPU cores:\t\t\t" + str(cpuCount))
if queuesAvailable < 2:
raise SystemError(f'Only 1 NIC rx/tx queue available for interface {interfaceName}. You will need to use a NIC with 2 or more rx/tx queues available.')
+248
View File
@@ -0,0 +1,248 @@
//! CPU topology helpers (hybrid P-core/E-core detection).
//!
//! LibreQoS primarily runs on Linux and may be deployed on hybrid CPUs that
//! include both performance cores ("P-cores") and efficiency cores ("E-cores").
//! When possible, we detect E-cores via sysfs and provide a list of CPUs that
//! should be used for shaping / CPU binning.
use crate::Config;
use std::collections::HashSet;
use std::path::Path;
use thiserror::Error;
const POSSIBLE_CPUS_PATH: &str = "/sys/devices/system/cpu/possible";
const CPU_CORE_SYSFS_PATHS: [&str; 2] = [
"/sys/bus/event_source/devices/cpu_core/cpus",
"/sys/devices/cpu_core/cpus",
];
const CPU_ATOM_SYSFS_PATHS: [&str; 2] = [
"/sys/bus/event_source/devices/cpu_atom/cpus",
"/sys/devices/cpu_atom/cpus",
];
/// Indicates which mechanism was used to choose shaping CPUs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShapingCpuSource {
/// Exclusion is disabled; use all possible CPUs.
ExclusionDisabled,
/// Used `/sys/.../cpu_core/cpus` to identify P-cores.
CpuCoreSysfs,
/// Used `/sys/.../cpu_atom/cpus` to identify E-cores, then computed `possible - atom`.
PossibleMinusAtomSysfs,
/// Hybrid sysfs paths were unavailable; used `/sys/devices/system/cpu/possible`.
FallbackAllPossible,
/// Could not read `/sys/devices/system/cpu/possible`; used Rust's available parallelism.
FallbackAvailableParallelism,
}
/// Result of hybrid CPU detection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShapingCpuDetection {
/// Whether efficiency core exclusion was requested.
pub exclude_efficiency_cores: bool,
/// Source used to determine the shaping CPU list.
pub source: ShapingCpuSource,
/// All possible CPU IDs (best effort).
pub possible: Vec<u32>,
/// Detected performance-core CPU IDs (may be empty if unknown).
pub performance: Vec<u32>,
/// Detected efficiency-core CPU IDs (may be empty if unknown).
pub efficiency: Vec<u32>,
/// Final CPU list that should be used for shaping/bins.
pub shaping: Vec<u32>,
}
/// Errors that can occur when parsing a Linux cpulist string.
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum CpuListParseError {
/// Input was empty or contained no CPU entries.
#[error("CPU list is empty")]
Empty,
/// A numeric component failed to parse.
#[error("Unable to parse number in CPU list")]
ParseNumber,
/// A range was malformed (e.g. `3-1`).
#[error("Invalid range in CPU list")]
InvalidRange,
}
fn parse_cpu_list(input: &str) -> Result<Vec<u32>, CpuListParseError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(CpuListParseError::Empty);
}
let mut out = Vec::new();
for part in trimmed.split(',') {
let p = part.trim();
if p.is_empty() {
continue;
}
if let Some((start_s, end_s)) = p.split_once('-') {
let start = start_s.trim().parse::<u32>().map_err(|_| CpuListParseError::ParseNumber)?;
let end = end_s.trim().parse::<u32>().map_err(|_| CpuListParseError::ParseNumber)?;
if end < start {
return Err(CpuListParseError::InvalidRange);
}
for cpu in start..=end {
out.push(cpu);
}
} else {
let cpu = p.parse::<u32>().map_err(|_| CpuListParseError::ParseNumber)?;
out.push(cpu);
}
}
if out.is_empty() {
return Err(CpuListParseError::Empty);
}
out.sort_unstable();
out.dedup();
Ok(out)
}
fn try_read_cpu_list(path: &str) -> Option<Vec<u32>> {
if !Path::new(path).exists() {
return None;
}
let raw = std::fs::read_to_string(path).ok()?;
let list = parse_cpu_list(&raw).ok()?;
(!list.is_empty()).then_some(list)
}
fn first_cpu_list(paths: &[&str]) -> Option<Vec<u32>> {
for p in paths.iter().copied() {
if let Some(list) = try_read_cpu_list(p) {
return Some(list);
}
}
None
}
fn possible_cpu_list() -> (Vec<u32>, ShapingCpuSource) {
if let Some(list) = try_read_cpu_list(POSSIBLE_CPUS_PATH) {
return (list, ShapingCpuSource::FallbackAllPossible);
}
let fallback = std::thread::available_parallelism()
.map(|n| (0..(n.get() as u32)).collect::<Vec<u32>>())
.unwrap_or_else(|_| vec![0]);
(fallback, ShapingCpuSource::FallbackAvailableParallelism)
}
/// Detects the set of CPUs that should be used for shaping/CPU binning.
///
/// When `cfg.exclude_efficiency_cores` is enabled, this attempts to identify
/// performance cores via sysfs. If detection fails or yields an empty set,
/// this falls back to "all possible CPUs".
pub fn detect_shaping_cpus(cfg: &Config) -> ShapingCpuDetection {
let exclude = cfg.exclude_efficiency_cores;
let (possible, possible_source) = possible_cpu_list();
if !exclude {
return ShapingCpuDetection {
exclude_efficiency_cores: exclude,
source: ShapingCpuSource::ExclusionDisabled,
possible: possible.clone(),
performance: possible.clone(),
efficiency: Vec::new(),
shaping: possible,
};
}
let perf_opt = first_cpu_list(&CPU_CORE_SYSFS_PATHS);
let eff_opt = first_cpu_list(&CPU_ATOM_SYSFS_PATHS);
// Prefer cpu_core list if present.
if let Some(mut perf) = perf_opt.clone() {
// If both are present, remove overlaps just in case.
if let Some(eff) = eff_opt.clone() {
if !eff.is_empty() && !perf.is_empty() {
let eff_set: HashSet<u32> = eff.iter().copied().collect();
perf.retain(|c| !eff_set.contains(c));
}
}
// Ensure it is a subset of possible CPUs, if that list exists.
if !possible.is_empty() {
let poss_set: HashSet<u32> = possible.iter().copied().collect();
perf.retain(|c| poss_set.contains(c));
}
if !perf.is_empty() {
return ShapingCpuDetection {
exclude_efficiency_cores: exclude,
source: ShapingCpuSource::CpuCoreSysfs,
possible,
performance: perf.clone(),
efficiency: eff_opt.unwrap_or_default(),
shaping: perf,
};
}
}
// Next-best: if we know efficiency CPUs, compute possible - efficiency.
if let Some(eff) = eff_opt.clone() {
if !eff.is_empty() && !possible.is_empty() {
let eff_set: HashSet<u32> = eff.iter().copied().collect();
let perf: Vec<u32> = possible.iter().copied().filter(|c| !eff_set.contains(c)).collect();
if !perf.is_empty() {
return ShapingCpuDetection {
exclude_efficiency_cores: exclude,
source: ShapingCpuSource::PossibleMinusAtomSysfs,
possible,
performance: perf.clone(),
efficiency: eff,
shaping: perf,
};
}
}
}
// Final fallback: use all possible CPUs.
ShapingCpuDetection {
exclude_efficiency_cores: exclude,
source: possible_source,
possible: possible.clone(),
performance: possible.clone(),
efficiency: eff_opt.unwrap_or_default(),
shaping: possible,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_cpu_list_single() {
assert_eq!(parse_cpu_list("0").unwrap(), vec![0]);
}
#[test]
fn parse_cpu_list_range() {
assert_eq!(parse_cpu_list("0-3").unwrap(), vec![0, 1, 2, 3]);
}
#[test]
fn parse_cpu_list_mixed() {
assert_eq!(parse_cpu_list("0-3,8,10-12").unwrap(), vec![0, 1, 2, 3, 8, 10, 11, 12]);
}
#[test]
fn parse_cpu_list_whitespace_and_dedup() {
assert_eq!(parse_cpu_list(" 0-1, 1 , 2 \n").unwrap(), vec![0, 1, 2]);
}
#[test]
fn parse_cpu_list_invalid_range() {
assert_eq!(parse_cpu_list("3-1").unwrap_err(), CpuListParseError::InvalidRange);
}
#[test]
fn parse_cpu_list_empty() {
assert_eq!(parse_cpu_list(" \n").unwrap_err(), CpuListParseError::Empty);
}
}
@@ -108,6 +108,13 @@ pub struct Config {
/// Disable ICMP Ping Monitoring for Devices in the hosts view
pub disable_icmp_ping: Option<bool>,
/// Exclude efficiency cores (E-cores) from CPU assignment / shaping where possible.
///
/// On hybrid CPUs, this attempts to detect performance cores (P-cores) via sysfs and
/// restricts LibreQoS CPU binning and XDP redirection to those CPUs.
#[serde(default = "default_true")]
pub exclude_efficiency_cores: bool,
/// Enable per-circuit TemporalHeatmap collection.
#[serde(default = "default_true")]
pub enable_circuit_heatmaps: bool,
@@ -241,6 +248,7 @@ impl Default for Config {
webserver_listen: None,
stormguard: None,
disable_icmp_ping: Some(false),
exclude_efficiency_cores: true,
enable_circuit_heatmaps: true,
enable_site_heatmaps: true,
enable_asn_heatmaps: true,
+2
View File
@@ -8,6 +8,7 @@
#![deny(clippy::unwrap_used)]
#![warn(missing_docs)]
pub mod authentication;
mod cpu_topology;
mod etc;
mod network_json;
mod program_control;
@@ -15,6 +16,7 @@ mod qoo_profiles;
mod shaped_devices;
pub use authentication::{UserRole, WebUser, WebUsers};
pub use cpu_topology::{CpuListParseError, ShapingCpuDetection, ShapingCpuSource, detect_shaping_cpus};
pub use etc::{
BridgeConfig, Config, LazyQueueMode, SingleInterfaceConfig, StormguardConfig, Tunables,
disable_xdp_bridge, enable_long_term_stats, load_config, update_config,
+11
View File
@@ -598,6 +598,7 @@ fn liblqos_python(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(enable_actual_shell_commands, m)?)?;
m.add_function(wrap_pyfunction!(use_bin_packing_to_balance_cpu, m)?)?;
m.add_function(wrap_pyfunction!(monitor_mode_only, m)?)?;
m.add_function(wrap_pyfunction!(shaping_cpu_count, m)?)?;
m.add_function(wrap_pyfunction!(run_shell_commands_as_sudo, m)?)?;
m.add_function(wrap_pyfunction!(generated_pn_download_mbps, m)?)?;
m.add_function(wrap_pyfunction!(generated_pn_upload_mbps, m)?)?;
@@ -1149,6 +1150,16 @@ fn monitor_mode_only() -> PyResult<bool> {
Ok(config.queues.monitor_only)
}
/// Returns the number of CPUs that should be used for shaping / binning.
///
/// On hybrid CPUs, this may exclude efficiency cores (E-cores) when configured.
#[pyfunction]
fn shaping_cpu_count() -> PyResult<u32> {
let config = lqos_config::load_config().unwrap();
let det = lqos_config::detect_shaping_cpus(config.as_ref());
Ok(det.shaping.len() as u32)
}
#[pyfunction]
fn run_shell_commands_as_sudo() -> PyResult<bool> {
let config = lqos_config::load_config().unwrap();
+11
View File
@@ -288,6 +288,12 @@ int tc_iphash_to_cpu(struct __sk_buff *skb)
// We can short-circuit the redirect and bypass the second
// LPM lookup! Yay!
skb->priority = meta->tc_handle;
// Ensure the selected MQ queue matches the TC handle's major.
// This decouples shaping correctness from CPU selection.
__u16 major = meta->tc_handle >> 16;
if (major != 0) {
skb->queue_mapping = major;
}
#ifdef TRACING
{
__u64 now = bpf_ktime_get_ns();
@@ -336,6 +342,11 @@ int tc_iphash_to_cpu(struct __sk_buff *skb)
bpf_debug("(TC) Mapped to TC handle %x", ip_info.tc_handle);
#endif
skb->priority = ip_info.tc_handle;
// Ensure the selected MQ queue matches the TC handle's major.
__u16 major = ip_info.tc_handle >> 16;
if (major != 0) {
skb->queue_mapping = major;
}
#ifdef TRACING
{
__u64 now = bpf_ktime_get_ns();
+41 -15
View File
@@ -1,12 +1,12 @@
use crate::{linux::map_txq_config_base_setup, num_possible_cpus};
use crate::{linux::map_txq_config_shaping, num_possible_cpus};
use anyhow::{Error, Result};
use libbpf_sys::{bpf_map_update_elem, bpf_obj_get};
use std::{ffi::CString, os::raw::c_void};
use tracing::debug;
//* Provides an interface for querying the number of CPUs eBPF can
//* see, and marking CPUs as available. Currently marks ALL eBPF
//* usable CPUs as available.
//* see, and marking CPUs as available. Callers may provide a shaping
//* CPU allowlist (e.g. exclude E-cores on hybrid CPUs).
pub(crate) struct CpuMapping {
fd_cpu_map: i32,
@@ -33,15 +33,32 @@ impl CpuMapping {
})
}
pub(crate) fn mark_cpus_available(&self) -> Result<()> {
pub(crate) fn mark_cpus_available(&self, shaping_physical_cpus: &[u32]) -> Result<()> {
let cpu_count = num_possible_cpus()?;
let queue_size = 2048u32;
let val_ptr: *const u32 = &queue_size;
for cpu in 0..cpu_count {
debug!("Mapping core #{cpu}");
// Insert into the cpu map
let cpu_ptr: *const u32 = &cpu;
// Determine which physical CPUs we will redirect to.
let mut shaping: Vec<u32> = if shaping_physical_cpus.is_empty() {
(0..cpu_count).collect()
} else {
shaping_physical_cpus
.iter()
.copied()
.filter(|c| *c < cpu_count)
.collect()
};
shaping.sort_unstable();
shaping.dedup();
if shaping.is_empty() {
shaping = (0..cpu_count).collect();
}
// Populate CPU map entries for each destination CPU (physical CPU IDs).
for cpu_dest in shaping.iter().copied() {
debug!("Mapping destination CPU #{cpu_dest} into cpumap");
let cpu_ptr: *const u32 = &cpu_dest;
let error = unsafe {
bpf_map_update_elem(
self.fd_cpu_map,
@@ -51,27 +68,36 @@ impl CpuMapping {
)
};
if error != 0 {
return Err(Error::msg("Unable to map CPU"));
return Err(Error::msg("Unable to map CPU in cpumap"));
}
}
// Insert into the available list
// Populate logical->physical mapping table for ALL possible CPU indices.
// This prevents stale mappings from implicitly redirecting to CPU0 due to
// uninitialized array values.
let shaping_len = shaping.len() as u32;
for logical_cpu in 0..cpu_count {
let cpu_dest = shaping[(logical_cpu % shaping_len) as usize];
debug!("Mapping logical CPU #{logical_cpu} -> physical CPU #{cpu_dest}");
let key_ptr: *const u32 = &logical_cpu;
let val_ptr: *const u32 = &cpu_dest;
let error = unsafe {
bpf_map_update_elem(
self.fd_cpu_available,
cpu_ptr as *const c_void,
cpu_ptr as *const c_void,
key_ptr as *const c_void,
val_ptr as *const c_void,
0,
)
};
if error != 0 {
return Err(Error::msg("Unable to add to available CPUs list"));
}
} // CPU loop
}
Ok(())
}
pub(crate) fn setup_base_txq_config(&self) -> Result<()> {
Ok(map_txq_config_base_setup(self.fd_txq_config)?)
pub(crate) fn setup_base_txq_config(&self, shaping_physical_cpus: &[u32]) -> Result<()> {
Ok(map_txq_config_shaping(self.fd_txq_config, shaping_physical_cpus)?)
}
}
@@ -40,6 +40,65 @@ pub fn map_txq_config_base_setup(map_fd: i32) -> Result<(), MapTxqConfigError> {
Ok(())
}
/// Populate `map_txq_config` for a shaping CPU set.
///
/// The map is keyed by the **physical CPU** that will execute the TC program
/// (i.e. the destination of XDP redirection). Values set `queue_mapping` and
/// `htb_major` to match the logical queue index used by LibreQoS (major IDs
/// start at 1).
pub fn map_txq_config_shaping(
map_fd: i32,
shaping_physical_cpus: &[u32],
) -> Result<(), MapTxqConfigError> {
let possible_cpus = num_possible_cpus().map_err(|_| MapTxqConfigError::NumCpusError)?;
if map_fd < 0 {
error!("ERR: (bad map_fd:{map_fd}) cannot proceed without access to txq_config map");
return Err(MapTxqConfigError::BadMapFd);
}
if shaping_physical_cpus.is_empty() {
return map_txq_config_base_setup(map_fd);
}
// Clear prior values to avoid stale queue mappings on excluded CPUs.
let mut txq_cfg = TxqConfig::default();
for cpu in 0..possible_cpus {
let key_ptr: *const u32 = &cpu;
let val_ptr: *const TxqConfig = &txq_cfg;
let err = unsafe {
bpf_map_update_elem(map_fd, key_ptr as *const c_void, val_ptr as *mut c_void, 0)
};
if err != 0 {
error!("Unable to clear TXQ map");
return Err(MapTxqConfigError::BpfMapUpdateFail);
}
}
// Program shaping CPUs: physical CPU -> queue index+1.
for (logical_idx, &physical_cpu) in shaping_physical_cpus.iter().enumerate() {
// Ignore CPUs outside the possible range.
if physical_cpu >= possible_cpus {
continue;
}
let queue_u16: u16 = (logical_idx as u16).saturating_add(1);
txq_cfg.queue_mapping = queue_u16;
txq_cfg.htb_major = queue_u16;
let key_ptr: *const u32 = &physical_cpu;
let val_ptr: *const TxqConfig = &txq_cfg;
let err = unsafe {
bpf_map_update_elem(map_fd, key_ptr as *const c_void, val_ptr as *mut c_void, 0)
};
if err != 0 {
error!("Unable to update TXQ map for physical CPU {}", physical_cpu);
return Err(MapTxqConfigError::BpfMapUpdateFail);
}
}
Ok(())
}
#[derive(Error, Debug)]
pub enum MapTxqConfigError {
#[error("Unable to determine number of CPUs")]
+21 -2
View File
@@ -273,11 +273,30 @@ pub fn attach_xdp_and_tc_to_interface(
};
// Configure CPU Maps
let shaping_physical_cpus: Vec<u32> = match lqos_config::load_config() {
Ok(cfg) => {
let det = lqos_config::detect_shaping_cpus(cfg.as_ref());
info!(
"CPU topology: exclude_efficiency_cores={} source={:?} possible={} performance={} efficiency={} shaping={}",
det.exclude_efficiency_cores,
det.source,
det.possible.len(),
det.performance.len(),
det.efficiency.len(),
det.shaping.len()
);
det.shaping
}
Err(e) => {
warn!("Unable to load config for CPU topology detection: {:?}", e);
Vec::new()
}
};
{
let cpu_map = CpuMapping::new()?;
crate::cpu_map::xps_setup_default_disable(interface_name)?;
cpu_map.mark_cpus_available()?;
cpu_map.setup_base_txq_config()?;
cpu_map.mark_cpus_available(&shaping_physical_cpus)?;
cpu_map.setup_base_txq_config(&shaping_physical_cpus)?;
} // Scope block to ensure the CPU maps are closed
// Attach the TC program
@@ -157,6 +157,7 @@ function updateConfig() {
window.config.queue_check_period_ms = parseInt(document.getElementById("queueCheckPeriod").value);
window.config.disable_webserver = document.getElementById("disableWebserver").checked;
window.config.disable_icmp_ping = document.getElementById("disableIcmpPing").checked;
window.config.exclude_efficiency_cores = document.getElementById("excludeEfficiencyCores").checked;
window.config.enable_circuit_heatmaps = document.getElementById("enableCircuitHeatmaps").checked;
window.config.enable_site_heatmaps = document.getElementById("enableSiteHeatmaps").checked;
window.config.enable_asn_heatmaps = document.getElementById("enableAsnHeatmaps").checked;
@@ -193,6 +194,7 @@ loadConfig(() => {
document.getElementById("disableWebserver").checked = window.config.disable_webserver ?? false;
document.getElementById("webserverListen").value = window.config.webserver_listen ?? "";
document.getElementById("disableIcmpPing").checked = window.config.disable_icmp_ping ?? false;
document.getElementById("excludeEfficiencyCores").checked = window.config.exclude_efficiency_cores ?? true;
document.getElementById("enableCircuitHeatmaps").checked = window.config.enable_circuit_heatmaps ?? true;
document.getElementById("enableSiteHeatmaps").checked = window.config.enable_site_heatmaps ?? true;
document.getElementById("enableAsnHeatmaps").checked = window.config.enable_asn_heatmaps ?? true;
@@ -89,6 +89,12 @@
<div class="form-text">Disable ICMP ping monitoring for devices in the hosts view</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="excludeEfficiencyCores">
<label class="form-check-label" for="excludeEfficiencyCores">Exclude Efficiency Cores</label>
<div class="form-text">Avoid assigning shaping work to E-cores on hybrid CPUs (recommended)</div>
</div>
<button type="button" id="saveButton" class="btn btn-outline-primary">Save Changes</button>
</form>
</div>
+3 -2
View File
@@ -33,7 +33,9 @@ fn compute_stick_offset(config: &Config) -> Result<u32> {
count_tx_queues(&interface)?
};
let cpu_count = lqos_sys::num_possible_cpus().map_err(|e| anyhow::anyhow!("{e:?}"))?;
// Keep stick_offset consistent with CPU binning / cpumap behavior. On hybrid CPUs this may
// exclude efficiency cores, reducing the number of usable shaping CPUs.
let cpu_count = lqos_config::detect_shaping_cpus(config).shaping.len() as u32;
let queues_available = u32::min(queues_available, cpu_count);
let queues_per_direction = queues_available / 2;
@@ -83,4 +85,3 @@ fn count_tx_queues(interface: &str) -> Result<u32> {
Ok(tx_queues)
}
}