[Перевод] Futhark в браузере
В IT так: если что-то существует, то рано или поздно это будет существовать и в браузере. Сегодня так устроен мир. Благодаря работе Филипа Лассена, теперь вы можете гонять Futhark у себя в браузере. В данном посте рассказано, как этого удалось добиться, и почему этот функционал пока не так полезен, как мог бы (спойлер: пока не поддерживается работа с GPU), и какие есть перспективы этот функционал доработать. Подробно о том, как спроектирован этот язык, рассказано в магистерской диссертации Филипа.
Когда-то те языки, которые предполагалось использовать в браузере, приходилось компилировать в JavaScript. К счастью, теперь ситуация изменилась. Сегодня все крупные браузеры поддерживают WebAssembly (WASM), вполне канонический байт-код на основе регистров, интероперабельный с JavaScript. WebAssembly генерируется примерно так же, как и ассемблерный код. Конечно же, компилятор Futhark в настоящее время генерирует C (или Python), а не ассемблер. Написать бекенд на WASM было бы не так сложно, но поддерживать его пришлось бы неопределённо долго. Кроме того, у Futhark (крошечная) система выполнения, написанная на C и обеспечивающая, в частности, управление памятью, профилирование и вывод отчётов об ошибках. Да, это не конец света, но хотелось бы обойтись без поддержки совершенно нового бекенда.
Хорошо, что есть Emscripten. Emscripten — это компиляторный инструментарий, позволяющий компилировать написанные на C программы в WASM. Он построен так, что использует Clang в качестве компилятора фронтенда на C, а также работает с бекендом от LLVM, который написан на WASM. Emscripten играет роль клея, скрепляющего все эти элементы именно таким образом, что полученная конструкция просто работает. Также Emscripten генерирует низкоуровневые обёртки, написанные на JavaScript, тем самым позволяя забирать результат. Я был не уверен в том, насколько качественно это сработает, но оказывается, что в большинстве случаев можно просто взять код C, полученный от Futhark в качестве вывода, передать его Emscripten — и получить готовый к исполнению код WASM, к которому можно обращаться прямо из JavaScript! Да, тут есть некоторые шероховатости (например, JavaScript не поддерживает напрямую 64-разрядные целые числа), которые требуется разгребать разными способами (подробнее см. в диссертации Филипа), но ничего чрезмерно обременительного.
Конечно же, при таком подходе получим в результате модуль JavaScript, предоставляющий эквивалент API на С для Futhark, в том числе, объекты, в сущности, являющиеся указателями на кучу WASM. Конечно, я представляю, каким невероятно высоким болевым порогом, должно быть, обзавелись JavaScript-разработчики — учитывая, в какой среде им приходится разработать, но даже с их точки зрения здесь «мост слишком далеко». Таким образом, самой сложной частью WASM-бекенда для Futhark является автоматическая генерация относительно идиоматического API на JavaScript для скомпилированной программы на Futhark.
Например, написанную на Futhark программу futlib.fut
можно скомпилировать командой $ futhark wasm --lib futlib.fut
, которая сделает файлы futlib.wasm
и futlib.mjs
. Последний можно импортировать из JavaScript при помощи:
import { newFutharkContext } from './futlib.mjs';
newFutharkContext().then(ctx => ...);
Параметр ctx
— это экземпляр FutharkContext
. В этом классе будет предусмотрено по соответствующему методу для каждой входной точки в оригинальной программе. Массивы на стороне Futhark будут представлены специальным классом FutharkArray
, который можно преобразовать в типизированный массив JavaScript. Это делается, чтобы не приходилось (без абсолютной необходимости) копировать значения Futhark из кучи WASM в JavaScript. Эти значения будут просто передаваться из одной записи Futhark в следующую, поэтому нет необходимости их копировать. Всё это очень похоже на тот API, который мы предоставляем в генераторе кода Python. Эта тестовая программа хорошо помогает составить впечатление о практической стороне дела. А если вы хотите реально увидеть, как Futhark будет работать у вас в браузере, отправляйтесь сюда. Понастраивайте ползунки — и должен появиться фрактал, отображённый Futhark.
Параллельный WASM
Команда futhark wasm
сгенерирует последовательный код WASM. Генератор последовательного кода Futhark хорош, но очевидно неудовлетворителен для параллельного языка. Бекенд Futhark multicore
генерирует C, работающий с потоками POSIX (pthreads) в качестве базового потокового API. Можно ли просто передать его Emscripten? Вы удивитесь, но да, можно! Emscripten распознаёт API pthreads и реализует его при помощи комбинации веб-воркеров и SharedArrayBuffer. Так что наш бекенд futhark wasm-multicore
, в сущности, выполнит генерацию кода multicore
и передаст результат в Emscripten. Сгенерированный для JavaScript API будет таким же, как и для futhark wasm
.
Разумеется, это веб-программирование, поэтому всё работает несколько хуже, чем здесь описано. API SharedArrayBuffer сильно ограничен, так как потенциально может использоваться как вектор атаки SPECTRE, поэтому, если собираетесь использовать его на сайте, то нужно предпринимать специальные меры.
Всё-таки, если вы готовы помучиться, то попробуйте, каково выполнять Futhark в браузере на многих ядрах. Предварительный бенчмаркинг, выполненный в диссертации Филипа подсказывает, что наш планировщик потоков работает как в браузере, так и нативно. Но следует ожидать, что где-то на этом пути нас поджидает неприятный сюрприз.
К сожалению, никто ещё не заморочился настолько, чтобы заставить Futhark работать на GPU прямо в браузере.
GPU WASM
Начнём с того, что браузеры действительно допускают выполнение программ для GPU («шейдеров») в коде JavaScript. Учитывая качество драйверов для GPU, этот опыт, пожалуй, умеренно ужасен, но давайте об этом не будем. Есть два низкоуровневых API для GPU-программирования в браузере:
- WebGL — это, в принципе, стреноженная OpenGL ES. Поддерживается достаточно широко, но сам API (как и OpenGL) достаточно старомодный и не слишком хорошо подходит для современных GPU. Шейдеры пишутся на диалекте GLSL.
- WebGPU — это пока новинка, то есть, ещё не слишком широко поддерживается. Это более современный API, который не моделировался непосредственно с какого-либо нативного API, но по духу он сближается с Metal и Vulkan, хотя и гораздо проще последнего. Шейдеры пишутся на специально разработанном языке WGSL, и это небесспорное решение. Но, в конечном счёте, он очень похож на другие низкоуровневые шейдерные языки, такие как SPIR-V.
Как WebGL, так и WebGPU ориентированы преимущественно на работу с графикой, но WebGPU также явственно стремится обеспечивать качественную поддержку вычислительных шейдеров, а именно это и требуется Futhark. Поскольку, как кажется, за WebGPU будущее (если ещё не настоящее), именно в этом направлении мы и двинемся.
Современные GPU-бекенды для Futhark генерируют код OpenCL или CUDA. Может быть, нам повезло настолько, что Emscripten может просто переводить для нас эти API, как и в случае с pthreads? Нет. Но есть заманчиво похожая возможность. Смотрите, WebGPU — это не просто API JavaScript; нашлись умельцы, также определившие webgpu.h, API для C, предоставляемый нативными реализациями WebGPU, например, wgpu-native. При помощи WebGPU C API среда Emscripten может перевести программу C в программу WASM/JavaScript, вызывая API WebGPU JavaScript. Вот это круто. Таким образом, если мы сконструируем бекенд для компилятора Futhark, генерирующий код C и при этом вызывающий функции webgpu.h, то сможем компилировать его при помощи Emscripten и выполнять в браузере. Сколько же работы потребуется, чтобы создать такой бекенд?
К сожалению, эта работа не так тривиальна. По причинам, связанным с поддержкой, было бы наиболее удобно, если бы выдаваемый нами код для использования на уровне хоста (то есть, код, не используемый на GPU) был бы относительно схож на всех бекендах GPU. Кстати, генерируемый Futhark код для хоста получается не слишком сложным, но требует возможности обеспечивать полную синхронизацию, что пока не поддерживается WebGPU. Возможно, потому, что обычно принято подрубаться в цикл событий JavaScript, что также делается и в Emscripten. Это артефакт WebGPU, по-прежнему незрелый, и пока его желательно обходить.
Ещё интереснее такая проблема: как генерировать код, который мог бы работать на GPU — шейдеры (или ядра в терминологии OpenCL/CUDA). Как CUDA, так и OpenCL поддерживают подмножество C для написания кода GPU, поэтому поддерживать одновременно два этих генератора кода оказалось на удивление просто. Язык WGSL выглядит совершенно иначе (как минимум, в синтаксическом отношении), но на базовом уровне следует очень похожей модели программирования. Написать и поддерживать небольшой генератор кода WGSL в принципе не сложно — ключевые элементы нашего генератора C занимают менее 300 строк. Сложнее всего, что Futhark требуются возможности, не поддерживаемые WGSL. Что же это за детально проработанные возможности, ожидаемые в таком тепличном и академичном функциональном языке как Futhark, которые не в состоянии поддерживать никакой прагматичный и реалистичный шейдерный язык? Речь о таких изысках как:
- 8-разрядные, 16-разрядные и 64-разрядные целые числа.
- Числа с плавающей точкой, обладающие двойной точностью.
- Сравнение с заменой.
- Указатели.
Некоторые из этих вещей можно обойти. Указатели нужны, поскольку у нас предусмотрена оптимизация для повторного использования памяти, а некоторый блок памяти в начале работы программы может применяться для хранения 16-разрядных целых чисел, а позже — для чисел с плавающей точкой одиночной точности. Для этого потребуется «приводить» указатели внутри программы в сгенерированном коде (или просто организовать произвольные загрузки с нетипизированных адресов), но при необходимости такую оптимизацию можно и отключить.
Числа с плавающей точкой, обладающие двойной точностью — также не жёсткое требование. Поскольку даже в OpenCL некоторые GPU их не поддерживают, у нас есть код, условно активирующий их лишь в тех случаях, когда в программе без них не обойтись. Причём, решение, когда их использовать, всегда принимает программист.
Более серьёзная проблема — ограниченность целочисленных типов в WGSL. Одна сложность связана с тем, что Futhark использует 64-разрядные целые числа любого размера. Подозреваю, можно симулировать 64-разрядные целые как пары 32-разрядных целых и даже хранить их в памяти в таком виде. Что касается 8-разрядных и 16-разрядных целых — в скалярном коде их можно с лёгкостью симулировать в виде 32-разрядных целых чисел. Фактически, поскольку в реальных GPU нет 16-разрядных регистров или АЛУ, именно так они обычно и компилируются. Проблема здесь в том, каково представление в памяти. В Futhark ожидается, что в программе будут массивы 8-разрядных целых чисел, по байту на элемент, и что запись в элемент будет атомарной. Нельзя просто записать целиком 32-разрядное целое число, чтобы обновить 8-разрядный элемент, поскольку так мы частично затрём соседние элементы.
Futhark использует 8-разрядное целое число для метки конструктора в тип-сумме, так что даже если в вашем коде не используется никаких 8-разрядных целых чисел, они могут найтись в сгенерированном коде. Кроме того, применяемое в Futhark выравнивающее преобразование таково, что, даже если в вашем оригинальном коде ни разу явно не конструируется массив 8-разрядных целых чисел, компилятор всё равно может делать такие массивы. В этом суть компилятора, которую мы не можем взять и отключить на каком-нибудь перспективном бекенде WebGPU.
Пока не знаю, как с этим справиться. В первичном прототипе эту проблему определённо можно просто проигнорировать, поскольку она всплывает не в каждой программе. На долгосрочную перспективу в WGSL действительно зарезервированы ключевые слова, соответствующие недостающим типам целых чисел, поэтому, возможно, их поддержку удастся обеспечить.