Fidget: 大規模な数式評価ライブラリ
(mattkeeter.com)- Fidgetは、数百〜数千の算術節からなる数式を表現・コンパイル・評価する Rust ライブラリで、主な用途は陰関数曲面バックエンド
- 陰関数曲面は $f(x,y,z) \rightarrow d$ 形式の距離関数で内側と外側を区別し、CSG 演算や並列評価と相性が良い
- フロントエンドは Rhai スクリプトから数式ツリー、DAG、SSA テープ、再利用レジスタベースのバイトコードへと下るパイプラインを提供
- バックエンドはインタープリタと JIT コンパイラを提供し、単一点・SIMD 配列・順方向自動微分・区間演算評価をサポート
- 7867 個の式の 1024² brute force 評価では JIT が 5.8 秒を 182ms に短縮したが、最適化レンダリングでは 6ms と 4.6ms で差は約 25% まで縮小
Fidget の目標と陰関数曲面
- Fidgetは、大規模な数式を表現・コンパイル・評価するためのライブラリ
- 数百〜数千の算術節を扱う式を対象としている
- 主な用途は陰関数曲面バックエンドだが、他の用途にも使える
- 陰関数曲面は $f(x, y, z) \rightarrow d$ 形式の式で、単一の距離値 $d$ を返す
- $d$ が正なら点 $(x,y,z)$ はモデルの外側にある
- $d$ が負なら点はモデルの内側にある
- 半径 1 の球は $\sqrt{x^2 + y^2 + z^2} - 1$ と表現できる
- Fidget は、基本的な算術演算で式を構成する閉形式の陰関数曲面に注力している
- ピクセルシェーダの GLSL のように、チューリング完全なプログラムで距離値を計算する方式とは対照的
- こうした関数は手で直接書くというより、上位表現がターゲットにしやすい「shape の assembly language」に近い
陰関数曲面が有利な点
- 陰関数曲面は簡潔で並列評価に適している
- SIMD 命令や GPU を使った大規模並列評価に向いている
- CSG 演算が単純になる
- mesh や NURBS では難しい union や intersection のような演算を容易に表現できる
- 正確に重なった 2 本の円柱の和集合は
min(a, b)で表現される
- 閉形式の方程式は最適化の機会を生む
- Fidget は評価中にどの分岐が選ばれたかを示す実行トレース(trace) をキャプチャできる
- このトレースを使って式を単純化し、その後の評価コストを下げる
libfive の後に新しく作った理由
- Fidget は既存の
libfiveカーネルを置き換えるために新たに作られたライブラリlibfiveは約 40K 行の、ほとんどが C++ のコードで構成されている- 作者本人にとっても修正が難しく、数か月後に再コンパイルするとビルドが壊れて CMake をいじる必要が生じることが多かった
- 新実装は、現在興味深い問いを実験するための基盤でもある
- 陰関数カーネルに適した API を探り、互換性を壊す可能性も許容している
- GPU に移さずに性能を高めるため、ネイティブ JIT コンパイルを試している
- WebAssembly へクロスコンパイルし、アクセスしやすい Web デモを作れる
- Fidget はRustで書かれている
cargo build1 つでコンパイルできる- WebAssembly へ自然にクロスコンパイルできる
- Rust の強力な型システムとメモリ安全性により、リファクタリングの信頼性が高い
フロントエンド: スクリプトからバイトコードまで
- Fidget のフロントエンドは、入力スクリプトからバイトコードまで下るパイプラインを提供する
- ユーザーがこの全体の流れを必ずたどる必要はなく、中間段階のどこからでもライブラリを使える
-
Rhai スクリプティング
- Fidget は Rust 向け組み込みスクリプト言語 Rhai のバインディングを含む
- 演算子オーバーロードにより、数式をスクリプト内で構築できる
drawに渡される値は、スクリプトが作った式を表す数式ツリーである
-
ツリー、グラフ、SSA テープ
- 数式ツリーは重複除去を経て有向非巡回グラフ(DAG) に変換される
- トポロジカルソートによってグラフを直線的なコードへ平坦化する
- このコードはSSA(single static assignment) 形式である
- 任意個数の仮想レジスタ
rXがあり、各レジスタには 1 度だけ書き込まれる - SSA テープも評価できるが、各演算ごとにメモリ位置が必要なためスケーラビリティに欠ける
- 仮想レジスタが再利用されないためである
-
バイトコードとレジスタ割り当て
- 評価効率を高めるため、仮想レジスタを再利用可能な物理レジスタにマッピングする
- 例の SSA テープは 6 個の再利用レジスタに圧縮できる
- レジスタ割り当てには、以前紹介されたsimple algorithmを使う
- 単一パスのアルゴリズムで、効率より速度と決定性を優先する
- バイトコードインタープリタは 256 個のレジスタを使う
- レジスタインデックスは
u8に保存される - レジスタが不足すると、アロケータが
LOADとSTOREを挿入してu32インデックスの補助メモリに記録する
バックエンド: 評価方式と単純化
- Fidget のバックエンドは
Function、TracingEvaluator、BulkEvaluatorトレイトによってフロントエンドから分離されている- アルゴリズムは数式ツリー実装に密結合せず、一般的な
Functionを対象にできる - 現在
Functionトレイトの非数式ツリー実装は存在しない
- アルゴリズムは数式ツリー実装に密結合せず、一般的な
- 現在の数式ツリー評価方式は 2 つある
- バイトコードインタープリタ
- JIT コンパイル関数
-
評価モード
- Fidget は 4 つの評価モードを提供する
- 単一点評価
- 配列ベースのSIMD評価
- 順方向自動微分
- 区間演算
- bulk 評価では、ユーザーが入力値配列を渡し、出力配列を受け取る
- JIT バックエンドは SIMD コードを生成し、
AArch64では 4 個、x86-64では 8 個の要素を一度に処理する
- Fidget は 4 つの評価モードを提供する
-
順方向自動微分
- 微分評価器は値と最大 3 個の偏微分を計算する
- 陰関数曲面では通常 $(f, \partial f/\partial x, \partial f/\partial y, \partial f/\partial z)(x,y,z)$ を計算する
- 曲面上で $f(x,y,z)=0$ のとき、偏微分は表面法線の良い近似になる
- この値はシェーディングに使える
- 評価は順方向自動微分で行われる
- レジスタ値に微分値を付加し、各段階で連鎖律を適用する
- JIT 評価器は値と 3 個の微分を 1 つの
4 x f32レジスタに格納する
-
区間演算
- 区間演算は単一の入力値の代わりに入力値の範囲を評価する
- たとえば $x=1$ の代わりに $1 \le x \le 5$ を使える
- 出力も $2 \le f(x,y,z) \le 20$ のような区間になる
- 区間演算の結果は保守的である
- 実際の関数範囲を密に包めないことがある
- それでも、与えられた入力区間内で起こりうるすべての出力は含む
- 陰関数曲面評価では区間演算が中核的な構成要素である
- 空間領域を $x,y,z$ 区間として評価したとき、出力区間が明らかに 0 より大きければ、その領域全体は形状の外側なのでそれ以上見る必要がない
-
トレースベースの単純化
- 区間演算評価器は実行トレース(trace) もキャプチャする
min(a,b)で $0 \le a \le 1$, $4 \le b \le 5$ なら、aが常に小さいため式をaに単純化できる- 各
minとmax演算は、結果に影響する引数を記録する - 左、右、両方のいずれかを選択として記録する
- この選択を元の関数の単純化に使う
- Fidget は CSG に使われる
min・maxと、論理演算and・orの単純化をサポートする - CSG や論理がない形状では単純化の利点は得られない
- それでも、区間演算ベースで空の領域や満杯の領域をスキップできる利点は残る
区間演算とテープ単純化の組み合わせ
- 区間演算とテープ単純化の組み合わせは、大規模な式を扱いやすくする中核的な手法である
- 非アクティブな空間領域をスキップし、残ったアクティブ領域の評価もより低コストにする
- テープ単純化は、特定の空間領域でのみ有効な単純化済み式を計算する方式である
- 一般的なレイトレーシングの高速化構造と異なり、評価中に動的に高速化構造を作ることに相当する
- ラスタライズでは、区間評価のコストは複数のピクセルに分散される
- $N \times N$ ピクセル領域の区間評価はテープ長 $T$ に比例する $O(T)$ である
- ピクセル数には依存しない
- 小さな領域まで降りてからピクセルごとの評価を行う際には、はるかに短くなったテープを使う
- $M \times M$ 領域でのコストは $O(T' \times M \times M)$ である
- ここで $T' < T$ である
- 256×256 の 2D
hello, worldレンダリング例では、元のテープは 254 命令である- 32×32 ピクセルタイル 64 個を区間評価する
- 空の領域をスキップし、47 個のアクティブタイルが残る
- アクティブタイルの平均テープ長は 73 命令まで短くなる
- 各タイルを 16 個の 8×8 ピクセルタイルに細分化する
- 8×8 ピクセルタイル 752 個を区間評価する
- 空の領域をスキップし、351 個のアクティブタイルが残る
- アクティブタイルの平均テープ長は 20 命令まで短くなる
- 残った 351 個の 8×8 タイルはピクセルごとの評価を行う
- 32×32 ピクセルタイル 64 個を区間評価する
- ピクセルごとの評価時点では、テープは元の長さより10 倍以上短くなる
JIT コンパイル
- バイトコードインタプリタはタイトなループだが、避けられないオーバーヘッドがある
- 命令ディスパッチは予測しにくい単一分岐である
- 各命令は VM 評価器のレジスタスロットを介してメモリを読み書きする
- Fidget は最大性能のため、バイトコードをマシンコードへ落とし込む JIT コンパイラを含む
- マシン命令はディスパッチ不要の直線的なコードである
- 物理レジスタを直接使うため、メモリの読み書きが減る
- JIT の入力は従来と同じバイトコードテープである
- VM の標準 255 レジスタの代わりに、
x86-64では 12 個、AArch64では 24 個の物理レジスタに合わせて計画する AArch64ではv8-31、x86-64ではxmm4-15にマッピングする
- VM の標準 255 レジスタの代わりに、
- 各 opcode × データ型 × アーキテクチャの組み合わせごとにアセンブリ断片を直接記述する
- 必要な物理レジスタを断片にパッチし、
mmapされたメモリ領域へコピーする
- 必要な物理レジスタを断片にパッチし、
- Rust レベルでは、生成したメモリを関数ポインタにキャストして呼び出す
- 入力と出力は Rust の slice を raw pointer にキャストして渡す
-
性能値
- 7867 個の式からなる複雑な例では、1024² ピクセルの brute force 評価で JIT の効果が大きい
- バイトコードインタプリタ: 5.8 秒
- JIT バックエンド: 182ms
- 高速化: 31 倍
- brute force は区間演算やテープ単純化を利用しない
- より賢いアルゴリズムを使うと差は縮まる
- Fidget の最適化レンダリング実装は、同じ画像をバイトコードインタプリタで 6ms、JIT バックエンドで 4.6ms で描画する
- この場合の改善幅は約 25% である
レンダリングとメッシュ生成
-
レンダリング
- すべてのモデルレンダリングは
fidget::renderのアルゴリズムを使う - レンダリングは SIGGRAPH 論文の中核アルゴリズムを使う
- 大きな空間領域を区間演算でレンダリングする
- トレースに基づいて短縮されたテープを生成する
- 曖昧な領域は細分化して再帰処理する
- 3D レンダリングでは偏微分で法線を計算する
- モデルはレンダリング過程で 4×4 同次行列により変換される
- 透視変換をサポートする
- レンダリング結果は通常、heightmap とピクセルごとの法線という 2 枚の画像である
- 結果は SSAO など標準的な遅延レンダリング手法で描画できる
- すべてのモデルレンダリングは
-
メッシュ生成
- Fidget はメッシュ生成のために Manifold Dual Contouring を実装している
- この実装は常に次の特性を持つメッシュを生成することを目指す
- watertight
- manifold
- 鋭い edge と corner の保持
- 大部分が平坦な領域では三角形密度が低くなる adaptive な特性
- 既知の欠点もある
- 薄い特徴を必ずしも保持しない
- 生成されたメッシュが自己交差を含むことがある
- 頂点配置が adversarial cases に弱い
- 任意の陰関数曲面に対する優れたメッシュ生成は、依然として未解決問題である
- Manifold Dual Contouring は完璧ではないが、単純さと性能のバランス点にある
デモと Web GUI
- Fidget repository には複数の demos が含まれる
- Web GUI が最も興味深いデモとして紹介されている
- シンプルな CLI の
fidget-cliと、ネイティブのスクリプトビューアfidget-viewerもある
- Web デモは複数の Web 技術を組み合わせている
- GUI は TypeScript で書かれている
- Fidget crate はイベントループを主導せず、ライブラリとして使われる
- テキストエディタは CodeMirror を使う
- Node モジュールを使うためにバンドラが必要で、webpack を選んだ
- スクリプト評価とレンダリングは、メインイベントループを塞がないように web worker で実行される
- ブラウザに
std::threadがないことを回避するため、wasm-bindgen-rayonでレンダリングを並列化する - worker とメインイベントループはメモリを共有する
- ユーザーが新しい入力を与えると、worker と共有された
Arc<AtomicBool>フラグで長時間かかるレンダリングを中断できる
- 複数の構成要素を連携させる過程は難しかった
- 各構成要素には動作例があったが、バンドラ、設定、サーバーなどが互いに異なっていた
wasm-bindgenの最近のバグ修正によって、wasm-bindgen-rayonに必要だった挙動が変わり、古いバージョンを固定する必要があった
- Web デモはスマートフォンでも動作する
- マウスイベントを使っているため、カメラ操作はサポートされない
デモとライブラリの間の緊張関係
- Fidget はまず第一にライブラリである
- ユーザーはデモを実際の CAD ツールとして使うより、自分のプロジェクトにインフラとして組み込むことが想定された使い方である
- しかし、デモを触る人はライブラリでツールを作る人よりはるかに多い
- 中にはデモを設計作業に使うほどの人もいる
- より大きいデモ利用者層のためにデモを改善するか、より小さいツール開発者層のためにライブラリを改善するかの間に緊張がある
- カーネルと完全な CAD UI を同時に維持するのは難しく、デモの範囲は次第に縮小している
- Web エディタのような「最小限」のデモでさえ、かなりのプロジェクトである
- 計画は 3 つの方向にある
- 動機と集中力を維持するため、関心のあることを引き続き追う
- ツール利用者からの提案は受け取るが、期待値は現実的に設定する
- 理想的には、デモの負担を減らしてくれるツール開発者からのフィードバックを優先する
今後の可能性
-
GPU バックエンド
- GPU バックエンドは自然な拡張である
- 関連する SIGGRAPH 論文がすでにある
wgpu-bytecodeブランチですでに実装されている- Apple M1 Max ノートPCを基準にすると、性能は特別魅力的ではない
- バイトコードインタープリタのループが非常に非効率に見える
- 根本原因を引き続き調査中である
-
より良いメッシュ生成
- 本格的なライブラリ利用者にとって、メッシュ生成は大きな問題である
- Fidget は
libfiveと同じメッシュ生成戦略を使っているが、libfiveのより堅牢な動作を実現しているさまざまな細かなチューニングはない - 現在利用可能な選択肢に満足していないため、この部分にはあまり時間をかけていない
- dual contouring に継ぎ足すよりも、bulletproof なメッシュアルゴリズムを実装したいと考えている
- 要件を満たす文献上の手法や独自の代替案はまだない
- ユーザー需要に応じて一部のチューニングは行えるが、より良い選択肢も引き続き探していく予定である
-
標準 shape と transform ライブラリ
- 複数世代のソフトウェアにわたって、Fab Modules の標準 shape ライブラリを新しいツールへ移植してきた
- この作業は退屈だが、高水準モデリングのためのある程度標準的な基盤を提供する
libfiveでは各 shape を C++ で記述し、ヘッダーファイルから C、Python、Scheme バインディングを自動生成していた- README では、
libfive_stdlib.hが C ヘッダーであると同時に helper script が解析する構造化ドキュメントでもあると説明されている - Fidget のアプローチはまだ議論中である
- 進行中の議論は fidget#145 にある
- Fab shapes ライブラリを Rust に持ち込む方法は実現可能である
- Inigo Quilez の primitives ライブラリ のように GLSL vector を活用した、より洗練されたコードも参考になる
-
高水準言語バインディング
- Fidget は現在 Rhai バインディングのみを提供している
- Rhai は成熟した Rust-first のスクリプト言語の一つであるため選ばれた
- 統合が容易で、WebAssembly にコンパイルできるという利点がある
- 多くのユーザーは Python や Node バインディングを好むかもしれない
- バインディングの方式も未解決の問いである
- C API は各言語の FFI ライブラリを活用できるようにするが、Rust-first 設計では一段階下に降りるような感覚がある
- 標準ライブラリができれば、各バインディングに docstring、デフォルト引数など適切な使い勝手を備えて自動公開されるとよい
1件のコメント
Hacker News のコメント
こんにちは、これは私のプロジェクトです :)
CS のこの分野が特に良いのは、誰にでも合う要素があるからです。データ構造とアルゴリズム、低レベルの性能作業、コンパイラ、レンダリング/コンピュータグラフィックス、デザインツール向けの UI/UX、GPGPU プログラミングなどがすべて含まれます。
スレッドに出ている質問には答えますが、追加の更新はソーシャルメディア(https://mattkeeter.com/links/)やブログの RSS フィード(https://mattkeeter.com/atom.xml)でも追えます。
以前から頭の中にあった考えをよく示しています。この製造計画を設計したプロセス自体が、ユーザー向けの CAD API になったらどうでしょうか。木工、配管、金属加工、機械加工のような「作る」問題を扱うときは、原材料、手持ちの工具、望む結果を得るための作業順序を自然に考えるものです。
ところが、コード CAD ツールであれマウスベースの従来型インターフェースであれ、現在の CAD API はこのようには動作せず、実際にどう作るかよりも完成形状をどう表現するかに意識を向けさせます。結局重要なのは物を作ることで、モデリングはそれを助ける道具にすぎないのに、その道具が前面に出すぎています。
より現実に基づいたモデリングの流れには多くの利点があるように見えますが、CAD の経験がずっと豊富な立場から見て、この概念には発展の可能性があると思いますか、それとも行き止まりでしょうか。
ペンについてのフィードバックも一つ加えると、3 爪チャックで丸棒をつかんでコレットに合う寸法まで旋削し、寸法を合わせた棒からキャップ 1 個とボディ 2 個分のブランクを切り出した後、残りの作業をコレットで固定すれば同心度を保てます。ただし、最終寸法がコレット寸法と異なる場合は、材料の無駄が少し出ます。
機能面で Fidget は libfive や Ao とどう違うのでしょうか?
そのコードは、ユーザーフレンドリーな単一の式を GLSL 内の IA ライブラリ用のネストした関数呼び出しに変換するだけで、最適化はまったくありませんでした。これはずっと先まで進んでいますね。
ちょうど偶然、著者の別の優れた記事も読んでいたところでした: https://www.mattkeeter.com/projects/constraints/
デモ: https://mattkeeter.com/projects/fidget/constraints
ソース: https://github.com/mkeeter/fidget/blob/main/demos/constraint...
ソルバーのドキュメント: https://docs.rs/fidget/latest/fidget/solver/
うわあ、自分で陰関数曲面レンダラーを作っていたときにこれを知っていたら、ものすごく役に立ったはずです。
私のアプローチもある面では似ていて(区間演算)、別の面では違っていました。最適化はあまりされておらず、fragment shader 用の GLSL を直接生成していました。
正直、全部捨ててこれを再実装して置き換えたい気持ちにもなります。喜ぶべきか悲しむべきか分かりませんね。
アイデアを取り入れて使うこともできますし、これを使いながらプロジェクトに貢献することもできます。どちらにしても素晴らしいことです。
新しいオープンソースの CAD カーネルが出たのはすごいことです!記事だけでは、STEP のような一般的な形式へのエクスポートをサポートしているのか分かりませんでした。
可能なら、あるいは可能になれば、さまざまなオープンソース CAD ライブラリの優れた基盤になりそうです。
ほとんどの STEP ファイルは形状をサーフェスの集合として表現します。たとえば trimmed NURBS のようなものです。これらのサーフェスは隙間のない多様体を構成する必要があり、そうすればソリッドの体積として扱えます。
実際にこれを動作させるには、Fidget の関数表現(f-reps)ではなく、境界表現(b-reps)カーネルが必要です。このようなカーネルを書くのは、はるかに難しい問題です。たとえば、2 つの NURBS サーフェスの交差が常に閉じた形の表現を持つとは限りません。
業界の人と話したとき、以前に経験のあるチームでも、まともな b-rep カーネルを書くにはエンジニア 6 人で約 1 年かかるだろうと見積もっていました。
もっと知りたい場合は、偶然にも私が STEP ファイルビューアも書いており、そこには産業用レベルにはほど遠い b-rep カーネルが含まれています: https://www.mattkeeter.com/projects/foxtrot/
「1024²ピクセルに対して総当たり評価を行うと、バイトコードインタプリタは5.8秒、JITバックエンドは182msかかり、31倍高速になる」
「より賢いアルゴリズムを使うと、速度向上はそれほど劇的ではない。総当たり方式は区間演算やテープの単純化を活用していない。Fidgetの最適化されたレンダリング実装は、この画像をバイトコードインタプリタで6ms、JITバックエンドで4.6msで描画するため、改善は約25%にすぎない」
ここでJITバックエンドがアルゴリズム最適化後にはそれほど重要ではない点に焦点を当て、アルゴリズム最適化がバイトコードでは1000倍、JITでは40倍の改善をもたらす点に焦点を当てていないところが気に入っています
数年前、大学で原子核物理シミュレータ、つまり原子炉モデリングのようなものに少し取り組んだことがあります
その幾何モデルは陰関数曲面、特にR-functionsに基づいていました。min(x,y)もその一例で、あらゆる場所で微分可能であるなど、興味深い性質があります
よい入門資料はこれです。おそらく英語で書かれた唯一の資料かもしれません: https://ecommons.cornell.edu/items/35ae0f68-1af5-4f28-8b8b-7...
原子核分野を離れてからかなり経ちますが、モデリングには今でも古いFortranコードが大量に使われているのではないかと思います。Fidgetは新しいシミュレーションパッケージ用カーネルとして興味深い可能性があります
少し別の話ですが、コードベースのCADソフトウェアで最良のものを探しています
CadQueryを試してみましたが、いくつか問題がありました。3Dプリント用途でおすすめできるものはありますか?
https://youtu.be/0wn7vUmWQgg?si=9Rc1tvbiQgQDgQzd&t=2766
Rustで1つ開発中でもありますが、まだ準備ができているとは言いにくいです
https://github.com/gumyr/build123d
OpenSCAD, DSLCAD, CadQuery, Build123d, Cascade Studio, Declaracad, Replicad
興味深いです。こうした陰関数曲面に関する論文やデモを以前にも見たことがあります。おそらく著者の仕事だったのかもしれません
想像力を働かせればどんなモデルを作れるのかは印象的ですが、トイ例より大きなものを見てみたいです
たとえばb-repカーネルで可能なようにサーフェスをextrudeしたり、SVG/フォントを取り込んでソリッドにしたりすることは可能なのでしょうか?
こうした機能をサポートしつつ、並列化もうまくできる高速なオープンソースカーネルをぜひ見てみたいです
Ian Henryのhttps://bauble.studio/を強く思い出します
私もSDFを使って曲面生成用の抽象ツリーを扱う似たようなものをやってみたいと思っていました
目標となるメッシュや点群があり、hill climbing/annealingで望む形状によく合うツリーを探していくというアイデアです
https://arxiv.org/abs/2407.10954
微分可能なleaf(二次曲面)を、微分可能なBoolean風の演算で組み合わせてCSGツリーを作り、形状全体に対してhill climbingできるようにします