科学・技術
パンゲノミクス向けフェイクシェル
A Fake Shell for Pangenomics (cs.cornell.edu)
要約
この記事では、筆者が開発したパンゲノミクスツール「FlatGFA」をゲノム研究者に使いやすくするための「フェイクシェル」について解説しています。FlatGFAはゼロコピーのデータ形式により高速ですが、一般的なCLIやRust APIでは合成の限界や開発の難しさがありました。そこで、Unixシェルスクリプトの利便性とRust APIのパフォーマンスを両立させる、パイプラインのデータをインメモリで処理する新しいアプローチが提案されています。
全文翻訳
私はパンゲノミクス向けの効率的なツールキットであるFlatGFAに取り組んできました。odgiのような他のパンゲノミクスツールと比較して、FlatGFAにはゼロコピーデータ形式という唯一のトリックがあります。インメモリデータ形式がオンディスク形式と同一であるため、FlatGFAはすべてのシリアライズとデシリアライズのコストをスキップできます。ファイルを開くことはmmap(2)で構成されます。不公平に選ばれたワークロードでは、FlatGFAはodgiよりも数千倍高速になる可能性があります。
さて、ここからが大変な部分です。ゲノム学者である同僚にFlatGFAを実際に使ってもらいたいのです。私は高性能な操作のインベントリを作成し、実際の科学者たちがそれらを組み合わせて完全なワークフローを構築できるようにしたいと考えています。彼らにその種の合成を可能にするためには、2つの簡単な選択肢がありました。
(1) すべての演算子を公開するコマンドラインインターフェースを作成し、科学者たちがシェルスクリプトを書いてそれらを組み合わせるか、
(2) Rust APIを設計し、科学者たちがRustコードを書くか。
どちらの選択肢もあまり魅力的ではありません。CLIアプローチは、できる合成の種類を制限します。すべての中間データはファイルかパイプである必要があり、これは扱いにくくなる可能性があり、確実にある程度のオーバーヘッドを伴います。我々の内部Rust APIは、私たちが用いるすべてのデータ構造トリックのために、愛らしく独特なスタイルを使用しています。生物学者の共同研究者たちは優れたRustハッカーですが、彼らが喜んで使うような良いAPIがあるとは良心的に言えません。この記事は、私たちが最近構築した非常に馬鹿げた代替案についてです。それは、オプション1を提供しているふりをしながら、オプション2のパフォーマンスを近似するフェイクシェルです。
Ousterhoutの二分法について
長い間、FlatGFAのようなパフォーマンス指向のライブラリを「パッケージ化」する正しい方法は、標準的なOusterhoutの二分法を用いることだと考えていました。パフォーマンスに敏感なルーチンはRustに残し、それらのルーチンを全体のワークフローに合成するための高レベル言語へのバインディングを構築します。その結果はPyTorchによく似たものになるでしょう。Pythonがあまり高速でなくても、MLエンジニアにとっては問題ありません。なぜなら、時間の99%以上はC++とCUDAで書かれた最適化されたカーネルルーチンで費やされるからです。Pythonは、現代においてOusterhoutの二分法における「グルー言語」の役割として自然な選択です。1 そこで、私たちは素晴らしいPyO3プロジェクトを使ってFlatGFAのPythonバインディングの構築を始めました。基本的な部分はかなりうまく機能しました。
例えば、これを試して動作を確認してください:
```bash
$ curl -LO https://raw.githubusercontent.com/pangenome/odgi/refs/heads/master/test/LPA.gfa
$ uv run --with flatgfa python
>>> import flatgfa
>>> graph = flatgfa.parse("LPA.gfa")
>>> [path.name for path in graph.paths]
```
しかし、Pythonバインディングにはいくつかの深刻な欠点がありました。
PyO3を使っても、バインディングを効率的に書くのは難しいことです。問題は、Rustの静的なライフタイムとPythonの動的に管理されるヒープとの間の根本的な不一致にあります。FlatGFAのパフォーマンス上の利点は、コピー、アロケーション、ポインタチェイシングを排除することから生まれますが、これらはRust/Pythonの境界で再び忍び込んできます。
ワークロードの全体像を把握できません。素直なPythonバインディングでは、高速化の機会はライブラリの各呼び出し内のみであり、複数の呼び出しにわたって多くを行うことはできません。たとえば、ユーザーがFlatGFAデータ構造を反復するPythonのforループを書いた瞬間、ほぼ確実にパフォーマンス競争に負けます。これは、たとえばPyTorchが別途、オプションのコンパイルモードを持っているのと同じ根本的な理由です。
結局のところ、私たちの生物学者の共同研究者たちは、そもそもPythonにそれほど熱心ではありません!パンゲノムパイプラインを構成する伝統的で馴染み深い方法は、Unixシェルを使うことです。個人的には、Pythonが親しみやすさのデフォルトの選択肢であることに慣れすぎていました。当然のことながら、ドメイン専門家の好みは文脈によって異なり、多岐にわたります。odgiなどがすべて使用するCLI指向のアプローチを再検討することは理にかなっていました。
シェルを再考する
この分野でのシェルベースの構成の例を見てみましょう。odgiドキュメントのチュートリアルでは、odgi自体とbedtoolsの演算子を組み合わせて、ヒト染色体8の反復配列を見つける方法を示しています。
```bash
odgi depth -i chr8.pan.og -r chm13#chr8 | \
bedtools makewindows -b /dev/stdin -w 5000 > chm13.chr8.w5kbps.bed
odgi depth -i chr8.pan.og -b chm13.chr8.w5kbps.bed --threads 2 | \
bedtools sort > chr8.pan.depth.w5kbps.bed
```
フル機能の動的スクリプト言語よりもシェルスクリプトを好むのは奇妙に思えるかもしれませんが、このようなシェルスクリプトにはPythonよりもいくつかの実質的な利点があります。
適切な状況では、パイプを介したストリーミングI/Oは大規模データセットに非常に適しています。
シンプルなパイプライン並列処理は簡単に表現できます。
中間結果をファイルに永続化するのが簡単です。
シェルは究極のグルー言語のようなものです。別々に開発され、異なる言語で書かれたコンポーネントを、特別なバインディング作業なしで結合できます。(唯一の「バインディング」はUnixユーザーランドAPIです。)
この例のワークフローでは、2つの異なるパッケージから4つの演算子、2つのUnixパイプ、1つの中間ファイルを使用しています。この例ではそれほど重要ではないと思いますが、シェルパイプラインによって2組のコマンドを並行して実行できるのは良い点です。Greenbergらの言葉を借りれば、シェルは実際には良いものです。2
しかし、1つの巨大な欠点があります。操作間でデータを交換する唯一の方法はファイルとパイプです。ファイルは、すべてのバイトがメモリに快適に収まる場合でも、不要なディスクへの書き込みを伴うことがあります。パイプはディスクI/Oを回避でき、ストリーミング演算子には非常に適していますが、一般にすべてをテキストにシリアライズする必要があり、すべてのプロデューサーとコンシューマーの関係が自然にストリーミングをサポートするわけではありません。たとえば、あるコマンドが新しいバリエーショングラフ(新しいGFAファイル)を生成する場合、次のコマンドは作業を開始する前にそれをすべて読み込む必要があるでしょう。
パンゲノミクスに関する助成金の週次会議で、グループはシェルベースの合成のこれらの根本的な限界について少し熱い議論になりました。OSのディスクキャッシュはファイルI/Oのコストをほとんど軽減できるでしょうか?RAMディスクにファイルを置くことで強制できますか?(RAMディスク上のファイルをmmapするとどうなるのでしょうか?)データセットがメインメモリをあふれるほど大きくなった場合、これらはどれも実用的ではないのでしょうか?すべての基礎となるコマンドをテキストベースのストリーミングではなくゼロコピーバイナリファイル形式を使用するように変更した場合、これらすべてのトレードオフはどうなるでしょうか?そのアプローチはスクリプトをより複雑にし、パイプライン処理のすべての利点を犠牲にするでしょうか?その議論の中で、私はこれらすべての欠点を回避できる、ばかげていて非実用的ながらも非常に楽しい代替案があることに気づきました。
余談:ベクトル化されたインタープリタ
2023年、Graydon HoareはUCSCで「ベクトル化されたインタープリタ」について講演し、私に大きな感銘を与えました。3 彼は、ネイティブコードコンパイラ(特にJIT)がコードからパフォーマンスを引き出すための非常に複雑な方法であると指摘しています。私に印象に残ったのは、プログラミングモデルからの適切な協力があれば、一括で動作するインタープリタがシンプルで高速な代替手段になり得るというアイデアでした。バイトコードの各命令が大量のデータに対する大きな計算(たとえば、単一のスカラー整数加算ではなく)を表す場合、そのバイトコードを素直に解釈するだけで十分に効率的です。たとえば、時間の99.99%がそれらの分厚い命令の実装の実行に費やされる場合、バイトコード命令のディスパッチのコストを心配する必要はありません。Graydonのプレゼンテーションでは、PyTorchとNumPyはどちらもベクトル化されたインタープリタの例です。しかし、上で述べたように、それらはPythonのプログラム表現とインタープリタを再利用しているため、アドレス指定可能な「命令ウィンドウ」が制限されています。私は、パンゲノミクス操作のために特注のベクトル化されたインタープリタでより良い方法があるはずだと考えていました。そして、シェルスクリプトワークフローの問題が、それについて何かを試す言い訳となりました。
フェイクシェル
そのアイデアは、フェイクシェルを構築することです。POSIXのほんの一部しかサポートしていません