-
Notifications
You must be signed in to change notification settings - Fork 174
Expand file tree
/
Copy pathMakefile
More file actions
372 lines (333 loc) · 16.8 KB
/
Copy pathMakefile
File metadata and controls
372 lines (333 loc) · 16.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# WASTE — embeddable MoE inference engine + CLI
#
# make library + cli
# make test the validation binaries
# make WASTE_ENABLE_METAL=1 (accelerators are build-time options)
# Explicit, because per-object rules below would otherwise make the first
# of them the default goal.
.DEFAULT_GOAL := all
CC ?= cc
# gnu11, not c11: with -std=c11 glibc sets __STRICT_ANSI__ and hides every
# POSIX extension, so pread, fcntl, posix_memalign and pthread_* would all
# be implicitly declared on Linux. Only model.c defines _GNU_SOURCE itself.
CFLAGS ?= -O2 -std=gnu11 -Wall -Wextra
LDLIBS := -lm -lpthread
# The interpreter for the three Python recipes below, resolved by RUNNING a
# candidate rather than by looking one up. On Windows the name `python3` on
# PATH is usually the Microsoft Store App Execution Alias: a zero-byte
# reparse point that exists, exits 49, and prints an advert for the Store
# instead of running anything, so a recipe reports its own tool broken. Same
# finding as tests/run.sh, which shims PATH because it has 49 call sites and
# subprocesses to cover; three recipes want a name instead. PATH set inside
# run.sh does not reach a recipe make runs itself, which is why this is here
# and not there.
#
# Windows installs the versionless `python`, and `py` besides, so a working
# interpreter is normally there under another name. If none answers this
# stays `python3` and the recipe fails loudly at first use, which is right
# for a build target: unlike the suite, there is nothing here to skip.
#
# Recursive rather than `:=` so the probe runs only when a recipe actually
# expands $(PY). Immediate assignment costs every `make` invocation two
# process spawns, `make clean` and a no-op build included, and on the box
# this was found the first spawn is the Store alias itself: measured 196 ms
# added per invocation, which nearly triples a no-op make. The fallback
# lives inside the shell for the same reason, since an `ifeq` on $(PY)
# would force the expansion at parse time and undo it.
PY = $(shell for p in python3 python py; do \
"$$p" -c '' >/dev/null 2>&1 && { echo "$$p"; exit 0; }; done; echo python3)
# The target triple rather than `uname -m`, because those differ the moment
# anyone cross-compiles: building for Windows from this ARM laptop, uname
# says arm64 and would put the NEON translation unit into an x86 binary
# while leaving out the two SIMD ones the target actually dispatches to.
TRIPLE := $(shell $(CC) -dumpmachine 2>/dev/null)
# Stock MinGW ships gcc.exe and no cc.exe, so the default CC does not exist
# there and -dumpmachine answered nothing. The fallback below was `uname -m`,
# which on MSYS says x86_64 and never contains "mingw" — so WINDOWS, EXE and
# SOEXT stayed unset and make quietly built for the wrong platform, first
# noticed when it tried to link an extensionless test_kda with a compiler
# that is not installed. A missing compiler must not look like a Linux host.
CC0 := $(CC)
# A CC given on the command line wins over this assignment, by make's own
# rules — so an explicitly named compiler that does not work is an error
# rather than something quietly replaced.
ifeq (,$(TRIPLE))
CC := gcc
TRIPLE := $(shell $(CC) -dumpmachine 2>/dev/null)
endif
ifeq (,$(TRIPLE))
$(error no working C compiler: `$(CC0)` does not answer -dumpmachine$(if \
$(filter-out $(CC0),$(CC)), and neither does `$(CC)`). Name one that does, \
e.g. `make CC=gcc` on MinGW)
endif
ARCH := $(TRIPLE)
# Shared-library suffix: convert.py and serve/engine.py look for all three
# through ctypes.
ifneq (,$(findstring mingw,$(ARCH)))
WINDOWS := 1
endif
ifdef WINDOWS
SOEXT := dll
EXE := .exe
# The archiver has to match the target as well as the compiler. macOS `ar`
# writes an archive of PE objects without complaining; what comes out is
# 96 bytes and a page of undefined references at link time, with nothing
# anywhere saying the archiver was the wrong one.
#
# The -posix/-win32 strip is for Debian and Ubuntu, which ship the two
# threading models as separate compilers. Only -posix has pthread.h, so
# that is the one to build with — and its name does not end in `gcc`.
CCBASE := $(patsubst %-posix,%,$(patsubst %-win32,%,$(CC)))
AR := $(if $(filter %gcc,$(CCBASE)),$(patsubst %gcc,%ar,$(CCBASE)),ar)
# msvcrt's printf has no %zu and no C99 anything. mingw ships a conforming
# one; this is what selects it, and without it every size_t we print is
# garbage rather than a compile error.
CFLAGS += -D__USE_MINGW_ANSI_STDIO=1
# One self-contained .exe/.dll: winpthreads and libgcc go inside rather
# than turning `waste.exe` into a DLL hunt on a machine with no toolchain.
# libwastevq needs it for the same reason from the other direction — it is
# loaded by convert.py through ctypes, where a missing dependency surfaces
# as "could not be found" naming the library that was found.
STATIC := -static
LDLIBS += $(STATIC)
# ld exports everything from a DLL only until one symbol is marked
# dllexport. Nothing here is, but saying it keeps serve/'s ctypes binding
# working the day one is.
SHLDFLAGS := -Wl,--export-all-symbols
else ifeq ($(shell uname -s),Darwin)
SOEXT := dylib
# Same rule as the Windows branch above, from the other direction: the
# archiver has to match the target, and here PATH is what gets it wrong.
# Apple's ld requires every 64-bit Mach-O member of an archive to begin on
# an 8-byte boundary, which Apple's ar arranges by padding the member's
# name field (`#1/20` for a 10-character name). GNU ar writes the short
# name form instead, putting the first member at offset 68, and the build
# then gets all the way to the link before failing with "64-bit mach-o
# member 'kda_neon.o' not 8-byte aligned in 'libwaste.a'" — issue #16, on a
# machine with Homebrew's binutils ahead of /usr/bin. Name the system
# archiver rather than trusting PATH. A command-line AR= still overrides.
AR := $(if $(wildcard /usr/bin/ar),/usr/bin/ar,ar)
else
SOEXT := so
endif
# Windows code is position-independent by definition and gcc warns, once
# per file, when told again.
PICFLAG := $(if $(WINDOWS),,-fPIC)
VQ_SUPER ?= 2
CFLAGS += -DVQ_SUPER=$(VQ_SUPER)
# Track header dependencies. Without this a changed struct in a header
# leaves stale objects compiled against the old layout — which links fine
# and then corrupts memory at run time.
CFLAGS += -MMD -MP
SRC := src/model.c src/kda.c src/backend.c src/ecache.c src/version.c \
src/tokenizer.c src/waste.c src/vq.c src/vision.c src/image.c \
src/crc32.c src/memory.c
# Match what backend.c tests for. Linux/aarch64 reports "aarch64", which
# does not contain "arm" — the old findstring left kda_neon.c out of the
# build while backend.c still emitted the call to it, so the link failed
# with an undefined waste_kda_register_neon.
ifneq (,$(filter arm% aarch64%,$(ARCH)))
SRC += src/kda_neon.c src/simd_i8mm.c
endif
# One translation unit per x86 ISA, each built with its own flags so the
# baseline binary stays runnable on a CPU that has neither. waste_backend_init
# picks between them from CPUID, so a single binary adapts at run time —
# which is the whole reason these are separate files rather than #ifdefs
# inside model.c.
ifneq (,$(filter x86_64% amd64%,$(ARCH)))
X86SRC := src/simd_avx2.c src/simd_avx512.c
SRC += $(X86SRC)
endif
# WASTE_NATIVE=1 builds for this exact CPU, which on ARMv8.6 turns on the
# SMMLA batched matmul (still opt-in at runtime with WASTE_I8MM=1 — it
# quantizes activations to int8, so it does not produce the f32 numbers).
# The default build stays portable across ARM.
ifdef WASTE_NATIVE
CFLAGS += -mcpu=native
endif
# Accelerator backends are build-time options, and each needs a source file
# that registers it. Metal has one — src/metal.m, so WASTE_ENABLE_METAL=1
# builds and `waste version` then reports `backend Metal`. CUDA and BLAS do
# not, and before these checks existed their flags were reachable and
# produced only "Undefined symbols: _waste_register_cuda" at link time.
# Fail early and say why instead.
#
# Metal keeps its check even though it passes: the guard is about the file
# being there, not about the backend being unfinished, and it is what turns
# a deleted or unstaged src/metal.m into a sentence rather than a link
# error. Do not read the $(error) text below as the status of a backend —
# it is the message for a missing file, and Make never expands it while the
# file is present.
ifdef WASTE_ENABLE_METAL
ifeq (,$(wildcard src/metal.m))
$(error WASTE_ENABLE_METAL=1, but src/metal.m does not exist — the Metal \
backend is not implemented. Build without the flag: CPU+NEON is the only \
backend this engine has)
endif
CFLAGS += -DWASTE_ENABLE_METAL=1
OBJCSRC := src/metal.m
LDLIBS += -framework Metal -framework Foundation
endif
ifdef WASTE_ENABLE_CUDA
ifeq (,$(wildcard src/cuda.cu))
$(error WASTE_ENABLE_CUDA=1, but src/cuda.cu does not exist — the CUDA \
backend is not implemented. Build without the flag: CPU+NEON is the \
default, and Metal is the only accelerator this engine has)
endif
CFLAGS += -DWASTE_ENABLE_CUDA=1
SRC += src/cuda.cu
LDLIBS += -lcudart
endif
ifdef WASTE_ENABLE_BLAS
ifeq (,$(wildcard src/blas.c))
$(error WASTE_ENABLE_BLAS=1, but src/blas.c does not exist — the BLAS \
backend is not implemented. Build without the flag: CPU+NEON is the \
default, and Metal is the only accelerator this engine has)
endif
CFLAGS += -DWASTE_ENABLE_BLAS=1
SRC += src/blas.c
LDLIBS += -lblas
endif
OBJ := $(SRC:.c=.o) $(OBJCSRC:.m=.o)
src/metal.o: src/metal.m
$(CC) $(CFLAGS) -fobjc-arc -c -o $@ $<
# `override`, and it is load-bearing. A plain target-specific `CFLAGS +=`
# is discarded whenever CFLAGS arrives from the command line — which is
# exactly what `make asan` and `make fuzz-asan` do when they re-enter make
# with the sanitizer flags. Without it those builds compile simd_avx2.c
# with no -mavx2, and gcc then refuses to inline the always_inline AVX
# intrinsics ("target specific option mismatch") instead of doing anything
# so helpful as warning that the flag went missing. Invisible on ARM,
# where these translation units are not in SRC at all.
src/simd_avx2.o: override CFLAGS += -mavx2 -mfma
src/simd_avx512.o: override CFLAGS += -mavx512f -mavx512bw -mavx512vbmi
# FEAT_I8MM is ARMv8.6 and the portable ARM build targets something older,
# so arm_neon.h hides vmmlaq_s32 without this. Entered only after
# waste_cpu_features() reports the bit, exactly like the two above.
src/simd_i8mm.o: override CFLAGS += -march=armv8.2-a+i8mm
all: waste$(EXE) libwaste.a libwaste.$(SOEXT) libwastevq.$(SOEXT)
# `make` builds the shipped artifacts; `make test` also builds the checkers.
# They are separate targets, so remember which one you need — testing a
# stale test binary costs more time than rebuilding it.
# shared object so tools/convert.py can call the encoder through ctypes
libwastevq.$(SOEXT): src/vq.c
$(CC) $(CFLAGS) -shared $(PICFLAG) -o $@ $< -lm -lpthread $(STATIC)
libwaste.a: $(OBJ)
$(AR) rcs $@ $^
# The whole engine as a shared object. waste.h says the engine is a
# library first and the CLI is just one of its clients; serve/ is the
# other one, and it reaches the same functions through ctypes rather
# than through a second copy of the engine in Python. Built from its own
# objects because -fPIC is not in CFLAGS for the static path: mixing a
# non-PIC libwaste.a into a shared object fails to link on Linux.
SHOBJ := $(SRC:.c=.pic.o) $(OBJCSRC:.m=.pic.o)
%.pic.o: %.c
$(CC) $(CFLAGS) $(PICFLAG) -c -o $@ $<
src/metal.pic.o: src/metal.m
$(CC) $(CFLAGS) $(PICFLAG) -fobjc-arc -c -o $@ $<
src/simd_avx2.pic.o: override CFLAGS += -mavx2 -mfma
src/simd_avx512.pic.o: override CFLAGS += -mavx512f -mavx512bw -mavx512vbmi
src/simd_i8mm.pic.o: override CFLAGS += -march=armv8.2-a+i8mm
libwaste.$(SOEXT): $(SHOBJ)
$(CC) $(CFLAGS) -shared -o $@ $^ $(SHLDFLAGS) $(LDLIBS)
waste$(EXE): cli/main.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
# One list, used by both `test` and `clean`. A stale test binary is one of
# the two failures tests/run.sh was written to catch, so a binary that
# `test` builds and `clean` forgets defeats the check meant to notice it.
TESTNAMES := test_kda test_container test_forward test_tokenizer test_k3parts \
test_state test_vision test_vision_glm test_image test_memory \
test_cpus test_lock sweep
TESTBINS := $(addsuffix $(EXE),$(TESTNAMES))
test: $(TESTBINS)
test_kda$(EXE): tests/test_kda.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
# Every link rule passes LDLIBS, including the one target that does not
# currently need it. `test_image` omitted it and linked fine on macOS for
# a week: clang folded the one sqrt() in image.c, glibc did not, and the
# Linux build failed on an undefined reference the day CI first saw it.
# The one test that links no engine: it reads the container with nothing
# but the format header. crc32.c comes along because the checksum in that
# header is part of the format, and checking it against zlib's values is
# what says the two agree.
test_container$(EXE): tests/test_container.o src/crc32.o
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
test_forward$(EXE): tests/test_forward.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
# Not a check — a measurement harness. It is here because it links the same
# library the checks do and must never drift from it.
sweep$(EXE): tests/sweep.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
test_tokenizer$(EXE): tests/test_tokenizer.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
test_k3parts$(EXE): tests/test_k3parts.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
test_image$(EXE): tests/test_image.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
test_vision_glm$(EXE): tests/test_vision_glm.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
test_vision$(EXE): tests/test_vision.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
test_state$(EXE): tests/test_state.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
# Links the cgroup reader alone, not the engine: the point of the paths
# being parameters is that the policy is checkable in milliseconds against
# synthetic files, with no container and no /proc on the host to depend on.
test_memory$(EXE): tests/test_memory.o src/memory.o
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
# The pool is a header, so this links no engine at all — which is the point:
# it can check placement without a container, and the parse half of it runs
# on the platforms that cannot bind a thread.
test_cpus$(EXE): tests/test_cpus.o
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
test_lock$(EXE): tests/test_lock.o libwaste.a
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
-include $(OBJ:.o=.d) $(SHOBJ:.o=.d) cli/main.d \
$(patsubst %.c,%.d,$(wildcard tests/*.c))
clean:
rm -f $(OBJ) $(SHOBJ) cli/*.o tests/*.o $(OBJ:.o=.d) $(SHOBJ:.o=.d) \
cli/*.d tests/*.d libwaste.a waste waste.exe \
$(TESTBINS) $(TESTNAMES) $(addsuffix .exe,$(TESTNAMES)) \
libwaste.dylib libwaste.so libwaste.dll \
libwastevq.dylib libwastevq.so libwastevq.dll
rm -rf libwastevq.dylib.dSYM libwaste.dylib.dSYM
rm -rf serve/__pycache__ tests/serve/__pycache__
check: test
@tests/run.sh
# The Python server's own suite. Separate from `check` because it needs
# libwaste as a shared object rather than the archive the CLI links, and
# because it is the one part of this repo that is not C.
serve-check: libwaste.$(SOEXT)
@$(PY) -m unittest discover -s tests/serve -t . -p "test_*.py"
# Sanitizers. Separate targets rather than a flag on `make`, because they
# need a full rebuild: mixing instrumented and uninstrumented objects
# produces false reports and missed ones. ASan's own allocator refuses
# very large mappings, so these run against a synthetic container, which
# is what tests/run.sh falls back to when given a path that does not exist.
SAN_FLAGS := -fsanitize=address,undefined -fno-omit-frame-pointer \
-fno-sanitize-recover=all -g -O1
asan:
@$(MAKE) --no-print-directory clean
@$(MAKE) --no-print-directory all test \
CFLAGS="-std=gnu11 -Wall -Wextra -DVQ_SUPER=$(VQ_SUPER) -MMD -MP $(SAN_FLAGS)" \
LDLIBS="-lm -lpthread $(SAN_FLAGS)"
@rc=0; ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 \
WASTE_SANITIZED=1 \
tests/run.sh /nonexistent-container-use-synthetic || rc=$$?; \
$(MAKE) --no-print-directory clean; exit $$rc
fuzz: test
@$(PY) tools/fuzz_container.py --runs $(FUZZ_RUNS)
FUZZ_RUNS ?= 200
# What CI runs: the fuzzer against an instrumented build, so a read past
# the end of a buffer aborts instead of returning plausible garbage.
fuzz-asan:
@$(MAKE) --no-print-directory clean
@$(MAKE) --no-print-directory all test \
CFLAGS="-std=gnu11 -Wall -Wextra -DVQ_SUPER=$(VQ_SUPER) -MMD -MP $(SAN_FLAGS)" \
LDLIBS="-lm -lpthread $(SAN_FLAGS)"
@rc=0; ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 \
$(PY) tools/fuzz_container.py --runs $(FUZZ_RUNS) || rc=$$?; \
$(MAKE) --no-print-directory clean; exit $$rc
.PHONY: all test check serve-check clean asan fuzz fuzz-asan