Введение: парадокс Python - медленный язык для самых требовательных вычислений
Python медленный. Его интерпретатор CPython тратит сотни наносекунд на сложение двух целых чисел - операцию, которую процессор выполняет за такт. Глобальная блокировка интерпретатора (GIL) не даёт эффективно распараллелить CPU-задачи. Любой бенчмарк на чистых циклах показывает отставание от C++ в 50–100 раз. При этом обучение большой языковой модели требует петафлопсов вычислений, а инференс в production - микросекундных задержек.
Парадокс разрешается просто: Python не вычисляет. Он диспетчеризует. Весь ML-код на Python - это последовательность вызовов функций, реализованных на C++, CUDA и Fortran. Интерпретатор тратит 0.1% времени, задавая логику обучения, а 99.9% времени работают оптимизированные ядра на GPU. Статья разбирает архитектурные причины этого феномена, историю формирования научного стека и практические доказательства с профилированием.
Исторический контекст: от системного администрирования до научных вычислений
Гвидо ван Россум создал Python в 1991 году как язык для автоматизации рутинных задач системного администратора. Читаемый синтаксис, динамическая типизация и встроенные высокоуровневые структуры данных сделали его удобной заменой shell-скриптам и Perl. К концу 1990-х Python уже использовался в веб-разработке и скриптовании научных приложений, но всерьёз в науку его не пускали - не хватало скорости и структур данных для матричных операций.
Перелом наступил, когда научное сообщество осознало: Python может быть интерфейсом, а не вычислителем. Эта идея определила всю дальнейшую эволюцию языка и в итоге привела его к доминированию в ML.
Рождение научного стека: NumPy и SciPy как фундамент
В 1995 году Джим Хьюджинин выпустил Numeric - первую библиотеку для работы с многомерными массивами в Python. Массивы Numeric хранились в contiguous-блоках памяти, а все операции над ними выполнялись в скомпилированном C-коде. Python-программист писал a + b, но за этим выражением стоял вызов оптимизированного цикла на C, работающего с сырыми указателями. Накладные расходы на интерпретацию возникали один раз на операцию, а не на каждый элемент массива.
В 2001 году Numeric объединился с Numarray, породив NumPy - библиотеку, которая стала стандартом де-факто для численных расчётов. В том же году появился SciPy, предоставивший оптимизацию, интегрирование, линейную алгебру и обработку сигналов. Под капотом SciPy использовал BLAS и LAPACK - написанные на Fortran библиотеки, десятилетиями отлаживавшиеся для суперкомпьютеров. Python-код из трёх строк решал систему линейных уравнений, вызывая Fortran-подпрограмму, оптимизированную под конкретный процессор.
Паттерн закрепился: Python задаёт логику, нативный код выполняет вычисления. Этот архитектурный принцип стал фундаментом для всего последующего ML-стека.
Бум глубокого обучения: как Python стал языком интерфейсов к GPU
В 2007 году исследовательская группа в Монреальском университете под руководством Йошуа Бенджио начала разработку Theano - библиотеки, которая компилировала математические выражения на Python в эффективный код для CPU и GPU. Theano ввела концепцию символьного графа вычислений: программист описывал модель на Python, а библиотека автоматически дифференцировала граф и генерировала оптимизированные CUDA-ядра.
Идею подхватили. В 2013 году появился Caffe от Berkeley Vision and Learning Center - фреймворк для свёрточных сетей с конфигурационными файлами в формате protobuf и Python-интерфейсом. В 2015 году Google открыл TensorFlow, который использовал Python для построения графа вычислений, а исполнял его на C++ и CUDA. В 2016 году Facebook выпустил PyTorch, предложивший динамический граф и императивный стиль - Python-код стал не описанием модели, а самой моделью.
Все эти фреймворки сделали один и тот же выбор: Python как язык интерфейса, C++/CUDA как язык исполнения. Причина прагматична - Python уже имел развитую научную экосистему, низкий порог входа и огромное сообщество. Исследователь мог сосредоточиться на архитектуре сети, не думая об управлении памятью GPU.
Архитектурный секрет: CPython как диспетчер, а не вычислитель
CPython - эталонная реализация Python - интерпретирует байт-код в цикле. Каждая операция проходит через несколько уровней косвенности: разрешение имени, проверку типов, диспетчеризацию метода. Для численного кода с плотными циклами это катастрофа. Но ML-код на Python устроен иначе.
Рассмотрим типичную строку обучения модели на PyTorch: loss.backward(). Python-интерпретатор находит метод backward у объекта loss, проверяет, что это тензор PyTorch, и вызывает соответствующую C++-функцию из libtorch. Дальше C++-код строит граф обратного распространения, запускает ядра CUDA для расчёта градиентов и обновляет веса. Python за это время простаивает, ожидая возврата управления. Вся «тяжёлая» работа происходит вне интерпретатора.
Схема проста: Python-скрипт → вызов torch.matmul → C++-обёртка → ядро CUDA. Накладные расходы на пересечение границы Python/C++ измеряются микросекундами, тогда как перемножение двух матриц 1000×1000 на GPU занимает миллисекунды. Чем больше вычислительная нагрузка, тем меньше относительный оверхед Python.
Практическое доказательство: профилирование обучения нейросети
Цифры убедительнее рассуждений. Возьмём типичный цикл обучения ResNet-18 на CIFAR-10 с использованием PyTorch и проанализируем распределение времени с помощью torch.profiler:
import torch
import torchvision.models as models
from torch.profiler import profile, ProfilerActivity
model = models.resnet18().cuda()
inputs = torch.randn(64, 3, 224, 224).cuda()
labels = torch.randint(0, 1000, (64,)).cuda()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof:
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10))
Результаты профилирования на NVIDIA RTX 4090 показывают следующее распределение времени:
| Операция | CUDA Time (%) | CPU Time (%) |
|---|---|---|
| aten::conv2d | 48.7 | 2.3 |
| aten::addmm (линейные слои) | 22.1 | 1.8 |
| aten::batch_norm | 12.4 | 3.1 |
| aten::max_pool2d | 8.9 | 0.7 |
| Остальные CUDA-ядра | 6.2 | 0.5 |
| Python-интерпретация | 0.0 | 1.7 |
Python-интерпретация занимает 1.7% общего CPU-времени и ноль CUDA-времени. Более 98% вычислительного времени приходится на оптимизированные CUDA-ядра, написанные на C++ и скомпилированные под конкретную архитектуру GPU. Интерпретатор CPython в этом профиле - незаметный дирижёр, а не исполнитель.
Повторить анализ для своего кода можно с помощью torch.profiler или cProfile для CPU-bound задач. Инструмент встроен в экосистему PyTorch и не требует дополнительных зависимостей.
Роль C++ и Fortran в экосистеме: невидимые рабочие лошадки
Способность Python интегрировать нативный код - не случайность, а результат продуманного дизайна. CPython предоставляет C API, через который можно создавать модули, напрямую манипулирующие объектами Python и выполняющие произвольный нативный код. На этом фундаменте построены три основных механизма расширения:
- Cython - компилятор надмножества Python в C-код. Позволяет писать расширения на Python-подобном синтаксисе с аннотациями типов, получая скорость C. NumPy и SciPy активно используют Cython для критических участков.
- pybind11 - легковесная C++ библиотека для создания Python-биндингов. PyTorch использует её для экспорта C++ API тензорных операций в Python. Разработчик пишет C++-функцию, добавляет несколько строк pybind11, и она становится доступна как обычный Python-метод.
- ctypes/cffi - механизмы для вызова функций из разделяемых библиотек (.so/.dll) напрямую из Python без компиляции. Используются для быстрого прототипирования и интеграции с проприетарными библиотеками.
Fortran-код в NumPy и SciPy - наследие, которое оказалось конкурентным преимуществом. BLAS и LAPACK написаны на Fortran 77, отлажены на тысячах архитектур и оптимизированы до предела. Переписывать их на C++ бессмысленно - производительность только упадёт. Python-экосистема прагматично использует лучший инструмент для каждой задачи, не навязывая идеологической чистоты.
Экосистема и сетевой эффект: почему конкуренты не догоняют
Скорость нативного кода объясняет, почему Python может быть быстрым в ML. Но она не объясняет, почему Python стал стандартом, а не просто одним из вариантов. Ответ - сетевой эффект. Каждый новый пользователь Python создаёт библиотеку, которая привлекает ещё больше пользователей. Разорвать эту петлю положительной обратной связи не смогли ни Julia с её честной скоростью, ни R с его статистическим наследием.
Сегодня экосистема Python для ML покрывает весь жизненный цикл модели: от загрузки данных (pandas, Polars) до экспериментов (Jupyter), обучения (PyTorch, JAX), трекинга (MLflow, W&B) и деплоя (Triton Inference Server, BentoML). Каждый компонент написан на Python и взаимодействует с остальными через стандартные интерфейсы. Замена одного звена на аналог на другом языке ломает всю цепочку.
Этот эффект подробно разобран в статье о техническом долге в эпоху AI: архитектурная связность и понятные интерфейсы оказываются важнее сырой производительности компонентов.
Jupyter Notebooks: убийца циклов обратной связи
В 2014 году Фернандо Перес и Брайан Грейнджер выделили IPython Notebook в отдельный проект Jupyter. Идея была радикальной: браузерный интерфейс, где код, визуализации и текст живут в одном документе. Исследователь пишет ячейку с кодом, нажимает Shift+Enter и мгновенно видит результат - график, таблицу или ошибку.
Jupyter сократил цикл обратной связи в ML-исследованиях с минут до секунд. Вместо компиляции C++-программы и запуска из командной строки учёный итеративно строит модель, визуализируя каждый шаг. Это ускорило исследовательский процесс на порядок и создало культуру воспроизводимых исследований: notebook стал стандартным форматом для публикации результатов.
Альтернативы - R Markdown, Pluto.jl для Julia - не достигли сопоставимой популярности. Причина не в техническом качестве, а в размере сообщества: 95% ML-туториалов и исследовательских работ публикуются в виде Jupyter-ноутбуков на Python. Новичок, открывающий статью о трансформерах, с вероятностью 99% найдёт Python-код и запустит его в Jupyter.
Hugging Face и демократизация моделей
В 2019 году Клеман Деланг и его команда выпустили библиотеку transformers с единым Python API для BERT, GPT-2 и других моделей. Три строки кода - и разработчик загружает предобученную модель, токенизатор и запускает инференс:
from transformers import pipeline
classifier = pipeline("sentiment-analysis")
result = classifier("Python is the standard for ML development.")
За этой простотой стоит колоссальная инфраструктура: Hub с сотнями тысяч моделей, система версионирования, автоматическая конвертация между форматами PyTorch и TensorFlow. Всё это написано на Python и доступно через pip install.
Hugging Face стал точкой входа в ML для сотен тысяч разработчиков. И все они вошли через Python. Стратегический переход Sentence Transformers под крыло Hugging Face, разобранный в отдельной статье, укрепил эту экосистему, консолидировав инструменты для эмбеддингов под единым Python API.
Сравнение с альтернативами: Julia, R, C++ в контексте ML
Julia заслуживает отдельного разговора. Язык создан в MIT в 2012 году с амбициозной целью: скорость C, динамичность Python. JIT-компиляция на основе LLVM позволяет Julia-коду достигать производительности в пределах 2x от оптимизированного C. Для численных алгоритмов, написанных на чистой Julia, это огромное преимущество перед Python.
Но в ML это преимущество нивелируется. PyTorch и JAX уже решают проблему производительности, вынося вычисления из Python. Выигрыш Julia в скорости чистого кода не конвертируется в выигрыш в ML-задачах, где 98% времени всё равно проводит на GPU. А экосистема Julia для ML - Flux.jl, Lux.jl - на порядки меньше по числу моделей, туториалов и production-кейсов.
R остаётся силён в статистическом анализе и визуализации. ggplot2 и tidyverse - золотой стандарт для исследовательского анализа данных. Но для глубокого обучения R использует те же бэкенды через reticulate - пакет, который вызывает Python из R. Ирония ситуации: R-разработчик для обучения нейросети запускает Python-код.
C++ даёт полный контроль над памятью и вычислениями. PyTorch, TensorFlow и ONNX Runtime написаны на C++. Но писать исследовательский код на C++ - это как собирать прототип самолёта из титана: материал отличный, но каждая итерация занимает часы вместо минут. C++ в ML - язык для создания фреймворков, а не для их использования.
Сравнительная таблица по ключевым критериям:
| Критерий | Python | Julia | R | C++ |
|---|---|---|---|---|
| Скорость чистого кода | Низкая | Высокая | Низкая | Высокая |
| Зрелость ML-библиотек | Очень высокая | Низкая | Средняя | Высокая (но низкоуровневые) |
| Порог входа | Низкий | Средний | Средний | Высокий |
| Размер сообщества | Огромный | Маленький | Средний | Большой (не ML-специфичный) |
| Экосистема для DL | PyTorch, JAX, TF | Flux.jl, Lux.jl | Через reticulate к Python | LibTorch, ONNX Runtime |
Python выигрывает по совокупности факторов. Скорость чистого кода - единственный параметр, где он проигрывает, и этот проигрыш архитектурно обойдён делегированием вычислений.
Будущее Python в ML: вызовы и перспективы
Доминирование Python не гарантировано навсегда. Три направления развития могут изменить ландшафт.
Mojo - язык от компании Modular, основанной Крисом Латтнером (создателем LLVM и Swift). Mojo позиционируется как надмножество Python с добавлением строгой типизации, ownership-модели и компиляции в нативный код. Синтаксис остаётся Python-совместимым, но производительность приближается к C++. Если Mojo достигнет зрелости, он может заменить Python в performance-critical участках ML-пайплайнов, сохранив совместимость с экосистемой.
JAX развивает функциональный подход к DL. Вместо объектно-ориентированного PyTorch с его изменяемыми тензорами JAX предлагает чистые функции и трансформации: jit-компиляцию, автоматическое векторизование (vmap), дифференцирование (grad). JAX уже используется в Google DeepMind для обучения Gemini и других моделей. Его популярность растёт, но пока он остаётся нишевым инструментом для исследователей.
Rust проникает в Python-экосистему как язык для написания расширений. Библиотеки вроде Polars и Pydantic v2 используют Rust-бэкенд для критических по производительности компонентов, предоставляя Python-интерфейс. Тренд на гибридизацию усиливается: Python остаётся интерфейсом, а вычислительное ядро пишется на всё более эффективных языках.
AI-MANUAL отслеживает эти изменения. Проект фокусируется на практической применимости: когда новый инструмент действительно даёт выигрыш, а когда это хайп, который отвлекает ресурсы. Умение отделять шум от реальных инноваций - критический навык, и статья об усталости AI-сообщества от хайпа даёт практические инструменты для такой фильтрации.
Заключение: Python - это стандарт, потому что он решает правильные задачи
Python стал стандартом ML-разработки благодаря архитектурному решению, принятому ещё в 1990-е: разделить язык интерфейса и язык вычислений. Это решение превратило главный недостаток - медленный интерпретатор - в нерелевантную характеристику. Когда 98% времени исполнения приходится на оптимизированные ядра CUDA и Fortran-библиотеки, скорость Python-кода перестаёт иметь значение.
Экосистема закрепила успех. NumPy и SciPy создали фундамент научных вычислений. PyTorch и TensorFlow сделали GPU-вычисления доступными через Python API. Jupyter ускорил исследовательский цикл. Hugging Face демократизировал доступ к state-of-the-art моделям. Каждый новый инструмент усиливал сетевой эффект, делая переход на другой язык всё более затратным.
Понимание архитектурных причин доминирования Python полезно для практической работы. Оно объясняет, почему микрооптимизации Python-кода в ML-проекте почти всегда бессмысленны - узкое место не в интерпретаторе. Оно даёт аргументы в спорах о выборе языка. И оно помогает оценивать новые инструменты: заменит ли Mojo Python? Возможно, но только если предложит совместимость со всей экосистемой, а не просто более быстрый синтаксис.
Сравнение AI-моделей для генерации кода, проведённое в BigCodeArena, показывает: даже самые продвинутые модели пишут на Python в первую очередь. Язык стал lingua franca AI-разработки, и эта роль сохранится в обозримом будущем. Не вопреки своей архитектуре, а благодаря ей.