Машина на глиняних ногах
5 / 18

Розділ 4. Привид фон Ноймана

Людина, що знала все

Якщо коли-небудь існувала людина, яку можна назвати “занадто розумною для власного добра” — це Джон фон Нойман.

Народився в Будапешті у 1903-му, в єврейській банкірській родині. У п’ять років ділив у голові вісім цифр на вісім. У шість розмовляв стародавньою грецькою зі своїм батьком. У вісім знав диференціальне числення. У дев’ятнадцять мав дві опубліковані математичні роботи. У двадцять три отримав докторську з математики в Будапешті і тоді ж — диплом хімічного інженера в Цюріху. Паралельно.

До тридцяти років він перевернув основи квантової механіки (“Математичні основи квантової механіки”, 1932 — досі класика). Створив нову галузь — теорію ігор (“Теорія ігор та економічної поведінки”, 1944, разом з Оскаром Моргенштерном). Заснував теорію самовідтворюваних автоматів (з якої вийшли клітинні автомати, а з них — ігри типу Conway’s Game of Life і цілий напрямок програмування).

Паралельно працював у Манхеттенському проекті, де розрахував імплозійний механізм плутонієвої бомби. Після війни консультував уряд США з ядерної стратегії, був членом Комісії з атомної енергетики. Лауреат всього, що можна було дати.

Він помер у 1957 році, у віці 53 років, від раку підшлункової. Священик, який його сповідав (фон Нойман охрестився на смертному одрі), пізніше казав, що не зміг відповісти на частину його богословських питань.

І серед усього цього велетенського списку досягнень — є одна робота, мабуть, найнудніше названа, яку він написав у 1945 році. Вона не здобула Нобелівської. Про неї не пишуть у біографіях поряд з бомбою і теорією ігор. Але саме вона сьогодні визначає, як влаштований твій комп’ютер, мій комп’ютер, і кожен комп’ютер у світі.

Називалась вона: “First Draft of a Report on the EDVAC”.

Революція, яку мало хто помітив

У 1945 році більшість комп’ютерів не були “програмованими” у сучасному розумінні. ENIAC, про який ми говорили у першому розділі, перемикався на іншу задачу шляхом фізичного перемотування кабелів. Там були тисячі штекерів, які треба було вручну переставляти, щоб сказати машині, що робити. Зміна програми займала дні.

Фон Нойман сидів консультантом у команді ENIAC і його наступника — EDVAC. Він написав документ, який узагальнював обговорення команди і пропонував радикально іншу архітектуру. Його ідея була проста, але перевертала все:

Не відокремлюй код від даних. Зберігай інструкції в тій самій пам’яті, що і числа. Процесор нехай читає наступну інструкцію звідти ж, звідки читає дані для обчислень.

Це те, що зараз називають архітектурою фон Ноймана (хоча історики досі сперечаються, скільки саме ідей належало самому фон Нойману, а скільки — Еккерту і Мокчлі, які будували ENIAC і EDVAC; фон Нойман просто був тим, хто написав і підписав документ).

Чому це було революційно? Бо це зробило комп’ютер універсальним. Не треба перемотувати кабелі, щоб змінити задачу. Завантажив нову програму в пам’ять — і та сама машина робить іншу роботу. Комп’ютер перестав бути спеціалізованим приладом і став всезагальним обчислювачем.

Сучасне програмування — від while (true) в C до React-компонентів — можливе тільки тому, що у 1945 році фон Нойман написав: код — це просто ще одна форма даних.

Але в цій же ідеї була прихована бомба сповільненої дії. Та сама, яка сьогодні душить увесь AI.

Вузьке горло, яке виросло разом із нами

Подивимось на архітектуру фон Ноймана уважно. Один процесор. Одна пам’ять. Один канал між ними (у нас його називають шиною). Процесор читає інструкцію з пам’яті, читає дані, обчислює, записує результат назад у пам’ять, читає наступну інструкцію, і так далі.

Коли процесори були повільні і пам’ять була повільна — це працювало чудово. У 1970-х обидва працювали приблизно на одній швидкості. Процесор робив операцію за такт, пам’ять за кілька тактів — різниця невелика.

Але з часом процесори прискорилися набагато швидше за пам’ять. Закон Мура дав процесорам більше транзисторів, і вони стали розумніші, швидші, складніші. А пам’ять… пам’ять працює за фізикою, яка складніша: динамічна RAM зберігає біти як заряд у мікроконденсаторах, і цей заряд треба постійно оновлювати (англ. refresh), і читати його — процес повільний.

Звідки взявся цей дивний refresh? У 1966 році Роберт Деннард з IBM придумав геніальну економію: один транзистор + один конденсатор = один біт. Дешево, щільно, мільйони бітів на квадратний міліметр. Саме завдяки цьому винаходу пам’ять перестала бути дорогим дефіцитом (до того її робили з феритових кілець, які прошивали дротом вручну) і стала тим, що сьогодні легко вміщає 32 ГБ у твоєму ноутбуці. Але конденсатор тече — заряд зникає за мілісекунди. Тому DRAM мусить тисячі разів на секунду перезаписувати саму себе. Уся пам’ять твого комп’ютера — це нескінченне нагадування собі, що вона ще існує.

У 1980-х розрив був 10×. У 1990-х — 100×. Сьогодні — 300-400×. Процесор може зробити 300 інструкцій за той час, за який доходить одне звертання до RAM.

Це називають фон Нойманівським вузьким горлом (von Neumann bottleneck). Процесор — швидкісний. Пам’ять — повільна. Канал між ними став єдиним реальним обмежувачем швидкості сучасних систем.

Ти можеш мати процесор у мільярд разів швидший за 1945-й, але якщо він чекає дані з пам’яті 300 циклів з кожних 400 — насправді він “працює” тільки чверть часу. Решту — сидить і дивиться в одну точку.

Винахід кешу

Індустрія зрозуміла це у 1980-х і вигадала рятівний круг: кеш.

Ідея проста. Покласти маленьку, але надзвичайно швидку пам’ять прямо на процесор. Коли процесор читає з RAM — записувати копію прочитаного в цей маленький кеш. Наступного разу, коли потрібні ті самі дані — беремо з кешу, не з RAM.

Це спирається на емпіричне спостереження, яке називають локальністю посилань (locality of reference): програми зазвичай читають одні й ті самі дані багато разів підряд (тимчасова локальність), і читають дані, розташовані близько одне до одного (просторова локальність).

Спочатку зробили один рівень кешу (L1) — десятки кілобайт прямо в ядрі, 1-4 цикли на доступ. Потім додали L2 — більше, але повільніше. Потім L3, ще більше, ще повільніше. Потім саму RAM. Вийшла ієрархія пам’яті:

Рівень Розмір Час доступу
Регістри ~1 КБ <1 циклу
L1 кеш 32-128 КБ 3-5 циклів
L2 кеш 512 КБ – 2 МБ 10-20 циклів
L3 кеш 8-128 МБ 30-60 циклів
RAM 16-512 ГБ 200-400 циклів
SSD 1-8 ТБ ~100 000 циклів
HDD 1-20 ТБ ~10 000 000 циклів

Різниця між найшвидшим і найповільнішим рівнем — десять мільйонів разів. Якщо уявити час доступу до L1 як одну секунду, то доступ до HDD — це чотири місяці чекання.

Ось такий у нас комп’ютер під капотом. Ієрархічний. Нерівномірний. Повний пасток.

Кеш як тиха релігія

Тут починається цікаве. Кеш прихований від програміста. Ти ніколи не пишеш “поклади цю змінну в L1, а ту — в L3”. Ти просто пишеш int x = 5;. Процесор сам вирішує, де її тримати, коли витіснити, куди подівати.

Це — абстракція, яка тече. Вона виглядає як плоска пам’ять (як у фон Ноймана), але поводиться як ієрархічна. І якщо ти не знаєш, як вона працює, ти писатимеш код, який виглядає швидким, але працює повільно — бо весь час промахується повз кеш.

Класичний приклад. Маєш двовимірний масив 10000×10000 чисел. Треба пройти його і додати всі. Два способи:

// Варіант A: зовнішній цикл — рядки
for (int i = 0; i < 10000; i++)
    for (int j = 0; j < 10000; j++)
        sum += a[i][j];

// Варіант B: зовнішній цикл — стовпці
for (int j = 0; j < 10000; j++)
    for (int i = 0; i < 10000; i++)
        sum += a[i][j];

Вони дають однакову відповідь. Але варіант A зазвичай у 5-20 разів швидший за варіант B. Чому?

Бо в пам’яті масив лежить по рядках: a[0][0], a[0][1], a[0][2], ..., a[0][9999], a[1][0], a[1][1], .... Коли ти читаєш a[0][0], процесор завантажує в кеш не одне число, а цілий кеш-рядок (зазвичай 64 байти, тобто 8-16 чисел одразу). Варіант A читає далі a[0][1] — воно вже в кеші. Варіант B читає a[1][0] — це інша частина пам’яті, треба йти в RAM. І так на кожній ітерації.

Ти написав однаковий алгоритм. Отримав зовсім різну швидкість. Процесор тобі нічого не сказав. Кеш працював мовчки.

Є цілий напрямок у програмуванні під назвою data-oriented design (Майк Ектон з Insomniac Games — один з його головних проповідників), який говорить приблизно таке:

Перестаньте думати про програму як про “об’єкти і методи”. Думайте про неї як про потоки даних через кеш-ієрархію. Розташовуйте дані в пам’яті так, щоб CPU не мусив чекати. Це дасть 10-100× прискорення, і жодна “оптимізація алгоритму” з цим не зрівняється.

Це — прихована правда сучасного заліза, яку більшість програмістів у 2024 році досі не розуміють.

Чому GPU не потребує такої хитрої ієрархії

Коли з’явилися GPU, вони обрали зовсім інший підхід. Замість невеликого числа ядер зі складною ієрархією кешу — тисячі простих ядер із дуже широкою, дуже швидкою пам’яттю.

NVIDIA H100 має 80 ГБ HBM3-пам’яті, яка працює на пропускній здатності 3.35 терабайт на секунду. Для порівняння: звичайна DDR5 RAM у настільному ПК — 50 ГБ/с. Це в сімдесят разів повільніше.

GPU жертвує одним заради іншого: він не може швидко зробити одну операцію (затримка звернення до HBM у нього теж велика), але він може зробити тисячі операцій паралельно. Поки одне ядро чекає дані — друге працює, третє чекає, четверте працює. Так ховається затримка.

Саме тому сучасний AI живе на GPU. Нейромережа — це мільярди простих операцій (множення матриць), які можна розпаралелити і які впираються в пропускну здатність пам’яті, а не в складні алгоритми. GPU для цього ідеальний. Звичайний CPU — ні.

І саме тому великі мовні моделі такі дорогі. Вони не “складні обчислення”. Вони — величезна маса пам’яті, яку треба безперервно качати через обчислювальні блоки. Перевантажена шина. Привид фон Ноймана, що задихається від ваги.

Що приходить на заміну

Якщо перекачування даних — основна проблема, напрошується розв’язок: не перекачувати їх. Обчислювати там, де вони вже лежать.

Цей підхід називається обчислення у пам’яті (processing-in-memory, PIM, або in-memory computing). Замість того, щоб пам’ять просто зберігала дані, вона ще й виконувала над ними операції — прямо всередині, без транспортування до процесора.

Це не нова ідея — вона обговорюється з 1970-х. Але тільки зараз стає реальною: Samsung у 2021 році показав HBM-PIM — пам’ять із вбудованими матричними множниками. Micron працює над тим самим. IBM робить чіпи на основі memristor — нових елементів, які одночасно зберігають і обчислюють.

Інший підхід — нейроморфні процесори. Замість архітектури фон Ноймана — архітектура, яка імітує мозок: мільйони маленьких “нейронів”, кожен з власною локальною пам’яттю, з’єднані паралельно. IBM TrueNorth, Intel Loihi, SpiNNaker у Манчестері — це все проекти в цьому напрямку. Вони в сотні-тисячі разів енергоефективніші за традиційний CPU для задач типу розпізнавання, але їх важко програмувати (тут немає python), і вони погано підходять під більшість звичайних задач.

Третій напрямок — аналогові обчислення. Старий добрий аналог, про який забули у 1970-х, повертається саме для AI. Нейромережа — це по суті аналогове множення матриць. Його можна зробити напряму через фізику ланцюга, без двійкових обчислень, і це буде в сотні разів ефективніше. Компанія Mythic AI — один з прикладів (хоча в 2023-му в них були проблеми з фінансуванням). IBM і HP теж ведуть дослідження.

Усі ці напрямки — повстання проти привида фон Ноймана. Спроби сказати: “Ми будуємо неправильно сімдесят років. Пам’ять і обчислення повинні жити разом, а не через канал зв’язку”.

Що з цього винести

  1. Архітектура фон Ноймана (1945) зробила комп’ютер універсальним — але створила приховану залежність від каналу “процесор-пам’ять”.
  2. З роками цей канал став вузьким горлом. Сучасний CPU 50-90% часу чекає на пам’ять, а не обчислює.
  3. Кеш — хитра милиця, яка маскує цю проблему. Але він прихований від програміста і вимагає від тебе розуміння, якщо ти хочеш писати швидкий код.
  4. Те, що ти напишеш в один рядок Python, у пам’яті виглядає як десятки тисяч переміщень між L1, L2, L3, RAM — і більшість часу витрачається саме на ці переміщення, а не на сам “код”.
  5. GPU перемогли в AI не завдяки розумним алгоритмам, а завдяки пам’яті з широкою шиною і паралелізму, що ховає затримки.
  6. Майбутнє, можливо, в обчисленнях у пам’яті — переверненні архітектури фон Ноймана. Уже експериментують усі великі гравці.

У наступних розділах ми піднімемося на рівень вище — від фізики і пам’яті до архітектури процесорів. Подивимось на x86 — архітектуру, яка живе з 1978 року і досі тягне з собою усю свою історію у вигляді кучугури байтових інструкцій, якими ніхто вже не користується. Потім — на ARM, який народився як перемога простоти. Потім — на майбутнє, яке ще не настало: аналогові, квантові, нейроморфні обчислення.

Але спочатку — звідки взялася ця x86-чума, і чому вона досі тут.