Merge feature/tinytorch-core: fix Jupyter kernel mismatch (#1147)

This commit is contained in:
Vijay Janapa Reddi
2026-02-04 10:28:52 -05:00
12 changed files with 519 additions and 159 deletions

View File

@@ -6,7 +6,12 @@
# Uses Ollama LLM to parse natural language and extract:
# - GitHub username (with or without @)
# - Contribution type(s)
# - Target project (optional, defaults based on issue context)
#
# Project detection is DETERMINISTIC (not LLM-guessed):
# - PR file paths: tinytorch/ → tinytorch, book/ → book, kits/ → kits, labs/ → labs
# - Explicit mention in comment: "in tinytorch", "for kits", etc.
# - Issue labels/title context
# - If none of the above → asks the user (never silently defaults)
#
# Flexible formats - all of these work:
# @all-contributors @username helped verify the fix worked
@@ -34,9 +39,8 @@ env:
# Valid contribution types (comma-separated)
CONTRIBUTION_TYPES: 'bug,code,doc,design,ideas,review,test,tool'
# Valid projects (comma-separated, first one is default)
# Valid projects (comma-separated)
PROJECTS: 'book,tinytorch,kits,labs'
DEFAULT_PROJECT: 'book'
# Project aliases (format: alias1:project1,alias2:project2)
PROJECT_ALIASES: 'tito:tinytorch'
@@ -54,9 +58,15 @@ jobs:
pull-requests: write
steps:
- name: Extract trigger line and context
# =====================================================================
# STEP 1: Extract trigger line + detect project from PR files
# =====================================================================
- name: Extract trigger line and detect project
id: extract
uses: actions/github-script@v7
env:
PROJECTS: ${{ env.PROJECTS }}
PROJECT_ALIASES: ${{ env.PROJECT_ALIASES }}
with:
script: |
const body = context.payload.comment.body;
@@ -73,19 +83,103 @@ jobs:
console.log('Trigger line:', triggerLine);
// Get issue context for project detection
// --- Configuration ---
const validProjects = process.env.PROJECTS.split(',');
const projectAliases = {};
if (process.env.PROJECT_ALIASES) {
process.env.PROJECT_ALIASES.split(',').forEach(pair => {
const [alias, proj] = pair.split(':');
if (alias && proj) projectAliases[alias.trim()] = proj.trim();
});
}
// --- Helper: detect project name in text ---
const detectProjectInText = (text) => {
const lower = text.toLowerCase();
for (const p of validProjects) {
if (lower.includes(p)) return p;
}
for (const [alias, proj] of Object.entries(projectAliases)) {
if (lower.includes(alias)) return proj;
}
return null;
};
// --- Get issue/PR context ---
const issue = context.payload.issue;
const labels = issue.labels.map(l => l.name.toLowerCase());
const title = issue.title.toLowerCase();
const issueBody = issue.body ? issue.body.toLowerCase() : '';
// Build context string for LLM
const issueContext = `Issue title: ${issue.title}\nLabels: ${labels.join(', ') || 'none'}`;
// =============================================================
// PROJECT DETECTION (deterministic, priority order)
// =============================================================
let project = null;
let projectSource = 'unknown';
// Priority 1: Explicit mention in the trigger comment
const commentProject = detectProjectInText(triggerLine);
if (commentProject) {
project = commentProject;
projectSource = 'comment';
console.log(`Project from comment text: "${project}"`);
}
// Priority 2: PR changed files (top-level dir → project)
if (!project && issue.pull_request) {
try {
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: issue.number,
per_page: 100
});
const projectCounts = {};
for (const file of files) {
const topDir = file.filename.split('/')[0];
if (validProjects.includes(topDir)) {
projectCounts[topDir] = (projectCounts[topDir] || 0) + 1;
}
}
const detected = Object.keys(projectCounts);
console.log('PR file project counts:', JSON.stringify(projectCounts));
if (detected.length === 1) {
project = detected[0];
projectSource = 'pr_files';
console.log(`Project from PR files: "${project}"`);
} else if (detected.length > 1) {
projectSource = 'ambiguous';
console.log('PR spans multiple projects:', detected.join(', '));
}
} catch (e) {
console.log('Could not fetch PR files:', e.message);
}
}
// Priority 3: Issue labels / title
if (!project) {
const contextProject = detectProjectInText(issueContext);
if (contextProject) {
project = contextProject;
projectSource = 'issue_context';
console.log(`Project from issue context: "${project}"`);
}
}
// If still null → projectSource stays 'unknown', handled downstream
console.log(`Final project: ${project || 'NONE'} (source: ${projectSource})`);
core.setOutput('should_run', 'true');
core.setOutput('trigger_line', triggerLine);
core.setOutput('issue_context', issueContext);
core.setOutput('project', project || '');
core.setOutput('project_source', projectSource);
# =====================================================================
# STEP 2: LLM parses username + contribution types (NOT project)
# =====================================================================
- name: Parse with LLM
if: steps.extract.outputs.should_run == 'true'
uses: ai-action/ollama-action@v2
@@ -93,15 +187,10 @@ jobs:
with:
model: ${{ env.LLM_MODEL }}
prompt: |
Parse this contributor recognition comment and extract the required information.
Parse this contributor recognition comment. Extract ONLY the username and contribution types.
COMMENT: ${{ steps.extract.outputs.trigger_line }}
ISSUE CONTEXT:
${{ steps.extract.outputs.issue_context }}
TASK: Extract the GitHub username and contribution type(s) from the comment.
CONTRIBUTION TYPES (pick one or more):
- bug: Found or reported a bug, identified issues
- code: Wrote code, implemented features, fixed bugs
@@ -112,156 +201,128 @@ jobs:
- test: Tested features, verified fixes, QA testing
- tool: Built tools, scripts, automation, CLI utilities
PROJECT OPTIONS: book, tinytorch, kits, labs
- IMPORTANT: Look for project names in the comment! Phrases like "in tinytorch", "in Tinytorch", "for tinytorch", "tinytorch contributor" indicate the project
- Also check issue context (title/labels) for project hints
- Default to "book" ONLY if no other project is mentioned anywhere
OUTPUT FORMAT - Return ONLY a JSON object with exactly these 3 fields:
Return ONLY a JSON object with exactly these 2 fields:
{
"username": "<github-username-without-@>",
"types": ["<contribution-type>"],
"project": "<project-name>"
"types": ["<contribution-type>"]
}
FIELD REQUIREMENTS:
- username (string, required): GitHub username WITHOUT the @ symbol
- types (array of strings, required): One or more of: bug, code, doc, design, ideas, review, test, tool
- project (string, required): One of: book, tinytorch, kits, labs
RULES:
- username: The GitHub username WITHOUT the @ symbol. Ignore @all-contributors itself.
- types: Array of one or more contribution types from the list above.
- Do NOT include a "project" field. Project is detected separately.
EXAMPLES:
Input: "@all-contributors @jane-doe fixed typos"
Output: {"username": "jane-doe", "types": ["doc"], "project": "book"}
Input: "@all-contributors @jane-doe fixed typos in the documentation"
Output: {"username": "jane-doe", "types": ["doc"]}
Input: "@all-contributors please add @ngbolin for Doc in Tinytorch"
Output: {"username": "ngbolin", "types": ["doc"], "project": "tinytorch"}
Input: "@all-contributors @user123 helped verify the fix worked in tinytorch"
Output: {"username": "user123", "types": ["test"], "project": "tinytorch"}
Output: {"username": "ngbolin", "types": ["doc"]}
Input: "@all-contributors @dev42 implemented the new feature and wrote tests"
Output: {"username": "dev42", "types": ["code", "test"], "project": "book"}
Output: {"username": "dev42", "types": ["code", "test"]}
Input: "@all-contributors please add @user123 for code"
Output: {"username": "user123", "types": ["code"]}
Return ONLY the JSON object, no explanation or other text.
- name: Parse LLM response
# =====================================================================
# STEP 3: Parse LLM JSON + combine with deterministic project
# =====================================================================
- name: Parse LLM response and validate
if: steps.extract.outputs.should_run == 'true'
id: parse
uses: actions/github-script@v7
env:
LLM_RESPONSE: ${{ steps.llm.outputs.response }}
TRIGGER_LINE: ${{ steps.extract.outputs.trigger_line }}
ISSUE_CONTEXT: ${{ steps.extract.outputs.issue_context }}
PROJECT: ${{ steps.extract.outputs.project }}
PROJECT_SOURCE: ${{ steps.extract.outputs.project_source }}
CONTRIBUTION_TYPES: ${{ env.CONTRIBUTION_TYPES }}
PROJECTS: ${{ env.PROJECTS }}
DEFAULT_PROJECT: ${{ env.DEFAULT_PROJECT }}
PROJECT_ALIASES: ${{ env.PROJECT_ALIASES }}
with:
script: |
const response = process.env.LLM_RESPONSE || '';
const triggerLine = process.env.TRIGGER_LINE || '';
const issueContext = process.env.ISSUE_CONTEXT || '';
console.log('LLM response:', response);
// Load configuration from environment
const project = process.env.PROJECT || '';
const projectSource = process.env.PROJECT_SOURCE || '';
const validTypes = process.env.CONTRIBUTION_TYPES.split(',');
const validProjects = process.env.PROJECTS.split(',');
const defaultProject = process.env.DEFAULT_PROJECT;
// Parse project aliases (format: alias1:project1,alias2:project2)
const projectAliases = {};
if (process.env.PROJECT_ALIASES) {
process.env.PROJECT_ALIASES.split(',').forEach(pair => {
const [alias, project] = pair.split(':');
if (alias && project) projectAliases[alias] = project;
});
}
console.log('LLM response:', response);
console.log('Deterministic project:', project || 'NONE', `(source: ${projectSource})`);
let username = null;
let types = [];
let project = defaultProject;
// --- Parse LLM JSON response ---
try {
// Find JSON in response (LLM might add extra text)
const jsonMatch = response.match(/\{[\s\S]*?\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
// Extract username
if (parsed.username && typeof parsed.username === 'string') {
username = parsed.username.replace(/^@/, ''); // Remove @ if present
username = parsed.username.replace(/^@/, '');
}
// Extract types
if (parsed.types && Array.isArray(parsed.types)) {
types = parsed.types.filter(t => validTypes.includes(t.toLowerCase())).map(t => t.toLowerCase());
}
// Extract project
if (parsed.project && validProjects.includes(parsed.project.toLowerCase())) {
project = parsed.project.toLowerCase();
types = parsed.types
.map(t => t.toLowerCase().trim())
.filter(t => validTypes.includes(t));
}
}
} catch (e) {
console.log('Failed to parse JSON:', e.message);
console.log('Failed to parse LLM JSON:', e.message);
}
// Fallback: try to extract username from trigger line if LLM failed
// --- Fallback: extract username from @mentions ---
if (!username) {
const usernameMatch = triggerLine.match(/@(\w[-\w]*)/g);
if (usernameMatch && usernameMatch.length > 1) {
// Skip @all-contributors, take the next @mention
username = usernameMatch[1].replace(/^@/, '');
const mentions = triggerLine.match(/@([\w][\w-]*)/g);
if (mentions && mentions.length > 1) {
username = mentions[1].replace(/^@/, '');
console.log('Username from @mention fallback:', username);
}
}
// Fallback: detect project from trigger line or issue context if not in LLM response
// Priority: trigger line first (explicit user request), then issue context
if (project === defaultProject) {
const triggerLower = triggerLine.toLowerCase();
const contextLower = issueContext.toLowerCase();
// Helper function to detect project in text
const detectProject = (text) => {
// Check for direct project names (except default)
for (const p of validProjects) {
if (p !== defaultProject && text.includes(p)) return p;
}
// Check for aliases
for (const [alias, targetProject] of Object.entries(projectAliases)) {
if (text.includes(alias)) return targetProject;
}
return null;
};
// Check trigger line first, then issue context
const detected = detectProject(triggerLower) || detectProject(contextLower);
if (detected) project = detected;
}
console.log('Username:', username);
console.log('Types:', types);
console.log('Project:', project);
// --- Validate username ---
if (!username) {
console.log('Could not determine username');
core.setOutput('success', 'false');
core.setOutput('error', 'no_username');
return;
}
// --- Validate types ---
if (types.length === 0) {
console.log('Could not determine contribution type');
core.setOutput('success', 'false');
core.setOutput('error', 'no_types');
core.setOutput('username', username);
return;
}
// --- Validate project (deterministic — already resolved in Step 1) ---
if (!project || !validProjects.includes(project)) {
console.log('No valid project detected — will ask user');
core.setOutput('success', 'false');
core.setOutput('error', 'no_project');
core.setOutput('username', username);
core.setOutput('types', JSON.stringify(types));
core.setOutput('project_source', projectSource);
return;
}
// --- All good ---
console.log('✅ Final result:', { username, types, project, projectSource });
core.setOutput('success', 'true');
core.setOutput('username', username);
core.setOutput('types', JSON.stringify(types));
core.setOutput('project', project);
core.setOutput('project_source', projectSource);
# =====================================================================
# STEP 4: Checkout, update config, generate READMEs, commit
# =====================================================================
- name: Checkout repository
if: steps.extract.outputs.should_run == 'true' && steps.parse.outputs.success == 'true'
uses: actions/checkout@v4
@@ -401,6 +462,9 @@ jobs:
echo "Changes committed and pushed!"
fi
# =====================================================================
# STEP 5: Post success comment
# =====================================================================
- name: React to comment
if: steps.extract.outputs.should_run == 'true' && steps.parse.outputs.success == 'true'
uses: actions/github-script@v7
@@ -417,13 +481,23 @@ jobs:
// Post a reply
const username = '${{ steps.parse.outputs.username }}';
const project = '${{ steps.parse.outputs.project }}';
const projectSource = '${{ steps.parse.outputs.project_source }}';
const types = JSON.parse('${{ steps.parse.outputs.types }}');
const triggerLine = `${{ steps.extract.outputs.trigger_line }}`;
// Map source to human-readable explanation
const sourceLabels = {
comment: 'explicitly mentioned in comment',
pr_files: 'detected from PR changed files',
issue_context: 'detected from issue labels/title'
};
const sourceNote = sourceLabels[projectSource] || projectSource;
const body = [
"I've added @" + username + " as a contributor to **" + project + "**! :tada:",
"",
"**Recognized for:** " + types.join(', '),
"**Project:** " + project + " (" + sourceNote + ")",
"**Based on:** " + triggerLine,
"",
"The contributor list has been updated in:",
@@ -441,53 +515,86 @@ jobs:
body: body
});
# =====================================================================
# STEP 6: Handle failures — ask user when project is unknown
# =====================================================================
- name: Handle parsing failure
if: steps.extract.outputs.should_run == 'true' && steps.parse.outputs.success == 'false'
uses: actions/github-script@v7
env:
PROJECTS: ${{ env.PROJECTS }}
DEFAULT_PROJECT: ${{ env.DEFAULT_PROJECT }}
with:
script: |
const error = '${{ steps.parse.outputs.error }}';
const triggerLine = `${{ steps.extract.outputs.trigger_line }}`;
const projects = process.env.PROJECTS.split(',');
const defaultProject = process.env.DEFAULT_PROJECT;
const projectSource = '${{ steps.parse.outputs.project_source }}' || '';
const username = '${{ steps.parse.outputs.username }}' || '';
const typesRaw = '${{ steps.parse.outputs.types }}' || '[]';
const types = (() => { try { return JSON.parse(typesRaw); } catch { return []; } })();
let errorMsg = "I couldn't parse that comment.";
if (error === 'no_username') {
errorMsg = "I couldn't find a GitHub username in that comment.";
} else if (error === 'no_types') {
errorMsg = "I couldn't determine the contribution type.";
let body;
if (error === 'no_project') {
// === PROJECT UNKNOWN — ask the user ===
const userPart = username ? ` @${username}` : '';
const typesPart = types.length > 0 ? ` for ${types.join(', ')}` : ' for code';
if (projectSource === 'ambiguous') {
// PR touches multiple projects
body = [
"This PR touches files in **multiple projects**, so I need you to tell me which one. :thinking:",
"",
`I detected${userPart}${typesPart}, but which project should I add them to?`,
"",
"Please reply with the project specified:",
...projects.map(p => `- \`@all-contributors${userPart}${typesPart} in ${p}\``),
].join('\n');
} else {
// No project signal at all (issue with no labels, no PR files)
body = [
`I couldn't determine which project to add the contributor to. :thinking:`,
"",
"**Your comment:** " + triggerLine,
"",
"This repo has multiple projects, so please specify which one:",
...projects.map(p => `- \`@all-contributors${userPart}${typesPart} in ${p}\``),
"",
"**How project detection works:**",
"- On **PRs**: auto-detected from changed file paths (`tinytorch/` → tinytorch, `book/` → book, etc.)",
"- On **issues**: detected from labels or title, otherwise you need to specify explicitly",
].join('\n');
}
} else {
// === Other errors (no_username, no_types) ===
let errorMsg = "I couldn't parse that comment.";
if (error === 'no_username') {
errorMsg = "I couldn't find a GitHub username in that comment.";
} else if (error === 'no_types') {
errorMsg = "I couldn't determine the contribution type.";
}
body = [
errorMsg + " :thinking:",
"",
"**Your comment:** " + triggerLine,
"",
"**Example formats that work:**",
"```",
"@all-contributors @jane-doe fixed typos in the documentation",
"@all-contributors please add @john_smith for reviewing the PR",
"@all-contributors @user123 helped verify the fix worked",
"@all-contributors @dev42 implemented the new caching feature in tinytorch",
"```",
"",
"**Contribution types I understand:**",
"bug, code, doc, design, ideas, review, test, tool",
"",
`**Projects:** ${projects.join(', ')}`,
"On PRs, project is auto-detected from file paths. Otherwise, specify explicitly (e.g., 'in tinytorch')."
].join('\n');
}
const body = [
errorMsg + " :thinking:",
"",
"**Your comment:** " + triggerLine,
"",
"**Example formats that work:**",
"```",
"@all-contributors @jane-doe fixed typos in the documentation",
"@all-contributors please add @john_smith for reviewing the PR",
"@all-contributors @user123 helped verify the fix worked",
"@all-contributors @dev42 implemented the new caching feature",
"```",
"",
"**Contribution types I understand:**",
"- bug (found bugs, reported issues)",
"- code (wrote code, fixed bugs)",
"- doc (documentation, typos)",
"- design (UI/UX, architecture)",
"- ideas (suggestions, proposals)",
"- review (code review, feedback)",
"- test (testing, verification)",
"- tool (built tools, automation)",
"",
`**Projects:** ${projects.join(', ')} (default: ${defaultProject})`,
"Auto-detected from issue context, or mention it explicitly (e.g., 'in tinytorch')."
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,

View File

@@ -436,7 +436,11 @@ jobs:
- name: Run Fresh Install Test (Docker)
run: |
chmod +x tinytorch/scripts/test-fresh-install.sh
./tinytorch/scripts/test-fresh-install.sh --branch ${{ github.ref_name }}
# For PRs, github.ref_name is the merge ref (e.g. "1159/merge") which doesn't
# exist as a real branch on raw.githubusercontent.com. Use head_ref (the actual
# source branch) for PRs, falling back to ref_name for push events.
BRANCH="${{ github.head_ref || github.ref_name }}"
./tinytorch/scripts/test-fresh-install.sh --branch "$BRANCH"
# ===========================================================================
# STAGE 7: USER JOURNEY - Destructive full journey (after all stages)

View File

@@ -961,15 +961,6 @@
"contributions": [
"code"
]
},
{
"login": "AndreaMattiaGaravagno",
"name": "AndreaMattiaGaravagno",
"avatar_url": "https://avatars.githubusercontent.com/u/22458187?v=4",
"profile": "https://github.com/AndreaMattiaGaravagno",
"contributions": [
"code"
]
}
]
}

View File

@@ -221,7 +221,7 @@
% Abstract - REVISED: Curriculum design focus
\begin{abstract}
Machine learning education faces a fundamental gap: students learn algorithms without understanding the systems that execute them. They study gradient descent without measuring memory, attention mechanisms without analyzing $O(N^2)$ scaling, optimizer theory without knowing why Adam requires $3\times$ the optimizer-related memory of SGD (gradients plus two state buffers). This \emph{algorithm-systems divide} produces practitioners who can train models but cannot debug memory failures, optimize inference latency, or reason about deployment trade-offsthe very skills industry demands as ``ML systems engineering.'' We present TinyTorch, a 20-module curriculum that closes this gap through \emph{implementation-based systems pedagogy}: students construct PyTorch's core components (tensors, autograd, optimizers, CNNs, transformers) in pure Python, building a complete framework where every operation they invoke is code they wrote. The design employs three patterns: \emph{progressive disclosure} of complexity, \emph{systems-first integration} of profiling from the first module, and \emph{build-to-validate milestones} recreating 67 years of ML breakthroughsfrom Perceptron (1958) through Transformers (2017) to MLPerf-style benchmarking. Requiring only 4GB RAM and no GPU, TinyTorch demonstrates that deep ML systems understanding is achievable without specialized hardware. The curriculum is available open-source at \texttt{mlsysbook.ai/tinytorch}.
Machine learning education faces a fundamental gap: students learn algorithms without understanding the systems that execute them. They study gradient descent without measuring memory, attention mechanisms without analyzing $O(N^2)$ scaling, optimizer theory without knowing why Adam requires $3\times$ the optimizer-related memory of SGD (gradients plus two state buffers). This \emph{algorithm-systems divide} produces practitioners who can train models but cannot debug memory failures, optimize inference latency, or reason about deployment trade-offs---the very skills industry demands as ``ML systems engineering.'' We present TinyTorch, a 20-module curriculum that closes this gap through \emph{implementation-based systems pedagogy}: students construct PyTorch's core components (tensors, autograd, optimizers, CNNs, transformers) in pure Python, building a complete framework where every operation they invoke is code they wrote. The design employs three patterns: \emph{progressive disclosure} of complexity, \emph{systems-first integration} of profiling from the first module, and \emph{build-to-validate milestones} recreating 67 years of ML breakthroughs---from Perceptron (1958) through Transformers (2017) to MLPerf-style benchmarking. Requiring only 4GB RAM and no GPU, TinyTorch demonstrates that deep ML systems understanding is achievable without specialized hardware. The curriculum is available open-source at \texttt{mlsysbook.ai/tinytorch}.
\end{abstract}
% Main content
@@ -844,7 +844,7 @@ The Optimization Tier completes the systems-first integration arc: students who
Translating curriculum design into effective classroom practice requires addressing integration models, infrastructure accessibility, and student support structures. This section presents deployment patterns designed for diverse institutional contexts.
\textbf{Textbook Integration.}
TinyTorch serves as the hands-on implementation companion to the \emph{Machine Learning Systems} textbook~\citep{mlsysbook2025} (\texttt{mlsysbook.ai}), creating synergy between theoretical foundations and systems engineering practice. While the textbook covers the full ML lifecycledata engineering, training architectures, deployment monitoring, robust operations, and sustainable AITinyTorch provides the complementary experience of building core infrastructure from first principles. This integration enables a complete educational pathway: students study production ML systems architecture in the textbook (Chapter 4: distributed training patterns, Chapter 7: quantization strategies), then implement those same abstractions in TinyTorch (Module 06: autograd for backpropagation, Module 15: INT8 quantization). The two resources address different aspects of the same educational gap: understanding both \emph{how production systems work} (textbook's systems architecture perspective) and \emph{how to build them yourself} (TinyTorch's implementation depth).
TinyTorch serves as the hands-on implementation companion to the \emph{Machine Learning Systems} textbook~\citep{mlsysbook2025} (\texttt{mlsysbook.ai}), creating synergy between theoretical foundations and systems engineering practice. While the textbook covers the full ML lifecycle---data engineering, training architectures, deployment monitoring, robust operations, and sustainable AI---TinyTorch provides the complementary experience of building core infrastructure from first principles. This integration enables a complete educational pathway: students study production ML systems architecture in the textbook (Chapter 4: distributed training patterns, Chapter 7: quantization strategies), then implement those same abstractions in TinyTorch (Module 06: autograd for backpropagation, Module 15: INT8 quantization). The two resources address different aspects of the same educational gap: understanding both \emph{how production systems work} (textbook's systems architecture perspective) and \emph{how to build them yourself} (TinyTorch's implementation depth).
\subsection{Integration Models}
\label{subsec:integration}
@@ -996,7 +996,7 @@ TinyTorch is released as open source to enable community adoption and evolution.
Effective deployment requires structured TA support beyond instructor guidance.
\textbf{TA Preparation}: TAs should develop deep familiarity with critical modules where students commonly struggleModules 06 (Autograd), 09 (CNNs), and 13 (Transformers)by completing these modules themselves and intentionally introducing bugs to understand common error patterns. The \texttt{INSTRUCTOR.md} file documents frequent student errors (gradient shape mismatches, disconnected computational graphs, broadcasting failures) and debugging strategies.
\textbf{TA Preparation}: TAs should develop deep familiarity with critical modules where students commonly struggle---Modules 06 (Autograd), 09 (CNNs), and 13 (Transformers)---by completing these modules themselves and intentionally introducing bugs to understand common error patterns. The \texttt{INSTRUCTOR.md} file documents frequent student errors (gradient shape mismatches, disconnected computational graphs, broadcasting failures) and debugging strategies.
\textbf{Office Hour Demand Patterns}: Student help requests are expected to cluster around conceptually challenging modules, with autograd (Module 06) likely generating higher office hour demand than foundation modules. Instructors should anticipate demand spikes by scheduling additional TA capacity during critical modules, providing pre-recorded debugging walkthroughs, and establishing async support channels (discussion forums with guaranteed response times).

View File

@@ -161,7 +161,7 @@
title = {Computer Science Education Research},
author = {Fincher, Sally and Petre, Marian},
year = {2005},
publisher = {Taylor & Francis},
publisher = {Taylor \& Francis},
address = {London},
doi = {10.1201/9781482287325},
isbn = {9781482287325},
@@ -279,7 +279,7 @@
}
@misc{keller2025ai,
title = {{AI} & Machine-Learning Talent Gap 2025},
title = {{AI} \& Machine-Learning Talent Gap 2025},
author = {{Keller Executive Search}},
year = {2025},
url = {https://www.kellerexecutivesearch.com/intelligence/ai-machine-learning-talent-gap-2025/},

View File

@@ -43,6 +43,8 @@ dev = [
"nbformat>=5.10.0",
"jupyter>=1.1.0",
"jupyterlab>=4.2.0",
"ipykernel>=6.29.0",
"nbdev>=2.3.0",
]
visualization = [
"matplotlib>=3.9.0", # For plotting in Modules 17, 19, 20

View File

@@ -71,7 +71,12 @@ echo ""
echo "▶ Step 1: Running install script (branch: $BRANCH)..."
export TINYTORCH_BRANCH="$BRANCH"
export TINYTORCH_NON_INTERACTIVE=1
curl -sSL "https://raw.githubusercontent.com/harvard-edge/cs249r_book/${BRANCH}/tinytorch/site/extra/install.sh" -o /tmp/install.sh
curl -fsSL "https://raw.githubusercontent.com/harvard-edge/cs249r_book/${BRANCH}/tinytorch/site/extra/install.sh" -o /tmp/install.sh || {
echo "✗ Failed to download install script for branch: $BRANCH"
echo " URL: https://raw.githubusercontent.com/harvard-edge/cs249r_book/${BRANCH}/tinytorch/site/extra/install.sh"
echo " Hint: Does the branch '${BRANCH}' exist and contain tinytorch/site/extra/install.sh?"
exit 1
}
bash /tmp/install.sh
cd tinytorch

View File

@@ -156,8 +156,9 @@ def convert_py_to_notebook(module_path: Path, venv_path: Path, console) -> bool:
console.print("[dim]🔄 Overwriting existing notebook (Python file is source of truth)[/dim]" if notebook_file.exists() else "[dim]✨ Creating new notebook from Python file[/dim]")
try:
from ..core.virtual_env_manager import get_venv_bin_dir
jupytext_path = "jupytext"
venv_jupytext = venv_path / "bin" / "jupytext"
venv_jupytext = get_venv_bin_dir(venv_path) / "jupytext"
if venv_jupytext.exists():
test_result = subprocess.run([str(venv_jupytext), "--version"], capture_output=True, text=True)

View File

@@ -1111,20 +1111,39 @@ class ModuleWorkflowCommand(BaseCommand):
# Run nbdev_export using Python API directly (more reliable than subprocess)
from nbdev.export import nb_export
self.console.print(f"[dim]📦 Exporting {notebook_path.name} → tinytorch/core/...[/dim]")
lib_path = Path.cwd() / "tinytorch"
nb_export(notebook_path, lib_path=lib_path)
# Verify the export actually produced a file
if export_target != "unknown":
target_file = lib_path / (export_target.replace(".", "/") + ".py")
if not target_file.exists():
self.console.print(f"[red]❌ Export verification failed: {target_file} was not created[/red]")
self.console.print(f"[dim] Expected from #| default_exp: {export_target}[/dim]")
self.console.print("[yellow] Check that your notebook has #| export cells with code[/yellow]")
return 1
# Verify the file has actual content (not empty)
content = target_file.read_text(encoding="utf-8")
code_lines = [l for l in content.split('\n')
if l.strip() and not l.strip().startswith('#')]
if len(code_lines) < 2:
self.console.print(f"[red]❌ Export verification failed: {target_file} is empty[/red]")
self.console.print("[yellow] Your notebook's #| export cells may not contain code[/yellow]")
return 1
self.console.print(f"[dim]✅ Your code is now part of the tinytorch package![/dim]")
return 0
except ImportError:
self.console.print("[red]❌ nbdev not found. Is your environment set up?[/red]")
self.console.print("[red]❌ nbdev not found — cannot export module[/red]")
self.console.print("[yellow] Fix: pip install nbdev[/yellow]")
return 1
except Exception as e:
self.console.print(f"[red]Error exporting module: {e}[/red]")
self.console.print(f"[red]❌ Export failed: {e}[/red]")
return 1
def get_progress_data(self) -> dict:

View File

@@ -134,6 +134,8 @@ class SetupCommand(BaseCommand):
("jupyter", "jupyter>=1.0.0"),
("jupyterlab", "jupyterlab>=3.0.0"),
("jupytext", "jupytext>=1.13.0"),
("ipykernel", "ipykernel>=6.29.0"),
("nbdev", "nbdev>=2.3.0"),
("rich", "rich>=12.0.0"),
("pyyaml", "pyyaml>=6.0"),
("psutil", "psutil>=5.8.0"),
@@ -208,7 +210,6 @@ class SetupCommand(BaseCommand):
if result.returncode == 0:
progress.update(task, description="[green]✅ Tiny🔥Torch installed[/green]")
return True
else:
progress.update(task, description="[red]❌ Tiny🔥Torch install failed[/red]")
self.console.print(f"[red]Failed to install Tiny🔥Torch: {result.stderr}[/red]")
@@ -219,6 +220,36 @@ class SetupCommand(BaseCommand):
self.console.print(f"[red]Error installing Tiny🔥Torch: {e}[/red]")
return False
# Register Jupyter kernel so notebooks use this Python environment
self.console.print()
self.console.print("[bold]Registering Jupyter kernel...[/bold]")
try:
result = subprocess.run([
sys.executable, "-m", "ipykernel", "install",
"--user",
"--name", "tinytorch",
"--display-name", "TinyTorch (Python 3)"
], capture_output=True, text=True, timeout=60)
if result.returncode == 0:
self.console.print("[green]✅ Jupyter kernel 'tinytorch' registered[/green]")
self.console.print("[dim] Notebooks will use this Python environment[/dim]")
else:
self.console.print("[red]❌ Jupyter kernel registration failed[/red]")
self.console.print(f"[dim] {result.stderr.strip()}[/dim]")
self.console.print("[yellow] Fix: pip install ipykernel && "
"python -m ipykernel install --user --name tinytorch[/yellow]")
return False
except FileNotFoundError:
self.console.print("[red]❌ ipykernel not found — cannot register Jupyter kernel[/red]")
self.console.print("[yellow] Fix: pip install ipykernel[/yellow]")
return False
except Exception as e:
self.console.print(f"[red]❌ Kernel registration error: {e}[/red]")
return False
return True
def create_virtual_environment(self, force: bool = False) -> bool:
"""Create a virtual environment for Tiny🔥Torch development.
@@ -293,7 +324,9 @@ class SetupCommand(BaseCommand):
self.console.print(f"✅ Virtual environment created at {venv_path}")
# Verify architecture
venv_python = venv_path / "bin" / "python3"
from ..core.virtual_env_manager import get_venv_bin_dir
venv_bin = get_venv_bin_dir(venv_path)
venv_python = venv_bin / ("python.exe" if sys.platform == "win32" else "python3")
if venv_python.exists():
arch_check = subprocess.run(
[str(venv_python), "-c", "import platform; print(platform.machine())"],
@@ -371,6 +404,7 @@ class SetupCommand(BaseCommand):
("Python version (≥3.8)", self.check_python_version),
("NumPy", self.check_numpy),
("Jupyter", self.check_jupyter),
("Jupyter kernel (tinytorch)", self.check_jupyter_kernel),
("TinyTorch CLI", self.check_tinytorch_package)
]
@@ -428,6 +462,17 @@ class SetupCommand(BaseCommand):
except ImportError:
return False
def check_jupyter_kernel(self) -> bool:
"""Check if a TinyTorch Jupyter kernel is registered."""
try:
result = subprocess.run(
[sys.executable, "-m", "jupyter", "kernelspec", "list"],
capture_output=True, text=True, timeout=10
)
return result.returncode == 0 and "tinytorch" in result.stdout
except Exception:
return False
def check_tinytorch_package(self) -> bool:
"""Check if Tiny🔥Torch package is installed (tito CLI)."""
try:

View File

@@ -4,6 +4,7 @@ Health command for TinyTorch CLI: environment health check and validation.
import sys
import os
import subprocess
from argparse import ArgumentParser, Namespace
from pathlib import Path
from rich.panel import Panel
@@ -32,6 +33,9 @@ class HealthCommand(BaseCommand):
title="System Health", border_style="bright_green"))
console.print()
# Track issues for summary
issues = []
# Environment checks table - STATUS ONLY (no version numbers)
env_table = Table(title="Environment Check", show_header=True, header_style="bold blue")
env_table.add_column("Component", style="cyan", width=30)
@@ -55,8 +59,10 @@ class HealthCommand(BaseCommand):
venv_status = "[green]✅ OK[/green]"
elif venv_exists:
venv_status = "[yellow]⚠️ Not Activated[/yellow]"
issues.append("Virtual environment exists but is not activated")
else:
venv_status = "[red]❌ Missing[/red]"
issues.append("Virtual environment not found — run: tito setup")
env_table.add_row("Virtual Environment", venv_status)
# Required dependencies (from requirements.txt)
@@ -73,6 +79,20 @@ class HealthCommand(BaseCommand):
env_table.add_row(display_name, "[green]✅ OK[/green]")
except ImportError:
env_table.add_row(display_name, "[red]❌ Missing[/red]")
issues.append(f"{display_name} not installed")
# Workflow-critical dependencies (needed for module complete/export)
workflow_deps = [
('nbdev (export)', 'nbdev'),
('ipykernel (Jupyter)', 'ipykernel'),
]
for display_name, import_name in workflow_deps:
try:
__import__(import_name)
env_table.add_row(display_name, "[green]✅ OK[/green]")
except ImportError:
env_table.add_row(display_name, "[red]❌ Missing[/red]")
issues.append(f"{display_name} not installed — run: pip install {import_name}")
# Optional dependencies (nice to have, not required for core workflow)
optional_deps = [
@@ -89,6 +109,115 @@ class HealthCommand(BaseCommand):
console.print(env_table)
console.print()
# ── Notebook Readiness checks ──
# These diagnose the exact "ModuleNotFoundError" problem students hit
nb_table = Table(title="Notebook Readiness", show_header=True, header_style="bold yellow")
nb_table.add_column("Check", style="cyan", width=30)
nb_table.add_column("Status", justify="center", width=15)
nb_table.add_column("Detail", style="dim", width=35)
# 1. Can we import the tinytorch package at all?
try:
import tinytorch
nb_table.add_row(
"TinyTorch package",
"[green]✅ OK[/green]",
f"v{getattr(tinytorch, '__version__', 'unknown')}"
)
except ImportError as e:
nb_table.add_row(
"TinyTorch package",
"[red]❌ Not importable[/red]",
"run: pip install -e ."
)
issues.append("tinytorch package not importable — run: pip install -e .")
# 2. Does tinytorch/core/tensor.py exist? (the most common failure point)
core_dir = self.config.project_root / "tinytorch" / "core"
tensor_file = core_dir / "tensor.py"
if tensor_file.exists():
nb_table.add_row(
"Core module files",
"[green]✅ OK[/green]",
f"{len(list(core_dir.glob('*.py')))} files in tinytorch/core/"
)
else:
nb_table.add_row(
"Core module files",
"[red]❌ Missing[/red]",
"tinytorch/core/tensor.py not found"
)
issues.append("tinytorch/core/tensor.py missing — package may be corrupted")
# 3. Can the Tensor class actually be imported?
try:
from tinytorch.core.tensor import Tensor
if Tensor is not None:
nb_table.add_row(
"Tensor import",
"[green]✅ OK[/green]",
"from tinytorch.core.tensor import Tensor"
)
else:
nb_table.add_row(
"Tensor import",
"[yellow]⚠️ None[/yellow]",
"Module 01 may not be exported yet"
)
issues.append("Tensor is None — complete Module 01: tito module complete 01")
except ImportError as e:
nb_table.add_row(
"Tensor import",
"[red]❌ Failed[/red]",
str(e)[:35]
)
issues.append(f"Cannot import Tensor: {e}")
# 4. Jupyter kernel check — does a kernel exist that points to this Python?
kernel_status, kernel_detail = self._check_jupyter_kernel()
nb_table.add_row("Jupyter kernel", kernel_status, kernel_detail)
if "" in kernel_status or "⚠️" in kernel_status:
issues.append(kernel_detail)
# 5. Check that this Python == the Jupyter kernel's Python
# (catches the exact mismatch that causes ModuleNotFoundError in notebooks)
kernel_python = self._get_kernel_python()
if kernel_python:
if os.path.realpath(kernel_python) == os.path.realpath(sys.executable):
nb_table.add_row(
"Kernel ↔ tito Python",
"[green]✅ Match[/green]",
"Same interpreter"
)
else:
nb_table.add_row(
"Kernel ↔ tito Python",
"[red]❌ Mismatch[/red]",
f"Kernel: {kernel_python}"
)
issues.append(
f"Jupyter kernel uses a different Python than tito — "
f"run: python -m ipykernel install --user --name tinytorch"
)
else:
nb_table.add_row(
"Kernel ↔ tito Python",
"[dim]○ Skipped[/dim]",
"No kernel to check"
)
console.print(nb_table)
console.print()
# ── Issues Summary ──
if issues:
console.print(Panel(
"\n".join(f"{issue}" for issue in issues),
title=f"⚠️ {len(issues)} issue{'s' if len(issues) > 1 else ''} found",
border_style="yellow"
))
console.print()
# Module structure table
struct_table = Table(title="Module Structure", show_header=True, header_style="bold magenta")
struct_table.add_column("Path", style="cyan", width=25)
@@ -122,3 +251,53 @@ class HealthCommand(BaseCommand):
info_cmd.add_arguments(info_args)
info_args = info_args.parse_args([]) # Empty args for info
return info_cmd.run(info_args)
def _check_jupyter_kernel(self):
"""Check if a TinyTorch Jupyter kernel is registered."""
try:
result = subprocess.run(
[sys.executable, "-m", "jupyter", "kernelspec", "list"],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0 and "tinytorch" in result.stdout:
return "[green]✅ Registered[/green]", "tinytorch kernel found"
elif result.returncode == 0:
# Jupyter works but no tinytorch kernel
return (
"[yellow]⚠️ No tinytorch kernel[/yellow]",
"run: python -m ipykernel install --user --name tinytorch"
)
else:
return "[yellow]⚠️ Cannot list[/yellow]", "jupyter kernelspec list failed"
except FileNotFoundError:
return "[dim]○ Skipped[/dim]", "jupyter not installed"
except Exception:
return "[dim]○ Skipped[/dim]", "could not check"
def _get_kernel_python(self):
"""Get the Python executable path used by the default or tinytorch Jupyter kernel."""
try:
import json
# Try tinytorch kernel first, then python3 default
for kernel_name in ("tinytorch", "python3"):
result = subprocess.run(
[sys.executable, "-m", "jupyter", "kernelspec", "list", "--json"],
capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
return None
data = json.loads(result.stdout)
kernels = data.get("kernelspecs", {})
if kernel_name in kernels:
kernel_dir = kernels[kernel_name].get("resource_dir", "")
kernel_json = Path(kernel_dir) / "kernel.json"
if kernel_json.exists():
spec = json.loads(kernel_json.read_text())
argv = spec.get("argv", [])
if argv:
return argv[0] # First element is the Python path
except Exception:
pass
return None

View File

@@ -1,10 +1,17 @@
import os, json
import os, sys, json
from pathlib import Path
DEFAULT_VENV = ".venv"
CONFIG_FILE = ".tinyrc"
def get_venv_bin_dir(venv_path: Path) -> Path:
"""Return the bin directory for a venv (Scripts/ on Windows, bin/ on Unix)."""
if sys.platform == "win32" or os.name == "nt":
return venv_path / "Scripts"
return venv_path / "bin"
def get_venv_path() -> Path:
"""
Fetch venv in case users have a custom path