Нативно окружение живёт в modules/M00-setup/.venv, а в Docker — в отдельном томе /venvs/..., чтобы Linux-окружение контейнера не смешивалось с macOS-окружением на хосте.
Как устроен репозиторий
about = {"datasets": "скрипты загрузки датасетов (сами данные не коммитятся)","infra": "Docker-образ и docker compose: JupyterLab и сервисы","modules": "модули курса: теория, уроки, упражнения, решения","quarto": "оформление PDF и сайта курса","scripts": "служебные скрипты (проверка секретов, запуск ноутбуков)","shared": "общая библиотека mlcourse","templates": "заготовка нового модуля",}for d insorted(p for p in root.iterdir() if p.is_dir() andnot p.name.startswith((".", "_"))):print(f"{d.name +'/':<12}{about.get(d.name, '')}")
datasets/ скрипты загрузки датасетов (сами данные не коммитятся)
infra/ Docker-образ и docker compose: JupyterLab и сервисы
modules/ модули курса: теория, уроки, упражнения, решения
quarto/ оформление PDF и сайта курса
scripts/ служебные скрипты (проверка секретов, запуск ноутбуков)
shared/ общая библиотека mlcourse
templates/ заготовка нового модуля
Внутри модуля — всегда одна и та же структура:
module = root /"modules"/"M00-setup"for p insorted(module.iterdir()):if p.name.startswith(".") or p.name =="__pycache__":continueprint(("📁 "if p.is_dir() else"📄 ") + p.name)
pyproject.toml говорит, какие пакеты нужны модулю, а uv.lock — какие именно версии установлены, включая все транзитивные зависимости. Посмотрим на lock-файл:
import tomlliblock = tomllib.loads((module /"uv.lock").read_text(encoding="utf-8"))pinned = {p["name"]: p["version"] for p in lock["package"] if"version"in p}print(f"В uv.lock зафиксировано пакетов: {len(pinned)}")for name in ["openai", "httpx", "numpy", "jupyterlab", "matplotlib"]:print(f" {name:<11} == {pinned[name]}")
Прямых зависимостей в pyproject.toml меньше десятка, а пакетов в lock-файле — больше сотни: остальные подтянулись транзитивно. Без lock-файла любая из них могла бы обновиться и сломать урок.
Полезные команды (запускаются в терминале, в папке модуля):
Команда
Что делает
uv sync
привести окружение в точное соответствие с uv.lock
uv add <пакет>
добавить зависимость и обновить uv.lock
uv run <команда>
выполнить команду в окружении модуля
uv tree
дерево зависимостей
Библиотека курса и .env
Уроки не знают, где живут модели: адреса и имена моделей лежат в .env в корне репозитория, а библиотека mlcourse читает их и выдаёт готовые клиенты. Посмотрим на настройки (ключи не показываются, адрес удалённого сервера замаскирован):
main qwen3.8-27b https://<удалённый сервер>/v1
local qwen3:8b http://localhost:11434/v1
embed bge-m3 http://localhost:11434/v1
кэш record LLM_CACHE
Три роли моделей:
main — основная модель (в .env: LLM_BASE_URL, LLM_API_KEY, LLM_MODEL); если LLM_BASE_URL не задан, main = local;
local — локальная модель в Ollama (LLM_LOCAL_MODEL, по умолчанию qwen3:8b);
embed — модель эмбеддингов (EMBED_MODEL, по умолчанию bge-m3).
Шаблон настроек — файл .env.example. Сам .env в git не попадает: в нём могут быть ключи.
example = (root /".env.example").read_text(encoding="utf-8")print("\n".join(line for line in example.splitlines() if line andnot line.startswith("#")))
Упражнения лежат в modules/M00-setup/exercises/. Каждое — файл с заготовками функций (raise NotImplementedError("TODO")) и тесты к нему в exercises/tests/:
Файл
О чём
ex01_memory.py
сколько памяти занимают веса и KV-кэш модели
ex02_speed.py
скорость генерации и её предел
ex03_sse.py
разбор потокового ответа (SSE)
ex04_request.py
сборка запроса к /chat/completions
Проверка: make test M=00 в корне репозитория. Пока функции не написаны, тесты красные — это нормально. Упражнения 1–2 опираются на теорию, 3–4 — на урок 3.