Мой недавний набег на мир ИИ с такими проектами, как этот, раскрыл аспект Dart, с которым у меня не было проблем до сих пор, а именно загрузку больших файлов, особенно файлов размером более 0x7ffff000 (2 147 479 552 байта). Комментарий в файле file_impl.dart гласит: -
On Linux, the `read` will // transfer at most 0x7ffff000 bytes and return the number of bytes actually. // transfered.
Это ограничение распространяется и на другие системные вызовы, такие как write и sendfile.
Все большие файлы языковых моделей, с которыми я сталкивался, больше этого, конкретный файл, используемый в проекте Alpaca, имеет размер 3,9 ГБ, размер квантованной модели Falcon 40B, которую я сейчас рассматриваю, имеет размер 24,4 ГБ. В общем, чем больше параметров в модели, тем она больше. Эти файлы должны быть загружены и проанализированы вашим приложением, прежде чем можно будет выполнять какие-либо логические выводы ИИ.
Первоначально я, конечно, забыл об этом ограничении, и использование readAsBytesSync быстро показало, что я не могу использовать это, поэтому я переключился на использование асинхронного метода RandomAccessFile, читая более мелкие фрагменты и перемещая указатель файла традиционным способом. Несколько удивительно, что это, однако, показало точно такое же поведение, когда был достигнут предел 0x7ffff000.
В этот момент я думал об использовании, возможно, потокового считывателя для этого, пример которого показан в документации Dart File API, когда я внезапно подумал: Зачем я это делаю? Я знаю по прошлому опыту, что загрузка больших файлов в системах Unix и Linux лучше всего использовать mmap для загрузки файла в виртуальное адресное пространство процессов, а не для чтения этого в том, что в Dart фактически было бы большим UInt8List.
Система файлового ввода-вывода Dart не поддерживает mmap’ирование, поэтому я искал другое решение, к счастью, этот проект в настоящее время предназначен только для Linux, и я быстро нашел pub-пакет stdlibc, предназначенный для Linux и macOS. Существует Windows-эквивалент mmap, использующий файлы с отображением памяти.
Итак, код, который я реализовал, просто стал: -
# Load model parts for (int i = 0; i < nParts!; ++i) { final partId = i; var fnamePart = fname; if (i > 0) { fnamePart += '.$i'; } print( 'llamaModelLoad:: loading model part ${i + 1}/$nParts from "$fnamePart"'); int fileLength = stdlib.stat(fnamePart)!.st_size; if (fileLength <= 0) { print( 'llamaModelLoad:: failed to stat model part ${i + 1}/$nParts from "$fnamePart"'); return false; } final partFd = stdlib.open(fnamePart); if (partFd < 0) { print( 'llamaModelLoad:: failed to open model part ${i + 1}/$nParts from "$fnamePart"'); return false; } final pBufMapped = stdlib.mmap( length: fileLength, fd: partFd, prot: stdlib.PROT_READ, flags: stdlib.MAP_PRIVATE); if (pBufMapped!.data.lengthInBytes <= 0) { print( 'llamaModelLoad:: failed to mmap model part ${i + 1}/$nParts from "$fnamePart"'); } final bData = ByteData.view(pBufMapped.data); ......
Который просто регистрирует файл, чтобы получить длину, открывает его и mmap. Вы должны закрыть и munmap его, когда закончите, конечно. Это отлично сработало и было очень эффективным.
Теперь я думаю, что Dart действительно нуждается в отображении файлов, встроенном в среду выполнения, а не в том, чтобы полагаться на пакеты pub для этого. В SDK есть открытая проблема с просьбой об этом. Впервые это было поднято в августе 2022 года, чтобы добавить методы readMapped и readMappedSync в интерфейс RandomAccessFile. Полная история по этому вопросу, я просто добавил комментарий в конце, предполагая, что распространение больших файлов языковых моделей еще больше оправдывает это изменение. Будем надеяться, что это произойдет.
Забавно, не правда ли, когда вы сталкиваетесь с проблемой и решаете ее, вы часто получаете лучшее решение, чем то, о котором вы подумали сначала, напоминает мне одну старую пословицу из мира шахмат:
«Если вы видите хороший ход, ищите лучший».