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

  1. All three of your implementations must produce bit-identical output to cblas_sgemm (allow max relative error 1e-4 for FP32 rounding). Include a check() function that asserts this at every size.

  2. Time only the sgemm call itself, not allocation or packing. Warm up 3×, time 10×, report median.

  3. Report GFLOPS = 2·M·N·K / seconds / 1e9. Not seconds, not ms — GFLOPS.

  4. Plot: x-axis matrix size (log), y-axis GFLOPS (linear). Four lines: naive, blocked, SIMD, OpenBLAS.

  5. Pin to one core: taskset -c 0 ./bench on Linux, or single-threaded OpenBLAS via OPENBLAS_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 hw on Mac, lscpu on 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.c variant: symmetric quantized matmul using AVX-VNNI _mm256_dpbusd_epi32 or NEON vdotq_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

  1. Accept a numpy.ndarray (contiguous, C-order, dtype float32). Reject non-contiguous with PyErr_SetString(PyExc_ValueError, ...).

  2. Use the buffer protocol or NumPy’s C API — zero-copy input. No PyArg_ParseTuple("O", ...) followed by a scan.

  3. Release the GIL around the tight loop with Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS.

  4. Compile with -O3 -march=native. Auto-vectorization is fine; explicit intrinsics (AVX2 or NEON) is bonus.

  5. 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 quantize_ext

Pure Python for-loop

200–1000× slower

NumPy vectorized reference

~1–3× slower (NumPy is already fast)

quantize_ext (scalar C, -O3)

baseline

quantize_ext (SIMD C)

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 cibuildwheel green 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_0 from 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 LICENSE file (MIT is fine).

  • A .gitignore that excludes build artifacts, .so files, __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/ or notes/ 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