Skip to content

Detect CPU cache sizes via sysfs on aarch64 Linux#12233

Merged
trivialfis merged 4 commits into
dmlc:masterfrom
siqi-he:cachemanager-arm-support
Jun 9, 2026
Merged

Detect CPU cache sizes via sysfs on aarch64 Linux#12233
trivialfis merged 4 commits into
dmlc:masterfrom
siqi-he:cachemanager-arm-support

Conversation

@siqi-he

@siqi-he siqi-he commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

CacheManager only detects L1/L2/L3 sizes via CPUID, which is x86-only. This adds a Linux sysfs detector for the non-x86_64 path: it walks /sys/devices/system/cpu/cpu0/cache/index*/, keeps data/unified caches, and stores each at the slot for its reported level (L1→[0], L2→[1], L3→[2]).

Benchmark methodology

All benchmarks use tree_method='hist', max_depth=8, max_bin=256, 100 rounds, 3 repeats (average of runs 2-3). CPU pinning via taskset. The benchmarks were run using aws ec2 c8g.24xl instance, using all physical cores (i.e. nthread=96).

Datasets include real (Epsilon, Bosch, Santander) and synthetic (HIGGS with PolynomialFeatures expansion, various sparsity levels via injected NaN at fixed seed). Sparse datasets force the row-wise kernel path (IsDense()=false). Predictions were verified to be identical (measured by np.allclose) between master (60ae480) and this branch across all datasets at 100 rounds.

Results

Dataset Features master (s) branch (s) Speed-up
HIGGS poly-3 sparse 0.1% 4494 36.73 21.22 +42.2%
HIGGS poly-3 sparse 1% 4494 37.55 22.11 +41.1%
epsilon dense 2000 20.78 15.75 +24.2%
HIGGS poly-3 1k-feat sparse 0.1% 1000 5.96 6.17 -3.5%
HIGGS poly-3 1k-feat sparse 1% 1000 5.99 6.34 -5.8%
HIGGS poly-3 1k-feat sparse 10% 1000 6.73 6.54 +2.8%
HIGGS poly-3 1k-feat sparse 49.99% 1000 4.88 5.06 -3.7%
Bosch 968 3.14 3.10 +1.3%
HIGGS poly-3 500-feat dense 500 2.65 2.72 -2.6%
santander 200 1.70 1.71 -0.6%
HIGGS raw 1M 28 1.27 1.25 +1.6%
airline 23 1.21 1.18 +2.5%

The gains scale with feature width (+24% to +42% at ≥2000 features); datasets around ~1000 features and below are within noise, with a few small regressions (worst: −5.8%). Net positive for wide workloads, roughly neutral for narrow ones.

full benchmark script
#!/usr/bin/env python
"""Benchmark script for tiling PR.
Reproduces the results table in the PR description.

Usage:
    taskset -c 0-63 python bench_pr.py <label> [nthreads]

Datasets required:
    - HIGGS.csv (Kaggle Higgs)
    - data/epsilon.bz2 (LIBSVM format, from https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/)
    - data/santander_train.csv (Kaggle Santander)
    - train_numeric.csv (Kaggle Bosch)
"""
import datetime
import gc
import os
import sys

import numpy as np
import xgboost as xgb

LABEL = sys.argv[1] if len(sys.argv) > 1 else "unknown"
N_THREADS = int(sys.argv[2]) if len(sys.argv) > 2 else os.cpu_count() // 2
NUM_ROUND = 100
N_REPEATS = 3

PARAM = {
    "objective": "binary:logistic",
    "eta": 0.1,
    "max_depth": 8,
    "nthread": N_THREADS,
    "tree_method": "hist",
    "max_bin": 256,
}


def log(msg):
    print(msg, flush=True)


def inject_sparse(X, fraction, seed=123):
    rng = np.random.RandomState(seed)
    mask = rng.random(X.shape) < fraction
    X_sp = X.copy()
    X_sp[mask] = np.nan
    return X_sp


def bench(name, dm, n_features):
    hist_mb = n_features * 256 * 16 / (1024 * 1024)
    log(f"\n  {name}: hist={hist_mb:.1f}MB")
    xgb.train(PARAM, dm, 5)
    times = []
    for r in range(N_REPEATS):
        st = datetime.datetime.now()
        xgb.train(PARAM, dm, NUM_ROUND)
        elapsed = (datetime.datetime.now() - st).total_seconds()
        times.append(elapsed)
        log(f"    Run {r + 1}: {elapsed:.2f}s")
    avg = np.mean(times[1:])
    std = np.std(times[1:])
    log(f"    >> {name}: {avg:.2f}s +/-{std:.2f}")


log(f"{'=' * 70}")
log(f"Benchmark: {LABEL}")
log(f"Threads: {N_THREADS}, Rounds: {NUM_ROUND}, Repeats: {N_REPEATS}")
log(f"{'=' * 70}")

# ---------- Epsilon ----------
log("\nLoading Epsilon...")
from sklearn.datasets import load_svmlight_file

X_eps, y_eps = load_svmlight_file("data/epsilon.bz2", n_features=2000)
X_eps = X_eps.toarray().astype(np.float32)
y_eps = ((y_eps + 1) / 2).astype(np.float32)

bench("epsilon-dense", xgb.DMatrix(X_eps, label=y_eps), 2000)
bench(
    "epsilon-sparse0.1%",
    xgb.DMatrix(inject_sparse(X_eps, 0.001), label=y_eps, missing=np.nan),
    2000,
)
del X_eps, y_eps
gc.collect()

# ---------- HIGGS poly-3 ----------
log("\nLoading HIGGS poly-3...")
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures

df = pd.read_csv("HIGGS.csv", header=None, nrows=100_000)
data_raw = df.values[:, 1:].astype(np.float32)
label_h = df.values[:, 0].astype(np.float32)
poly = PolynomialFeatures(degree=3, interaction_only=False, include_bias=False)
X_h3 = poly.fit_transform(data_raw).astype(np.float32)
del df, data_raw, poly
gc.collect()

# 4494 features
bench(
    "higgs3-sparse0.1%",
    xgb.DMatrix(inject_sparse(X_h3, 0.001), label=label_h, missing=np.nan),
    4494,
)
bench(
    "higgs3-sparse1%",
    xgb.DMatrix(inject_sparse(X_h3, 0.01), label=label_h, missing=np.nan),
    4494,
)

# 1000 features
X_1k = X_h3[:, :1000].copy()
del X_h3
gc.collect()

bench(
    "higgs3-1kf-sparse0.1%",
    xgb.DMatrix(inject_sparse(X_1k, 0.001), label=label_h, missing=np.nan),
    1000,
)
bench(
    "higgs3-1kf-sparse1%",
    xgb.DMatrix(inject_sparse(X_1k, 0.01), label=label_h, missing=np.nan),
    1000,
)
bench(
    "higgs3-1kf-sparse10%",
    xgb.DMatrix(inject_sparse(X_1k, 0.10), label=label_h, missing=np.nan),
    1000,
)
bench(
    "higgs3-1kf-sparse49.99%",
    xgb.DMatrix(inject_sparse(X_1k, 0.4999), label=label_h, missing=np.nan),
    1000,
)

# 500 features dense
X_500 = X_1k[:, :500].copy()
del X_1k
gc.collect()
bench("higgs3-500f-dense", xgb.DMatrix(X_500, label=label_h), 500)
del X_500, label_h
gc.collect()

# ---------- Santander ----------
log("\nLoading Santander...")
df_s = pd.read_csv("data/santander_train.csv")
y_s = df_s["target"].values.astype(np.float32)
X_s = df_s.drop(columns=["ID_code", "target"]).values.astype(np.float32)
bench("santander", xgb.DMatrix(X_s, label=y_s), 200)
del X_s, y_s, df_s
gc.collect()

# ---------- Bosch ----------
log("\nLoading Bosch...")
df_b = pd.read_csv("train_numeric.csv")
y_b = df_b["Response"].values.astype(np.float32)
X_b = df_b.drop(columns=["Id", "Response"]).values.astype(np.float32)
bench("bosch", xgb.DMatrix(X_b, label=y_b, missing=np.nan), 968)
del X_b, y_b, df_b
gc.collect()

log(f"\n{'=' * 70}")
log("Done")

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends CacheManager CPU cache size detection beyond x86_64 by adding a non-x86 Linux fallback that reads data/unified cache sizes from Linux sysfs (/sys/devices/system/cpu/cpu0/cache/index*/) and maps them into the existing L1/L2/L3 slots used by histogram tiling heuristics.

Changes:

  • Add a Linux sysfs-based cache detector for non-x86_64 builds and wire it into CacheManager::CacheManager().
  • Introduce parsing for sysfs cache size strings (e.g., 64K, 2M) and store sizes by reported cache level.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +136 to +139
const std::string base = "/sys/devices/system/cpu/cpu0/cache/index";

for (int i = 0; i < 16; ++i) {
const std::string dir = base + std::to_string(i);
Comment on lines +101 to +130
// Parse a sysfs cache "size" string like "64K", "2048K", "36864K", "2M".
int64_t ParseCacheSize(const std::string& s) {
if (s.empty()) return -1; // kUninitCache
int64_t mult = 1;
std::string num = s;
switch (num.back()) {
case 'K':
case 'k':
mult = 1024;
num.pop_back();
break;
case 'M':
case 'm':
mult = 1024 * 1024;
num.pop_back();
break;
case 'G':
case 'g':
mult = 1024 * 1024 * 1024;
num.pop_back();
break;
default:
break;
}
try {
return static_cast<int64_t>(std::stoll(num)) * mult;
} catch (...) {
return -1; // kUninitCache
}
}
@trivialfis

trivialfis commented Jun 9, 2026

Copy link
Copy Markdown
Member

@siqi-he Thank you for the optimization. May I ask why x86 isn't using the same cache detection and resorts to CPU instructions? Is there any caveat to using sysfs?

note:

I can reproduce the speedup:

* Optimize cache

[127]	Train-rmse:162.13892
{'BenchIter': {'GetTrain': 1621.5918023586273}, 'Train': {'DMatrix-Train': 1754.6797909736633, 'Train': 707.7474420070648}}

* master

[127]	Train-rmse:162.13892
{'BenchIter': {'GetTrain': 1605.8990445137024}, 'Train': {'DMatrix-Train': 1739.5034222602844, 'Train': 790.615291595459}}

@trivialfis

Copy link
Copy Markdown
Member

If memory serves, at the time when the detection was added for x86, there were all sorts of weirdnesses like inaccurate size in VMs.

@siqi-he

siqi-he commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

i believe the existing cpuid approach could work for majority of os (e.g. linux, windows and mac), whereas the sysfs approach only works for linux based system. i did not touch the existing logic for handling x86 linux as i was under the assumption it's working as expected.

@trivialfis
trivialfis merged commit f416974 into dmlc:master Jun 9, 2026
81 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants