Day 10 · Overview
Week 2FNX 101 · 10Demo Day
Assemble all seven modules into one running Studio, present it with honest model cards, and run the Grand Benchmark that reveals the operation beneath everything.
Today you will
3- Assemble every module and launch the full AI Studio app
- Rehearse a great demo and write an honest model card
- Run the Grand Benchmark and meet C = A @ B, the operation under all of AI
Today's items
4 hLab
FNX 101 · 10Day 10 — Demo Day + The Road to HPC 🎓
Mission briefing: Ten days ago you walked into the Lab knowing basic Python. Today you walk out an AI explorer with a working AI Studio you built by hand — seven capabilities, one per day. Demo Day has three acts: assemble & launch the whole Studio, rehearse your demo, and then run the Grand Benchmark — where we reveal the single operation beneath everything you've built, and why making it fast is the next great adventure.
Previously in the Lab: yesterday you unlocked the 🎮 Game Master and trained two agents with reinforcement learning — a Q-table and a Deep Q-Network.
Today you will:
- Act I — assemble every module and launch the full AI Studio app
- Act II — rehearse a great demo and write an honest model card
- Act III — run the Grand Benchmark and meet
C = A @ B, the operation under all of AI - See why high-performance computing is the natural next chapter of your story
Today you unlock: 🎓 nothing new to build — today you ship everything you already made.
A precise reading
The classical dense matmul counts about 2n³ floating-point operations, but measured timing curves need not have slope 3 at every size. Compare the same size and precision, warm up, synchronize the GPU, and state whether transfer time is included. Speedups are measurements, not guarantees. Matrix products power many neural layers; tabular Q-learning is a scalar update, and other operations still matter.
# ╔═══════════════════════════════════════════════════════════════════╗
# ║ SETUP — run me first! (identical in every Lab notebook) ║
# ╚═══════════════════════════════════════════════════════════════════╝
import os, sys, pathlib
IN_COLAB = "google.colab" in sys.modules
SMOKE = os.environ.get("SMOKE_TEST", "0") == "1" # tiny fast run for automated tests
REPO_URL = "https://github.com/A-Halimi/ai-studio-internship.git" # instructor: set once
if IN_COLAB:
if not pathlib.Path("/content/ai-studio-internship").exists():
!git clone -q {REPO_URL} /content/ai-studio-internship
%cd /content/ai-studio-internship/notebooks/day10 # ← ADAPT day folder
%pip -q install gradio==6.19.0 # ← ADAPT per-day installs
HERE = pathlib.Path.cwd()
REPO = HERE.parents[1] # .../notebooks/dayNN -> repo root
DATA_DIR = pathlib.Path(os.environ.get("COURSE_DATA_DIR", REPO / "data"))
SAMPLES_DIR = REPO / "data_samples" # small datasets shipped with the repo
MODELS_DIR = REPO / "ai_studio" / "models" # where your Studio modules unlock
for p in (REPO / "data", MODELS_DIR):
p.mkdir(parents=True, exist_ok=True)
import random
import numpy as np
SEED = 42
random.seed(SEED); np.random.seed(SEED)
print(f"✅ Setup done | Colab: {IN_COLAB} | Smoke test: {SMOKE}")
print(f" data: {DATA_DIR}")import torch
torch.manual_seed(SEED)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if DEVICE.type == "cuda":
print(f"⚡ Running on GPU: {torch.cuda.get_device_name(0)}")
else:
print("🐢 No GPU found — running on CPU. Everything still works, just slower.")# ── CONFIG — the Lab's control panel for today's Grand Benchmark ──
PURE_SIZES = [32, 64] if SMOKE else [32, 64, 128] # pure-Python matmul
NP_SIZES = [64, 128, 256] if SMOKE else [64, 128, 256, 512, 1024, 2048] # NumPy & GPU matmul
print("pure-Python sizes:", PURE_SIZES)
print("NumPy / GPU sizes:", NP_SIZES)🎬 Act I — Assemble the Studio
Roll call
Your AI Studio is a small app (ai_studio/) that scans its modules/ folder and shows one tab per capability. A module lights up only when the files it needs — the ones each day's notebook saved — are present. Let's import the Studio's own code and take attendance.
sys.path.insert(0, str(REPO))
from ai_studio.studio import load_modules, missing_files, MODELS_DIR as STUDIO_MODELS
modules = load_modules()
n_unlocked = sum(1 for m in modules if not m["error"] and not missing_files(m["requires"]))
print(f"AI STUDIO — ROLL CALL ({n_unlocked}/{len(modules)} modules unlocked)")
print("=" * 64)
for m in modules:
day = m["name"].split("_")[0] # e.g. 'day05'
miss = missing_files(m["requires"])
if m["error"]:
status = "⚠️ crashed on load"
elif miss:
status = "🔒 locked — missing: " + ", ".join(miss)
else:
status = "✅ unlocked"
print(f" {m['title']:<24} [{day}] {status}")
if miss and not m["error"]:
print(f" → finish the {day} notebook to unlock it")
print("=" * 64)
print(f"Models live in: {STUDIO_MODELS}")🧪 Exercise 1 — how big are your models?
The roll call tells you which modules are ready. Now show how heavy each one is. For every unlocked module, print each required file with its size.
Use path.stat().st_size — it returns a file's size in bytes. Divide by 1024 for kilobytes. (Notice how tiny some are: the whole Q-table is a few hundred bytes, while a fine-tuned network is megabytes.)
print("Files behind each unlocked module:")
for m in modules:
if m["error"] or missing_files(m["requires"]):
continue
print(f"\n{m['title']}:")
for fname in m["requires"]:
path = STUDIO_MODELS / fname
# ==================== YOUR CODE HERE ====================
### HINT: path.stat().st_size gives the size in bytes
...
print(f" {fname:<34} {size_kb:9.1f} KB")Launch the Studio 🚀
One line assembles the whole app from its modules and serves it. Locally it opens at http://127.0.0.1:7860; on Colab it prints a public share link. Take a lap through every tab — a slider that names penguins, a canvas that reads your handwriting, a camera that plays rock-paper-scissors, agents that learned from rewards. You built all of it.
if not SMOKE:
from ai_studio.studio import build_demo
demo = build_demo()
demo.launch(share=IN_COLAB)
else:
print("Smoke test: skipping the live Studio launch (it would block forever).")🎬 Act II — Tell the story well
A demo is not a code dump — it's a story. The best researchers make people feel why the work matters in 3 minutes. Here's a skeleton you can rehearse from:
- The hook (1 sentence). What AI is not — magic — and what it is: patterns learned from data. Open with that reframe.
- The tour (pick 2 favorites). Don't show all seven. Demo the two modules you're proudest of, live. Let people try them.
- The hardest bug. Tell the story of the nastiest thing that broke and how you chased it down. Struggle is the interesting part.
- The surprise. One thing that genuinely surprised you — a weird failure, an unexpected success.
Real labs also publish a model card for every model they release: a short, honest fact sheet. Let's write one.
def render_model_card(card):
"""Pretty-print a model-card dict as a tidy text card."""
fields = [("task", "Task"), ("training_data", "Trained on"),
("accuracy_or_quality", "How good"),
("limitations", "Limitations"),
("surprising_behavior", "Surprised me")]
rule = "═" * 58
lines = [rule, f" 📋 MODEL CARD — {card.get('name', '?')}", rule]
for key, label in fields:
lines.append(f" {label:<12}: {card.get(key, '—')}")
lines.append(rule)
return "\n".join(lines)
print("Pretty-printer ready — fill in your card in the next cell.")🧪 Exercise 2 — write an honest model card
Pick your favorite module and fill in its card. Be honest — especially about limitations and surprising behavior. In science, stating what your model can't do is a feature, not an admission of defeat.
Fields: name, task, training_data, accuracy_or_quality, limitations, surprising_behavior.
# ==================== YOUR CODE HERE ====================
### HINT: pick YOUR favorite module and fill in every field honestly
...
print(render_model_card(my_model_card))🔎 Why model cards matter: a model that is 99% accurate on MNIST may be useless on digits photographed in the wild. Naming the training data and the limitations is how honest labs prevent their work from being misused. Limitations aren't the fine print — they're part of the result.
🎬 Act III — The Grand Benchmark ⚙️
One operation to rule them all
Time for the reveal. Look back at everything you built and ask: what is actually being computed?
- Day 2 — kNN: distances between feature vectors → vector math.
- Day 4 — you built
matmulby hand: the triple loop, from scratch. - Days 5–7 — every neural network layer:
output = weights @ input + bias→ matrix multiply. - Day 8 — attention in transformers: queries × keys × values → matrix multiplies.
- Day 9 — the DQN: every forward pass through the network → matrix multiplies.
Strip away the labels — penguins, pixels, words, rewards — and underneath it is the same operation, over and over:
Modern AI is, to a first approximation, matrix multiplication at enormous scale. So the most important question in high-performance computing is simply: how fast can we compute A @ B? Let's find out — three ways.
🧪 Exercise 3 — rebuild matmul from memory
You wrote this on Day 4. Write it again, from memory, with the classic triple loop: each entry C[i][j] is the dot product of row i of A with column j of B. The assert checks you against NumPy on a 32×32 problem.
def matmul_by_hand(A, B):
"""Multiply two 2D lists with three nested loops (the Day 4 way)."""
n, k, m = len(A), len(B), len(B[0]) # A is n x k, B is k x m, C is n x m
C = [[0.0] * m for _ in range(n)]
# ==================== YOUR CODE HERE ====================
### HINT: C[i][j] is the dot product of row i of A and column j of B
...
return C
rng = np.random.default_rng(SEED)
A = rng.standard_normal((32, 32))
B = rng.standard_normal((32, 32))
C_hand = np.array(matmul_by_hand(A.tolist(), B.tolist()))
assert np.allclose(C_hand, A @ B), "Your matmul doesn't match NumPy — check the loops!"
print("✅ matmul_by_hand matches NumPy on 32x32 — the triple loop is correct.")🧪 Exercise 4 — time the hand-written version
How long does your pure-Python triple loop take as the matrices grow? Time it on each size in PURE_SIZES and record the seconds.
Use time.perf_counter() right before and after the call, then subtract. Even at 128×128 you'll feel it — that's ~2 million multiply-adds in interpreted Python.
import time
pure_times = []
for n in PURE_SIZES:
A = rng.standard_normal((n, n)).tolist()
B = rng.standard_normal((n, n)).tolist()
# ==================== YOUR CODE HERE ====================
### HINT: t = time.perf_counter() - t0, where t0 was read just before the call
...
pure_times.append(t)
print(f" pure Python {n:>5} x {n:<5} {t*1000:12.1f} ms")The same job, industrial-strength
NumPy hands the multiply to BLAS — decades-old, hyper-optimized C and assembly that respects your CPU's cache and vector units. PyTorch on a GPU hands it to thousands of cores at once. Same math, wildly different engineering. We time NumPy (best of 3 runs) and, if a GPU is present, PyTorch too.
def _time_once(fn):
t0 = time.perf_counter(); fn(); return time.perf_counter() - t0
# NumPy on the CPU (best of 3 runs)
numpy_times = []
for n in NP_SIZES:
A = rng.standard_normal((n, n))
B = rng.standard_normal((n, n))
best = min(_time_once(lambda: A @ B) for _ in range(3))
numpy_times.append(best)
print(f" NumPy {n:>5} x {n:<5} {best*1000:12.3f} ms")
# GPU with PyTorch (only if we actually have one)
gpu_sizes, gpu_times = [], []
if DEVICE.type == "cuda":
for n in NP_SIZES:
A = torch.randn(n, n, device=DEVICE)
B = torch.randn(n, n, device=DEVICE)
for _ in range(3): # warm-up (first call is slow)
_ = A @ B
torch.cuda.synchronize() # wait for the GPU to actually finish
t0 = time.perf_counter()
for _ in range(10):
_ = A @ B
torch.cuda.synchronize()
t = (time.perf_counter() - t0) / 10
gpu_sizes.append(n); gpu_times.append(t)
print(f" GPU {n:>5} x {n:<5} {t*1000:12.3f} ms")
else:
print(" (No CUDA GPU here — skipping the GPU line. The story still works on CPU!)")🧪 Exercise 5 — the money plot 📈
Draw all three lines on one log-log chart: time (seconds) versus matrix size n. Plot pure_times over PURE_SIZES, numpy_times over NP_SIZES, and — if you measured it — gpu_times over gpu_sizes.
plt.loglog(sizes, times, "o-", label=...) draws one line. Label the axes and add a legend.
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 6))
# ==================== YOUR CODE HERE ====================
### HINT: one plt.loglog(...) call per line; pass label= so the legend fills in
...
plt.xlabel("matrix size n (multiplying n x n matrices)")
plt.ylabel("time (seconds)")
plt.title("The same multiplication, three ways")
plt.legend()
plt.grid(True, which="both", alpha=0.3)
plt.show()Operation counts and measured slopes
The classical triple loop uses approximately n³ multiply-adds, or 2n³ floating-point operations. Doubling n multiplies this work by about 8; tripling it by 27. This is O(n³) work.
Wall-clock timing is different: startup overhead, cache, parallelism and hardware utilization can bend the curves. A log-log slope near 3 is a possible large-size regime, not a requirement for all three measured lines. Compare speedups only at matching sizes and precision.
🧪 Exercise 6 — put a number on the gap
At the largest size measured by both the pure-Python and NumPy runs, compute two numbers:
- speedup = (pure-Python time) ÷ (NumPy time) — how many times faster NumPy is.
- GFLOP/s = — billions of floating-point operations per second (a matmul does about of them).
n_common = max(set(PURE_SIZES) & set(NP_SIZES)) # biggest size both methods ran
t_py = pure_times[PURE_SIZES.index(n_common)]
t_np = numpy_times[NP_SIZES.index(n_common)]
# ==================== YOUR CODE HERE ====================
### HINT: a matmul does ~2*n^3 flops; GFLOP/s = (2*n^3) / time / 1e9
...
print(f"At {n_common} x {n_common}, the SAME multiplication:")
print(f" pure Python : {t_py*1000:12.1f} ms")
print(f" NumPy : {t_np*1000:12.3f} ms → {speedup:,.0f}x faster, {gflops_np:,.1f} GFLOP/s")
if gpu_sizes and n_common in gpu_sizes:
t_gpu = gpu_times[gpu_sizes.index(n_common)]
gflops_gpu = 2 * n_common**3 / t_gpu / 1e9
print(f" GPU : {t_gpu*1000:12.3f} ms → {t_py/t_gpu:,.0f}x faster than pure Python, {gflops_gpu:,.1f} GFLOP/s")Same formula. Same answer. A factor of thousands — sometimes ~10,000× — in speed. Nobody changed the math. They changed how the hardware runs it: memory layout, cache blocking, vectorized instructions, and thousands of GPU cores working in parallel.
That is exactly what high-performance computing is about. Keep this plot. When you meet HPC for real, your mission: take the gap you just measured and close it yourself.
Why efficient computation matters
Neural inference combines matrix products with activations, normalization, memory access and other work. Its cost depends on the architecture, context length and generated token count; one FLOP is not one whole matrix multiplication.
At large scale, improving kernels, memory movement and batching can lower latency and cost. Use the experiment to identify which part of a measured workload is limiting performance, rather than assuming every operation is A @ B.
🎓 Graduation
Put your name in and run the cell. You earned this.
YOUR_NAME = "Your Name Here" # ← put your name here!
MODULES_BUILT = [
"Prediction Machine (Day 2 - supervised learning)",
"Pattern Finder (Day 3 - clustering)",
"Digit Reader (Day 5 - neural networks)",
"Photo Detective (Day 6 - convolutional networks)",
"RPS Arena (Day 7 - transfer learning)",
"Language Lab (Day 8 - transformers)",
"Game Master (Day 9 - reinforcement learning)",
]
def certificate(name):
W = 58
line = "=" * W
def center(s):
return "|" + s.center(W) + "|"
out = ["+" + line + "+", center(""), center("* CERTIFICATE OF COMPLETION *"), center("")]
out += [center("This certifies that"), center(""), center(name.upper()), center("")]
out += [center("is now a"), center("CERTIFIED AI EXPLORER"),
center("the Lab - Class of 2026"), center("")]
out += ["+" + line + "+", center(""), center("Seven modules, built by hand:"), center("")]
for m in MODULES_BUILT:
out.append("| " + m.ljust(W - 2) + "|")
out += [center(""), "+" + line + "+"]
return "\n".join(out)
print(certificate(YOUR_NAME))🏁 The whole journey
Look how far you came — one concept, one working module, every single day:
- Day 1 — What is AI? You met models as patterns learned from data, not magic.
- Day 2 — Supervised learning → 🐧 Prediction Machine (classify penguins).
- Day 3 — Unsupervised learning → 🎨 Pattern Finder (cluster colors with k-means).
- Day 4 — Neural nets from scratch — you built
matmuland a network in pure NumPy. (Today you saw why that operation matters so much.) - Day 5 — PyTorch & MNIST → ✍️ Digit Reader (a network reads your handwriting).
- Day 6 — Convolutional networks → 🕵️ Photo Detective (see the world in 32×32).
- Day 7 — Transfer learning → 🪨📄✂️ RPS Arena (fine-tune a pretrained vision model).
- Day 8 — Transformers → 💬 Language Lab (generate, classify, and fill in text).
- Day 9 — Reinforcement learning → 🎮 Game Master (agents that learn from rewards).
- Day 10 — Demo Day → you shipped the Studio and found
A @ Bunder all of it.
Week 1, you learned how machines learn. Week 2, you gave them eyes, words, and instincts. And you just discovered the one operation they all run on — and how staggeringly its speed can vary.
Next: make them fast. The road leads into high-performance computing — squeezing every last FLOP out of the hardware. See you on the other side of the matmul. 🚀
— the Lab