Урок 1. Окружение курса

🎯 Цели урока

📖 Теория: раздел «Окружение: воспроизводимость прежде всего» в theory.pdf.

Какой Python нас запускает

Каждый модуль курса — отдельный uv-проект со своим окружением. Ноутбук должен работать на Python 3.12 из окружения модуля, а не на системном Python.

import sys
from pathlib import Path

from mlcourse import repo_root

root = repo_root()
prefix = Path(sys.prefix)
where = prefix.relative_to(root) if prefix.is_relative_to(root) else prefix

print("Python:    ", sys.version.split()[0])
print("Окружение: ", where)
assert sys.version_info[:2] == (3, 12), "Нужен Python 3.12: запустите урок через `make native M=00` или `make lab M=00`"
Python:     3.12.14
Окружение:  modules/M00-setup/.venv

Нативно окружение живёт в modules/M00-setup/.venv, а в Docker — в отдельном томе /venvs/..., чтобы Linux-окружение контейнера не смешивалось с macOS-окружением на хосте.

Как устроен репозиторий

about = {
    "datasets": "скрипты загрузки датасетов (сами данные не коммитятся)",
    "infra": "Docker-образ и docker compose: JupyterLab и сервисы",
    "modules": "модули курса: теория, уроки, упражнения, решения",
    "quarto": "оформление PDF и сайта курса",
    "scripts": "служебные скрипты (проверка секретов, запуск ноутбуков)",
    "shared": "общая библиотека mlcourse",
    "templates": "заготовка нового модуля",
}
for d in sorted(p for p in root.iterdir() if p.is_dir() and not 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 in sorted(module.iterdir()):
    if p.name.startswith(".") or p.name == "__pycache__":
        continue
    print(("📁 " if p.is_dir() else "📄 ") + p.name)
📁 exercises
📁 lessons
📄 pyproject.toml
📁 solutions
📁 theory
📄 uv.lock

Зафиксированные версии

pyproject.toml говорит, какие пакеты нужны модулю, а uv.lock — какие именно версии установлены, включая все транзитивные зависимости. Посмотрим на lock-файл:

import tomllib

lock = 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]}")
В uv.lock зафиксировано пакетов: 118
  openai      == 3.19.2
  httpx       == 0.28.1
  numpy       == 2.5.3
  jupyterlab  == 4.6.4
  matplotlib  == 3.11.2

Прямых зависимостей в pyproject.toml меньше десятка, а пакетов в lock-файле — больше сотни: остальные подтянулись транзитивно. Без lock-файла любая из них могла бы обновиться и сломать урок.

Полезные команды (запускаются в терминале, в папке модуля):

Команда Что делает
uv sync привести окружение в точное соответствие с uv.lock
uv add <пакет> добавить зависимость и обновить uv.lock
uv run <команда> выполнить команду в окружении модуля
uv tree дерево зависимостей

Библиотека курса и .env

Уроки не знают, где живут модели: адреса и имена моделей лежат в .env в корне репозитория, а библиотека mlcourse читает их и выдаёт готовые клиенты. Посмотрим на настройки (ключи не показываются, адрес удалённого сервера замаскирован):

import mlcourse

print(mlcourse.describe_settings())
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 and not line.startswith("#")))
LLM_BASE_URL=http://localhost:11434/v1
LLM_API_KEY=ollama
LLM_MODEL=qwen3:8b
OLLAMA_BASE_URL=http://localhost:11434
LLM_LOCAL_MODEL=qwen3:8b
EMBED_MODEL=bge-m3
LLM_CACHE=on

✍️ Упражнения модуля

Упражнения лежат в 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.

✅ Итоги