From dd8c9078005ebf97f87112fd442450e8e456ca4e Mon Sep 17 00:00:00 2001 From: RDouglas <81336713+RDouglasSharp@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:01:12 +0100 Subject: [PATCH] macOS/Apple Silicon port: NEON kernels + platform shim (PR #1 by RDouglasSharp) --- c/Makefile | 24 +++++++++++++- c/compat.h | 50 ++++++++++++++++++++++++++++ c/glm.c | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++-- c/iobench.c | 11 +++++++ c/setup.sh | 20 ++++++++--- c/st.h | 7 ++++ 6 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 c/compat.h diff --git a/c/Makefile b/c/Makefile index b035b9e..e48d40d 100644 --- a/c/Makefile +++ b/c/Makefile @@ -1,3 +1,24 @@ +UNAME_S := $(shell uname -s) + +ifeq ($(UNAME_S),Darwin) +# --- macOS / Apple Silicon --- +# Apple clang non include il runtime OpenMP: se c'e' libomp di Homebrew lo usa +# (brew install libomp), altrimenti compila single-thread (i pragma omp sono ignorati). +# Niente -march: su arm64 NEON e' baseline (i kernel __ARM_NEON si attivano da soli). +CC = clang +OMPDIR := $(shell brew --prefix libomp 2>/dev/null) +ifneq ($(OMPDIR),) +OMPC = -Xclang -fopenmp -I$(OMPDIR)/include +OMPL = -L$(OMPDIR)/lib -lomp +else +$(warning libomp non trovato: build SINGLE-THREAD. Per il multithread: brew install libomp) +OMPC = +OMPL = +endif +CFLAGS = -O3 $(OMPC) -Wall -Wextra -Wno-unused-parameter -Wno-misleading-indentation -Wno-unused-function +LDFLAGS = -lm $(OMPL) +else +# --- Linux x86-64 (percorso originale, invariato) --- CC = gcc # ARCH=native -> ottimizzato per QUESTA macchina (default, piu' veloce). # ARCH=x86-64-v3 -> binario PORTABILE su qualsiasi x86-64 moderno con AVX2 (per distribuire). @@ -5,10 +26,11 @@ CC = gcc ARCH ?= native CFLAGS = -O3 -march=$(ARCH) -fopenmp -Wall -Wextra -Wno-unused-parameter -Wno-misleading-indentation -Wno-unused-function LDFLAGS = -lm -fopenmp +endif all: glm -glm: glm.c st.h json.h tok.h tok_unicode.h +glm: glm.c st.h json.h tok.h tok_unicode.h compat.h $(CC) $(CFLAGS) glm.c -o glm $(LDFLAGS) olmoe: olmoe.c st.h json.h diff --git a/c/compat.h b/c/compat.h new file mode 100644 index 0000000..6663d39 --- /dev/null +++ b/c/compat.h @@ -0,0 +1,50 @@ +/* compat.h — shim di portabilita' per piattaforme non-Linux (oggi: macOS / Apple Silicon). + * Su Linux questo header e' un NO-OP totale: nessun simbolo definito o ridefinito, + * zero impatto sul percorso x86 esistente. + * Regola: ogni differenza di piattaforma vive QUI; i .c restano puliti. */ +#ifndef COMPAT_H +#define COMPAT_H + +#ifdef __APPLE__ +#include +#include +#include + +/* --- posix_fadvise: assente su macOS --- + * WILLNEED -> F_RDADVISE (readahead esplicito: stessa semantica). + * DONTNEED -> no-op: XNU non espone un drop mirato per-range; la sua unified + * buffer cache si autoregola sotto pressione. Il motore usa DONTNEED + * solo come consiglio, quindi ignorarlo e' corretto (e su una macchina + * con molta RAM tenere le pagine e' proprio cio' che si vuole). */ +#ifndef POSIX_FADV_NORMAL +#define POSIX_FADV_NORMAL 0 +#define POSIX_FADV_RANDOM 1 +#define POSIX_FADV_SEQUENTIAL 2 +#define POSIX_FADV_WILLNEED 3 +#define POSIX_FADV_DONTNEED 4 +#define POSIX_FADV_NOREUSE 5 +#endif +static inline int compat_fadvise(int fd, off_t off, off_t len, int advice){ + if(advice==POSIX_FADV_WILLNEED){ + struct radvisory ra; + ra.ra_offset = off; + ra.ra_count = (int)(len>0x7FFFFFFF ? 0x7FFFFFFF : len); + return fcntl(fd, F_RDADVISE, &ra)<0 ? -1 : 0; + } + return 0; +} +#define posix_fadvise compat_fadvise + +/* --- O_DIRECT: assente su macOS --- + * L'equivalente e' F_NOCACHE sul fd (bypass della unified buffer cache). + * compat_open_direct() apre il fd "gemello" senza cache, come il twin O_DIRECT + * di st.h. Le pread allineate a 4K del chiamante restano valide: F_NOCACHE non + * impone vincoli di allineamento. */ +static inline int compat_open_direct(const char *path){ + int fd = open(path, O_RDONLY); + if(fd>=0) fcntl(fd, F_NOCACHE, 1); + return fd; +} +#endif /* __APPLE__ */ + +#endif /* COMPAT_H */ diff --git a/c/glm.c b/c/glm.c index fee5d78..5288782 100644 --- a/c/glm.c +++ b/c/glm.c @@ -32,6 +32,11 @@ static inline float hsum256(__m256 v){ /* somma orizzontale di 8 floa lo=_mm_add_ps(lo,hi); __m128 sh=_mm_movehl_ps(lo,lo); lo=_mm_add_ps(lo,sh); sh=_mm_shuffle_ps(lo,lo,1); lo=_mm_add_ss(lo,sh); return _mm_cvtss_f32(lo); } +#elif defined(__ARM_NEON) +#include /* Apple Silicon / aarch64: kernel NEON */ +#endif +#ifdef __APPLE__ +#include /* host_statistics64: MemAvailable di macOS */ #endif typedef struct { @@ -104,7 +109,13 @@ typedef struct { static void usage_save(Model *m); /* cache che impara: definita accanto a stats_dump */ static double now_s(void){ struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec+t.tv_nsec*1e-9; } -static double rss_gb(void){ struct rusage r; getrusage(RUSAGE_SELF,&r); return r.ru_maxrss/(1024.0*1024.0); } +static double rss_gb(void){ struct rusage r; getrusage(RUSAGE_SELF,&r); +#ifdef __APPLE__ + return r.ru_maxrss/(1024.0*1024.0*1024.0); /* macOS: ru_maxrss in BYTE */ +#else + return r.ru_maxrss/(1024.0*1024.0); /* Linux: in KB */ +#endif +} static float *falloc(int64_t n){ float *p=malloc(n*sizeof(float)); if(!p){fprintf(stderr,"OOM\n");exit(1);} return p; } /* y[S,O] = x[S,I] @ W^T, W[O,I] f32 */ @@ -123,6 +134,12 @@ static void matmul_q(float *y, const float *x, const int8_t *q, const float *sca for(;i+8<=I;i+=8){ __m256i wi=_mm256_cvtepi8_epi32(_mm_loadl_epi64((const __m128i*)(w+i))); acc=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i), _mm256_cvtepi32_ps(wi), acc); } a=hsum256(acc); +#elif defined(__ARM_NEON) + float32x4_t ac0=vdupq_n_f32(0), ac1=vdupq_n_f32(0); + for(;i+8<=I;i+=8){ int16x8_t w16=vmovl_s8(vld1_s8(w+i)); + ac0=vfmaq_f32(ac0, vld1q_f32(xs+i), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w16)))); + ac1=vfmaq_f32(ac1, vld1q_f32(xs+i+4), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w16)))); } + a=vaddvq_f32(vaddq_f32(ac0,ac1)); #endif for(;i>1)); /* 8 byte=16 nibble */ + uint8x8x2_t z=vzip_u8(vand_u8(by,m4), vshr_n_u8(by,4)); /* nibble in ordine */ + int16x8_t w0=vmovl_s8(vsub_s8(vreinterpret_s8_u8(z.val[0]),b8)); + int16x8_t w1=vmovl_s8(vsub_s8(vreinterpret_s8_u8(z.val[1]),b8)); + ac0=vfmaq_f32(ac0, vld1q_f32(xs+i), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w0)))); + ac1=vfmaq_f32(ac1, vld1q_f32(xs+i+4), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w0)))); + ac0=vfmaq_f32(ac0, vld1q_f32(xs+i+8), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w1)))); + ac1=vfmaq_f32(ac1, vld1q_f32(xs+i+12), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w1)))); } + a=vaddvq_f32(vaddq_f32(ac0,ac1)); #endif for(;i+1>1]; int lo=(int)(byte&0xF)-8, hi=(int)(byte>>4)-8; a += xs[i]*(float)lo + xs[i+1]*(float)hi; } @@ -168,6 +197,21 @@ static void matmul_i2(float *y, const float *x, const uint8_t *q2, const float * acc=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i), w0, acc); acc=_mm256_fmadd_ps(_mm256_loadu_ps(xs+i+8), w1, acc); } a=hsum256(acc); +#elif defined(__ARM_NEON) + const uint8x8_t m2v=vdup_n_u8(3); const int8x8_t b2v=vdup_n_s8(2); + float32x4_t ac0=vdupq_n_f32(0), ac1=vdupq_n_f32(0); + for(;i+16<=I;i+=16){ uint32_t wd; memcpy(&wd, w+(i>>2), 4); /* 4 byte=16 valori */ + uint8x8_t by=vreinterpret_u8_u32(vdup_n_u32(wd)); + uint8x8x2_t z01=vzip_u8(vand_u8(by,m2v), vand_u8(vshr_n_u8(by,2),m2v)); + uint8x8x2_t z23=vzip_u8(vand_u8(vshr_n_u8(by,4),m2v), vshr_n_u8(by,6)); + uint16x4x2_t zz=vzip_u16(vreinterpret_u16_u8(z01.val[0]), vreinterpret_u16_u8(z23.val[0])); + int16x8_t w0=vmovl_s8(vsub_s8(vreinterpret_s8_u16(zz.val[0]),b2v)); /* 16 valori in ordine */ + int16x8_t w1=vmovl_s8(vsub_s8(vreinterpret_s8_u16(zz.val[1]),b2v)); + ac0=vfmaq_f32(ac0, vld1q_f32(xs+i), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w0)))); + ac1=vfmaq_f32(ac1, vld1q_f32(xs+i+4), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w0)))); + ac0=vfmaq_f32(ac0, vld1q_f32(xs+i+8), vcvtq_f32_s32(vmovl_s16(vget_low_s16(w1)))); + ac1=vfmaq_f32(ac1, vld1q_f32(xs+i+12), vcvtq_f32_s32(vmovl_s16(vget_high_s16(w1)))); } + a=vaddvq_f32(vaddq_f32(ac0,ac1)); #endif for(;i>2]; int sh=(i&3)*2; a += xs[i]*(float)((int)((byte>>sh)&3)-2); } y[(int64_t)s*O+o]=a*sc; } } @@ -203,6 +247,21 @@ static inline int32_t dot_i8i8(const int8_t *w, const int8_t *x, int I){ acc=_mm256_add_epi32(acc,_mm256_madd_epi16(p,ones)); } sum=hsum256_i32(acc); +#elif defined(__ARM_NEON) + /* ARM: SDOT nativo se disponibile (Apple Silicon: sempre); altrimenti vmull/vpadal. + * Stesso bound anti-overflow del trucco AVX2: coppie <= 128*127*2 = 32512 < 32767. */ + int32x4_t acc=vdupq_n_s32(0); + for(;i+16<=I;i+=16){ + int8x16_t wv=vld1q_s8(w+i), xv=vld1q_s8(x+i); +#if defined(__ARM_FEATURE_DOTPROD) + acc=vdotq_s32(acc,wv,xv); +#else + int16x8_t p=vmull_s8(vget_low_s8(wv),vget_low_s8(xv)); + p=vmlal_s8(p,vget_high_s8(wv),vget_high_s8(xv)); + acc=vpadalq_s16(acc,p); +#endif + } + sum=vaddvq_s32(acc); #endif for(;i>1)); /* 16 byte = 32 nibble */ + uint8x16x2_t z=vzipq_u8(vandq_u8(by,m4q), vshrq_n_u8(by,4)); /* nibble in ordine */ + int8x16_t w0=vsubq_s8(vreinterpretq_s8_u8(z.val[0]),b8q); + int8x16_t w1=vsubq_s8(vreinterpretq_s8_u8(z.val[1]),b8q); + int8x16_t x0=vld1q_s8(x+i), x1=vld1q_s8(x+i+16); +#if defined(__ARM_FEATURE_DOTPROD) + acc=vdotq_s32(acc,w0,x0); acc=vdotq_s32(acc,w1,x1); +#else + int16x8_t p=vmull_s8(vget_low_s8(w0),vget_low_s8(x0)); /* |w|<=8: nessun overflow */ + p=vmlal_s8(p,vget_high_s8(w0),vget_high_s8(x0)); + acc=vpadalq_s16(acc,p); + p=vmull_s8(vget_low_s8(w1),vget_low_s8(x1)); + p=vmlal_s8(p,vget_high_s8(w1),vget_high_s8(x1)); + acc=vpadalq_s16(acc,p); +#endif + } + sum=vaddvq_s32(acc); #endif for(;i+1>1]; sum+=((int)(b&0xF)-8)*x[i]+((int)(b>>4)-8)*x[i+1]; } if(i>1]; sum+=((int)(b&0xF)-8)*x[i]; } @@ -1419,12 +1499,23 @@ static void pin_load(Model *m, const char *statspath, double gb){ } static double g_mem_avail_boot=0; /* MemAvailable all'avvio, prima di caricare il modello */ -/* RAM disponibile ADESSO (GB) da /proc/meminfo: e' il tetto vero, non il totale */ +/* RAM disponibile ADESSO (GB): e' il tetto vero, non il totale. Linux: MemAvailable + * da /proc/meminfo. macOS: pagine free+inactive+purgeable da host_statistics64 + * (stessa semantica: recuperabili senza swap). Senza questo ramo il fallback + * "assumo 8 GB" castrava la cache expert proprio sulle macchine con piu' RAM. */ static double mem_available_gb(void){ +#ifdef __APPLE__ + mach_msg_type_number_t cnt=HOST_VM_INFO64_COUNT; + vm_statistics64_data_t vm; + if(host_statistics64(mach_host_self(),HOST_VM_INFO64,(host_info64_t)&vm,&cnt)!=KERN_SUCCESS) return 0; + return ((double)vm.free_count+(double)vm.inactive_count+(double)vm.purgeable_count) + * (double)sysconf(_SC_PAGESIZE) / 1e9; +#else FILE *f=fopen("/proc/meminfo","r"); if(!f) return 0; char ln[256]; double kb=0; while(fgets(ln,sizeof(ln),f)) if(sscanf(ln,"MemAvailable: %lf",&kb)==1) break; fclose(f); return kb/1e6; +#endif } /* byte disponibili per gli expert (pin + LRU) nel budget — specchio del conto di cap_for_ram */ diff --git a/c/iobench.c b/c/iobench.c index dad0c44..cfacb87 100644 --- a/c/iobench.c +++ b/c/iobench.c @@ -12,7 +12,9 @@ #include #include #include +#ifdef _OPENMP #include +#endif static double now(){ struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec+t.tv_nsec*1e-9; } int main(int argc,char**argv){ if(argc<2){fprintf(stderr,"uso: %s file [blkMB] [n] [threads] [direct 0/1]\n",argv[0]);return 1;} @@ -20,9 +22,18 @@ int main(int argc,char**argv){ int n=argc>3?atoi(argv[3]):64; int nth=argc>4?atoi(argv[4]):8; int direct=argc>5?atoi(argv[5]):1; +#ifdef O_DIRECT int fd=open(argv[1],O_RDONLY|(direct?O_DIRECT:0)); if(fd<0 && direct){ fprintf(stderr,"O_DIRECT non disponibile (%s), uso buffered\n",strerror(errno)); direct=0; fd=open(argv[1],O_RDONLY); } +#else + int fd=open(argv[1],O_RDONLY); /* macOS: F_NOCACHE ~ O_DIRECT */ +#ifdef __APPLE__ + if(direct && fd>=0) fcntl(fd,F_NOCACHE,1); +#else + if(direct){ fprintf(stderr,"O_DIRECT non disponibile, uso buffered\n"); direct=0; } +#endif +#endif if(fd<0){perror("open");return 1;} off_t sz=lseek(fd,0,SEEK_END); if(sz/dev/null || { echo "manca gcc (es: sudo apt install build-essential)"; exit 1; } command -v make >/dev/null || { echo "manca make"; exit 1; } -echo " gcc: $(gcc -dumpversion) · $(nproc) core" -echo -n " OpenMP: "; echo 'int main(){return 0;}' | gcc -fopenmp -xc - -o /tmp/_omp 2>/dev/null && echo ok || { echo "manca (libgomp)"; exit 1; } +if [ "$(uname -s)" = "Darwin" ]; then + command -v clang >/dev/null || { echo "manca clang (xcode-select --install)"; exit 1; } + echo " clang: $(clang --version | head -1) · $(sysctl -n hw.ncpu) core" + echo -n " OpenMP: " + if brew --prefix libomp >/dev/null 2>&1; then echo "ok (libomp)" + else echo "libomp assente -> build single-thread (consigliato: brew install libomp)"; fi +else + command -v gcc >/dev/null || { echo "manca gcc (es: sudo apt install build-essential)"; exit 1; } + echo " gcc: $(gcc -dumpversion) · $(nproc) core" + echo -n " OpenMP: "; echo 'int main(){return 0;}' | gcc -fopenmp -xc - -o /tmp/_omp 2>/dev/null && echo ok || { echo "manca (libgomp)"; exit 1; } +fi # 2) build: nativa (veloce, per QUESTA macchina). Per un binario da distribuire: make portable echo " compilo (ARCH=${ARCH:-native})…" @@ -23,7 +31,11 @@ if [ -d glm_tiny ] && [ -f ref_glm.json ]; then fi # 4) info macchina (la velocità dipende da QUESTI due numeri, non dalla GPU) -ram=$(awk '/MemTotal/{printf "%.0f", $2/1e6}' /proc/meminfo 2>/dev/null || echo "?") +if [ "$(uname -s)" = "Darwin" ]; then + ram=$(( $(sysctl -n hw.memsize) / 1000000000 )) +else + ram=$(awk '/MemTotal/{printf "%.0f", $2/1e6}' /proc/meminfo 2>/dev/null || echo "?") +fi echo " RAM: ${ram} GB (più RAM = più expert in cache = più veloce)" echo echo "pronto. Prossimi passi:" diff --git a/c/st.h b/c/st.h index f608481..61b84f1 100644 --- a/c/st.h +++ b/c/st.h @@ -15,6 +15,7 @@ #include #include #include "json.h" +#include "compat.h" typedef struct { char *name; @@ -78,7 +79,13 @@ static int st_open_fd(shards *S, const char *path) { int fd = open(path, O_RDONLY); if (fd < 0) { perror(path); exit(1); } S->paths[S->nfd] = strdup(path); S->fds[S->nfd] = fd; +#ifdef O_DIRECT S->dfds[S->nfd] = open(path, O_RDONLY | O_DIRECT); /* eager: lookup poi thread-safe */ +#elif defined(__APPLE__) + S->dfds[S->nfd] = compat_open_direct(path); /* macOS: F_NOCACHE ~ O_DIRECT */ +#else + S->dfds[S->nfd] = -1; /* niente equivalente: solo buffered */ +#endif S->nfd++; return fd; }