Phase 6 · Projects¶
Two deliverables. Both are hard. Both go on your GitHub, both get a README with numbers, and both are the tangible proof at end-M11 that you are the person the M13 pitch sentence describes. If you skip these, everything before them is theory. Budget: 40–50 hours total across the phase.
Project 1 · gemm_bench — naive → blocked → SIMD matmul vs OpenBLAS¶
Repo: github.com/<you>/gemm_bench · Language: C11 · Effort: 20–25 hours
Deliverable¶
A single-file-per-implementation matmul benchmark suite with three C source files and one plot. All three compute C = A × B for FP32 row-major matrices, size swept over 64, 128, 256, 512, 1024, 2048.
Files¶
gemm_bench/
├── src/
│ ├── gemm_naive.c # rung 1: ijk triple loop
│ ├── gemm_blocked.c # rung 3: cache-blocked, from 04_matmul_and_gemm.md
│ ├── gemm_simd.c # rung 4: AVX2 or NEON micro-kernel
│ ├── gemm_openblas.c # calls cblas_sgemm
│ └── bench.c # driver, timing, correctness check
├── Makefile
├── plot.py # matplotlib GFLOPS chart
├── results.png
└── README.md # setup, numbers, analysis paragraph
Rules¶
All three of your implementations must produce bit-identical output to
cblas_sgemm(allow max relative error 1e-4 for FP32 rounding). Include acheck()function that asserts this at every size.Time only the
sgemmcall itself, not allocation or packing. Warm up 3×, time 10×, report median.Report GFLOPS = 2·M·N·K / seconds / 1e9. Not seconds, not ms — GFLOPS.
Plot: x-axis matrix size (log), y-axis GFLOPS (linear). Four lines: naive, blocked, SIMD, OpenBLAS.
Pin to one core:
taskset -c 0 ./benchon Linux, or single-threaded OpenBLAS viaOPENBLAS_NUM_THREADS=1.
Target numbers (single-core, ~3 GHz)¶
Impl |
1024×1024 GFLOPS |
% of OpenBLAS |
|---|---|---|
Naive |
0.5–2 |
1–4% |
Blocked (no SIMD) |
5–15 |
10–25% |
Blocked + SIMD |
40–80 |
60–85% |
OpenBLAS |
60–100 |
100% |
If your SIMD version is <4× your -O3 naive, keep tuning. If it’s >8×, you are in professional territory.
README must contain¶
Machine specs: CPU model, cache sizes (
sysctl hwon Mac,lscpuon Linux), memory bandwidth if known.Compiler + flags used.
The plot.
A paragraph explaining the gap to OpenBLAS. Register blocking? Packing overhead? Kernel-per-µarch dispatch? Do not hand-wave — name the specific technique OpenBLAS uses that you didn’t.
A one-sentence honest reflection on what surprised you.
Stretch goals (optional, +10 h)¶
Add OpenMP parallelization to the outer loop of
gemm_simd.c. Plot speedup vs core count.Add a
gemm_int8.cvariant: symmetric quantized matmul using AVX-VNNI_mm256_dpbusd_epi32or NEONvdotq_s32. Bridge to Project 2.
Project 2 · quantize_ext — Python C extension for INT8 quant/dequant¶
Repo: github.com/<you>/quantize_ext · Language: C11 + Python · Effort: 15–20 hours
Deliverable¶
A pip-installable Python extension that quantizes an FP32 NumPy array to INT8 (symmetric, per-tensor) and dequantizes it back, with a benchmark against a pure-NumPy reference and a pure-Python reference.
Files¶
quantize_ext/
├── src/
│ └── quantize_module.c # the CPython extension
├── quantize_ext/
│ └── __init__.py # re-exports + a Python reference impl
├── tests/
│ ├── test_correctness.py # compare vs NumPy ref
│ └── test_bench.py # timing harness
├── setup.py # or pyproject.toml + setuptools
├── pyproject.toml
├── .github/workflows/ci.yml # cibuildwheel on Linux + macOS
└── README.md
API¶
import numpy as np
from quantize_ext import quantize_int8, dequantize_int8
x = np.random.randn(1024, 1024).astype(np.float32)
q, scale = quantize_int8(x) # q: int8 array, scale: float
x_hat = dequantize_int8(q, scale) # float32 array
assert np.abs(x - x_hat).max() < scale # bounded per-element error
Rules¶
Accept a
numpy.ndarray(contiguous, C-order, dtypefloat32). Reject non-contiguous withPyErr_SetString(PyExc_ValueError, ...).Use the buffer protocol or NumPy’s C API — zero-copy input. No
PyArg_ParseTuple("O", ...)followed by a scan.Release the GIL around the tight loop with
Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS.Compile with
-O3 -march=native. Auto-vectorization is fine; explicit intrinsics (AVX2 or NEON) is bonus.Bench against a NumPy one-liner (
(x / scale).round().clip(-128, 127).astype(np.int8)) and a pure-Python loop. Report all three.
Target speedup¶
Baseline |
Expected ratio vs |
|---|---|
Pure Python for-loop |
200–1000× slower |
NumPy vectorized reference |
~1–3× slower (NumPy is already fast) |
|
baseline |
|
1.5–2.5× faster than NumPy on large arrays |
If your extension is slower than the NumPy one-liner, you have a bug — probably a copy in the input path. Profile with py-spy record --native.
README must contain¶
pip install .install instructions.The benchmark table with real numbers on your machine.
A section explaining reference counting and GIL release with the exact lines of code that manage each.
CI badge showing
cibuildwheelgreen on Linux + macOS (universal2).
Stretch goals (optional, +10 h)¶
Add block-wise Q4_0-style quantization (32-element blocks, 4-bit packed nibbles, FP16 scale per block). Match the layout of
block_q4_0from 05_quantization_kernels_in_c.md. This is 80% of what ggml does at the leaf.Publish the wheel to TestPyPI (not real PyPI — the name is generic).
Write a 600-word Zoho-internal blog post explaining what you built and why the team could use it. Draft on your laptop, don’t publish, keep for Phase 7 · projects.md.
Both projects must have¶
A
LICENSEfile (MIT is fine).A
.gitignorethat excludes build artifacts,.sofiles,__pycache__,build/,*.o.A commit history that shows progression — not one “initial commit” with 500 lines. Small commits with meaningful messages. This is what a hiring manager checks.
A
docs/ornotes/folder with your rough working notes. This is where you’ll refer back in M12–M13 when you write the blog post required in Phase 7 projects.
Exit gate¶
You may not enter Phase 7 until both repos are public, both READMEs contain real numbers, and you’ve asked at least one other engineer (colleague, friend, or a random senior on r/C_Programming) to read one of them. Feedback is part of the deliverable. Ship it.
Return to README.md · Next: ../08_production_and_mastery/README.md