← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
cpu_features.c
Go to the documentation of this file.
1/**
2 * CPU Feature Detection and Cache-Aware Parameter Tuning
3 *
4 * Detects CPU features, cache sizes, and core counts at runtime.
5 * Computes optimal GEMM blocking parameters based on actual hardware.
6 */
7
8#include "cpu_features.h"
9#include <stdio.h>
10#include <string.h>
11#include <stdlib.h>
12
13#ifdef _WIN32
14#include <windows.h>
15#include <intrin.h>
16#else
17#include <unistd.h>
18#endif
19
20#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
21#define X86_CPU 1
22#if defined(__GNUC__) || defined(__clang__)
23#include <cpuid.h>
24#endif
25#endif
26
27// Global instances
31
32// =============================================================================
33// CPUID helpers for x86
34// =============================================================================
35
36#ifdef X86_CPU
37static void cpuid(int leaf, int subleaf, uint32_t* eax, uint32_t* ebx, uint32_t* ecx, uint32_t* edx) {
38#if defined(__GNUC__) || defined(__clang__)
39 __cpuid_count(leaf, subleaf, *eax, *ebx, *ecx, *edx);
40#elif defined(_MSC_VER)
41 int regs[4];
42 __cpuidex(regs, leaf, subleaf);
43 *eax = regs[0]; *ebx = regs[1]; *ecx = regs[2]; *edx = regs[3];
44#else
45 *eax = *ebx = *ecx = *edx = 0;
46#endif
47}
48
49static uint64_t xgetbv0(void) {
50#if defined(__GNUC__) || defined(__clang__)
51 uint32_t eax = 0, edx = 0;
52 __asm__ __volatile__(".byte 0x0f, 0x01, 0xd0" : "=a"(eax), "=d"(edx) : "c"(0));
53 return ((uint64_t)edx << 32) | eax;
54#elif defined(_MSC_VER)
55 return _xgetbv(0);
56#else
57 return 0;
58#endif
59}
60
61static void detect_x86_features(CPUInfo* info) {
62 uint32_t eax, ebx, ecx, edx;
63
64 // Check max CPUID leaf
65 cpuid(0, 0, &eax, &ebx, &ecx, &edx);
66 uint32_t max_leaf = eax;
67
68 if (max_leaf >= 1) {
69 cpuid(1, 0, &eax, &ebx, &ecx, &edx);
70 int has_osxsave = (ecx >> 27) & 1;
71 uint64_t xcr0 = has_osxsave ? xgetbv0() : 0;
72 int os_avx = ((xcr0 & 0x6) == 0x6);
73 int os_avx512 = ((xcr0 & 0xE6) == 0xE6);
74 int os_amx = ((xcr0 & 0x60000) == 0x60000);
75
76 info->has_avx = ((ecx >> 28) & 1) && os_avx;
77 info->has_fma = (ecx >> 12) & 1;
78
79 if (max_leaf >= 7) {
80 cpuid(7, 0, &eax, &ebx, &ecx, &edx);
81 info->has_avx2 = ((ebx >> 5) & 1) && os_avx;
82 info->has_avx512f = ((ebx >> 16) & 1) && os_avx512;
83 info->has_avx512dq = ((ebx >> 17) & 1) && os_avx512;
84 info->has_avx512bw = ((ebx >> 30) & 1) && os_avx512;
85 info->has_avx512vl = ((ebx >> 31) & 1) && os_avx512;
86 info->has_avx512_vnni = ((ecx >> 11) & 1) && os_avx512;
87 info->has_amx_bf16 = ((edx >> 22) & 1) && os_amx;
88 info->has_amx_tile = ((edx >> 24) & 1) && os_amx;
89 info->has_amx_int8 = ((edx >> 25) & 1) && os_amx;
90 }
91 }
92
93 if (max_leaf >= 7) {
94 cpuid(7, 1, &eax, &ebx, &ecx, &edx);
95 if (info->has_avx512f) {
96 info->has_avx512_bf16 = (eax >> 5) & 1;
97 }
98 }
99}
100
101// Detect cache sizes using CPUID leaf 0x04 (Intel) or leaf 0x8000001D (AMD)
102static void detect_x86_cache_sizes(CPUInfo* info) {
103 uint32_t eax, ebx, ecx, edx;
104
105 // Try Intel deterministic cache parameters (leaf 0x04)
106 for (int index = 0; index < 16; index++) {
107 cpuid(0x04, index, &eax, &ebx, &ecx, &edx);
108
109 int cache_type = eax & 0x1F;
110 if (cache_type == 0) break; // No more caches
111
112 int cache_level = (eax >> 5) & 0x7;
113 int line_size = (ebx & 0xFFF) + 1;
114 int partitions = ((ebx >> 12) & 0x3FF) + 1;
115 int ways = ((ebx >> 22) & 0x3FF) + 1;
116 int sets = ecx + 1;
117
118 size_t cache_size = (size_t)line_size * partitions * ways * sets;
119
120 if (cache_type == 1 || cache_type == 3) { // Data or unified cache
121 if (cache_level == 1) {
122 info->l1d_size = cache_size;
123 info->l1_line_size = line_size;
124 } else if (cache_level == 2) {
125 info->l2_size = cache_size;
126 } else if (cache_level == 3) {
127 info->l3_size = cache_size;
128 }
129 }
130 }
131
132 // If Intel method didn't work, try AMD method (leaf 0x8000001D)
133 if (info->l1d_size == 0) {
134 cpuid(0x80000000, 0, &eax, &ebx, &ecx, &edx);
135 if (eax >= 0x8000001D) {
136 for (int index = 0; index < 16; index++) {
137 cpuid(0x8000001D, index, &eax, &ebx, &ecx, &edx);
138
139 int cache_type = eax & 0x1F;
140 if (cache_type == 0) break;
141
142 int cache_level = (eax >> 5) & 0x7;
143 int line_size = (ebx & 0xFFF) + 1;
144 int partitions = ((ebx >> 12) & 0x3FF) + 1;
145 int ways = ((ebx >> 22) & 0x3FF) + 1;
146 int sets = ecx + 1;
147
148 size_t cache_size = (size_t)line_size * partitions * ways * sets;
149
150 if (cache_type == 1 || cache_type == 3) {
151 if (cache_level == 1) {
152 info->l1d_size = cache_size;
153 info->l1_line_size = line_size;
154 } else if (cache_level == 2) {
155 info->l2_size = cache_size;
156 } else if (cache_level == 3) {
157 info->l3_size = cache_size;
158 }
159 }
160 }
161 }
162 }
163}
164#endif
165
166// =============================================================================
167// Linux sysfs fallback for cache detection
168// =============================================================================
169
170#if defined(__linux__)
171static size_t read_sysfs_cache_size(int cpu, int index) {
172 char path[256];
173 snprintf(path, sizeof(path),
174 "/sys/devices/system/cpu/cpu%d/cache/index%d/size", cpu, index);
175
176 FILE* f = fopen(path, "r");
177 if (!f) return 0;
178
179 char buf[32];
180 if (!fgets(buf, sizeof(buf), f)) {
181 fclose(f);
182 return 0;
183 }
184 fclose(f);
185
186 size_t size = 0;
187 char unit = 'K';
188 sscanf(buf, "%zu%c", &size, &unit);
189
190 if (unit == 'K' || unit == 'k') size *= 1024;
191 else if (unit == 'M' || unit == 'm') size *= 1024 * 1024;
192
193 return size;
194}
195
196static int read_sysfs_cache_level(int cpu, int index) {
197 char path[256];
198 snprintf(path, sizeof(path),
199 "/sys/devices/system/cpu/cpu%d/cache/index%d/level", cpu, index);
200
201 FILE* f = fopen(path, "r");
202 if (!f) return -1;
203
204 int level = 0;
205 if (fscanf(f, "%d", &level) != 1) level = -1;
206 fclose(f);
207 return level;
208}
209
210static void detect_linux_cache_sizes(CPUInfo* info) {
211 // Read from CPU 0's cache info
212 for (int index = 0; index < 10; index++) {
213 size_t size = read_sysfs_cache_size(0, index);
214 if (size == 0) break;
215
216 int level = read_sysfs_cache_level(0, index);
217
218 // Check if it's data or unified cache
219 char path[256];
220 snprintf(path, sizeof(path),
221 "/sys/devices/system/cpu/cpu0/cache/index%d/type", index);
222 FILE* f = fopen(path, "r");
223 if (f) {
224 char type[32] = {0};
225 if (fscanf(f, "%31s", type) != 1) type[0] = '\0';
226 fclose(f);
227
228 if (strcmp(type, "Data") == 0 || strcmp(type, "Unified") == 0) {
229 if (level == 1) info->l1d_size = size;
230 else if (level == 2) info->l2_size = size;
231 else if (level == 3) info->l3_size = size;
232 }
233 }
234 }
235}
236
237static int detect_linux_physical_cores(void) {
238 // Count unique physical cores by reading core_id
239 int max_core_id = -1;
240 int num_processors = 0;
241
242 FILE* f = fopen("/proc/cpuinfo", "r");
243 if (!f) return 1;
244
245 char line[256];
246 while (fgets(line, sizeof(line), f)) {
247 if (strncmp(line, "processor", 9) == 0) {
248 num_processors++;
249 }
250 if (strncmp(line, "cpu cores", 9) == 0) {
251 int cores = 0;
252 sscanf(line, "cpu cores : %d", &cores);
253 if (cores > 0) {
254 fclose(f);
255 return cores;
256 }
257 }
258 if (strncmp(line, "core id", 7) == 0) {
259 int core_id = 0;
260 sscanf(line, "core id : %d", &core_id);
261 if (core_id > max_core_id) max_core_id = core_id;
262 }
263 }
264 fclose(f);
265
266 // Fallback: use processor count (may include hyperthreads)
267 if (max_core_id >= 0) {
268 return max_core_id + 1;
269 }
270 return num_processors > 0 ? num_processors : 1;
271}
272#endif
273
274// =============================================================================
275// Physical core detection
276// =============================================================================
277
278static int detect_physical_cores(void) {
279#if defined(__linux__)
280 return detect_linux_physical_cores();
281#elif defined(_WIN32)
282 SYSTEM_INFO sysinfo;
283 GetSystemInfo(&sysinfo);
284 // Windows: this gives logical processors, divide by 2 for HT estimate
285 return sysinfo.dwNumberOfProcessors / 2;
286#elif defined(__APPLE__)
287 int cores = 1;
288 size_t len = sizeof(cores);
289 sysctlbyname("hw.physicalcpu", &cores, &len, NULL, 0);
290 return cores;
291#else
292 return 1;
293#endif
294}
295
296// =============================================================================
297// Compute optimal GEMM blocking parameters
298// =============================================================================
299
300static void compute_gemm_params(const CPUInfo* cpu, GEMMParams* params) {
301 // Microkernel sizes based on SIMD width
302 // MUST match compile-time MR_FIXED/NR_FIXED in gemm_microkernel.c
303 if (cpu->has_avx512f) {
304 params->MR = 6; // 6 rows
305 params->NR = 32; // 32 cols (2 x ZMM registers)
306 } else if (cpu->has_fma) {
307 // AVX2+FMA: can use 6x16 with FMA hiding register spilling
308 params->MR = 6; // 6 rows
309 params->NR = 16; // 16 cols (2 x YMM registers)
310 } else if (cpu->has_avx || cpu->has_avx2) {
311 // AVX without FMA: use 4x16 to avoid register spilling
312 params->MR = 4; // 4 rows (reduced to fit in 16 YMM registers)
313 params->NR = 16; // 16 cols (2 x YMM registers)
314 } else {
315 params->MR = 4;
316 params->NR = 4;
317 }
318
319 // Get cache sizes (use defaults if detection failed)
320 size_t l1 = cpu->l1d_size > 0 ? cpu->l1d_size : 32 * 1024; // Default 32KB
321 size_t l2 = cpu->l2_size > 0 ? cpu->l2_size : 256 * 1024; // Default 256KB
322 size_t l3 = cpu->l3_size > 0 ? cpu->l3_size : 8 * 1024 * 1024; // Default 8MB
323
324 // BLIS-style blocking parameter computation
325 // Reference: "Anatomy of High-Performance Matrix Multiplication" (Goto & Van de Geijn)
326 //
327 // KC: Controls L1 usage
328 // - A micropanel: MR x KC
329 // - B micropanel: KC x NR (streamed from L2)
330 // - Want MR * KC * sizeof(float) to fit in ~half of L1
331 //
332 // MC: Controls L2 usage
333 // - A block: MC x KC should fit in L2
334 //
335 // NC: Controls L3 usage / main memory streaming
336 // - B panel: KC x NC
337
338 // KC: Controls L1 usage
339 // For optimal performance, both A micropanel and B row should fit in L1:
340 // - A micropanel: MR * KC floats
341 // - B row for streaming: NR floats per iteration (small)
342 // Use ~25% of L1 for A micropanel to leave room for B and working set
343 size_t l1_for_a = (l1 * 25) / 100;
344 params->KC = (int)(l1_for_a / (params->MR * sizeof(float)));
345
346 // Round KC to multiple of 8 for alignment
347 params->KC = (params->KC / 8) * 8;
348 if (params->KC < 64) params->KC = 64;
349 if (params->KC > 512) params->KC = 512; // Cap at 512 for better cache fit
350
351 // MC: A block = MC * KC * 4 bytes should fit in ~80% of L2
352 size_t l2_for_a = (l2 * 80) / 100;
353 params->MC = (int)(l2_for_a / (params->KC * sizeof(float)));
354
355 // Round MC to multiple of MR
356 params->MC = (params->MC / params->MR) * params->MR;
357 if (params->MC < params->MR * 4) params->MC = params->MR * 4;
358 if (params->MC > 512) params->MC = 512;
359
360 // NC: B panel = KC * NC * 4 bytes
361 // For L3, we want good streaming, use ~50% of L3 / num_cores
362 size_t l3_per_core = l3 / (cpu->num_cores > 0 ? cpu->num_cores : 1);
363 size_t l3_for_b = (l3_per_core * 50) / 100;
364 params->NC = (int)(l3_for_b / (params->KC * sizeof(float)));
365
366 // Round NC to multiple of NR
367 params->NC = (params->NC / params->NR) * params->NR;
368 if (params->NC < params->NR * 8) params->NC = params->NR * 8;
369 if (params->NC > 8192) params->NC = 8192;
370}
371
372// =============================================================================
373// Public API
374// =============================================================================
375
377 if (g_cpu_initialized) return;
378
379 memset(&g_cpu_info, 0, sizeof(g_cpu_info));
380 memset(&g_gemm_params, 0, sizeof(g_gemm_params));
381
382 // Detect SIMD features
383#ifdef X86_CPU
384 detect_x86_features(&g_cpu_info);
385 detect_x86_cache_sizes(&g_cpu_info);
386#endif
387
388 // Linux sysfs fallback for cache sizes
389#if defined(__linux__)
390 if (g_cpu_info.l1d_size == 0) {
391 detect_linux_cache_sizes(&g_cpu_info);
392 }
393#endif
394
395 // Detect physical cores
397
398 // Compute GEMM parameters based on detected hardware
400
402}
403
406 return &g_gemm_params;
407}
408
409const CPUInfo* get_cpu_info(void) {
411 return &g_cpu_info;
412}
413
414void print_cpu_info(void) {
416
417 printf("=== CPU Info ===\n");
418 printf("Physical cores: %d\n", g_cpu_info.num_cores);
419 printf("L1 Data Cache: %zu KB\n", g_cpu_info.l1d_size / 1024);
420 printf("L2 Cache: %zu KB\n", g_cpu_info.l2_size / 1024);
421 printf("L3 Cache: %zu MB\n", g_cpu_info.l3_size / (1024 * 1024));
422 printf("Cache line: %zu bytes\n", g_cpu_info.l1_line_size);
423 printf("AVX: %s\n", g_cpu_info.has_avx ? "yes" : "no");
424 printf("AVX2: %s\n", g_cpu_info.has_avx2 ? "yes" : "no");
425 printf("AVX-512F: %s\n", g_cpu_info.has_avx512f ? "yes" : "no");
426 printf("AVX-512BW/DQ/VL:%s/%s/%s\n",
427 g_cpu_info.has_avx512bw ? "yes" : "no",
428 g_cpu_info.has_avx512dq ? "yes" : "no",
429 g_cpu_info.has_avx512vl ? "yes" : "no");
430 printf("AVX-512 VNNI: %s\n", g_cpu_info.has_avx512_vnni ? "yes" : "no");
431 printf("AVX-512 BF16: %s\n", g_cpu_info.has_avx512_bf16 ? "yes" : "no");
432 printf("AMX tile/int8/bf16: %s/%s/%s\n",
433 g_cpu_info.has_amx_tile ? "yes" : "no",
434 g_cpu_info.has_amx_int8 ? "yes" : "no",
435 g_cpu_info.has_amx_bf16 ? "yes" : "no");
436 printf("FMA: %s\n", g_cpu_info.has_fma ? "yes" : "no");
437 printf("\n=== GEMM Blocking Parameters ===\n");
438 printf("MR (microkernel rows): %d\n", g_gemm_params.MR);
439 printf("NR (microkernel cols): %d\n", g_gemm_params.NR);
440 printf("MC (M block): %d\n", g_gemm_params.MC);
441 printf("NC (N block): %d\n", g_gemm_params.NC);
442 printf("KC (K block): %d\n", g_gemm_params.KC);
443 printf("\n");
444}
void print_cpu_info(void)
static void compute_gemm_params(const CPUInfo *cpu, GEMMParams *params)
const GEMMParams * get_gemm_params(void)
const CPUInfo * get_cpu_info(void)
CPUInfo g_cpu_info
static int detect_physical_cores(void)
int g_cpu_initialized
void cpu_features_init(void)
GEMMParams g_gemm_params
int has_amx_int8
size_t l1d_size
int has_amx_bf16
size_t l3_size
int has_avx2
int has_amx_tile
int has_fma
int has_avx
int has_avx512f
int has_avx512_bf16
int has_avx512dq
int num_cores
int has_avx512vl
size_t l2_size
int has_avx512bw
size_t l1_line_size
int has_avx512_vnni