TECH PLAY

Sass

イベント

該当するコンテンツが見つかりませんでした

マガジン

該当するコンテンツが見つかりませんでした

技術ブログ

MathJax={tex:{inlineMath:[['$','$']],displayMath:[['$$','$$']],processEscapes:true}}; こんにちは、Insight Edgeでデータサイエンティストをしている新見です。 cuTile Pythonとは 背景 特徴 従来のCUDA(SIMT)との違い 文法 TileGymで行列積ベンチマーク 倍精度行列積エミュレーション Ozaki Schemeについて 分解(Split) 行列積の計算 素朴な実装と初回結果 最適化 Fast Mode(GEMMの削減) Fused Split Kernel(分割の融合) 最適化後の結果 dによる精度/速度トレードオフ まとめ 参考文献 今回はNVIDIAが発表したばかりの「cuTile Python」を試してみました。普段は、GPUカーネルを業務で書くことはありませんが、cuTileはPythonで書かれていて、文法もシンプルなようなので、GPUプログラミングの勉強の意味も含めて記事にしました。 cuTile Pythonとは cuTile Pythonは、NVIDIA GPU向けの新しい並列プログラミングモデル「cuTile」をPythonから使うためのDSL(ドメイン固有言語)です。 背景 GPU上で高速に動作する処理を自前で記述したい場面は増えていますが、CUDA C++の習得コストは依然として高いのが実情です。 PyTorchやcuBLASといった高レベルAPIで日常的な開発は十分カバーできるものの、LLM推論の最適化など低レイヤへの介入が求められる局面も増えてきました。NVIDIA Ampere世代以降のGPUではTensor CoreやTMA(Tensor Memory Accelerator)といったハードウェア機能が追加されており、これらを十分に活用するにはより踏み込んだプログラミングが必要になります。 しかし、ハードウェアを意識したコードを書く難易度は上がり続けています。メモリ階層ひとつ取っても、共有メモリ、レジスタ、Blackwell世代で追加されたTensor Memoryなど、用途に応じて使い分ける必要があり、それぞれの特性に合わせたデータ配置や転送の制御が求められます。 さらに、特定のアーキテクチャに最適化したコードは新しい世代のGPUが登場した途端に書き直しが必要になることも多く、保守コストも無視できません。 こうした背景から、ハードウェアの詳細を抽象化しつつ高い性能を引き出すDSLへの需要が高まっています。OpenAIの gpt-oss リポジトリでもTritonという同様のDSLが採用されており、この手のアプローチは業界でも広く注目されています。 特徴 GPUプログラミングには、cuBLASやPyTorchのような高レベルライブラリか、CUDA C++やPTXといった低レベルなスレッド制御か、という二極化がありました。cuTileはこの中間に位置する「Tileレベル」のプログラミングモデルです。 抽象レベルについて(動画[1]より) 以下、動画[1]で紹介されていた特徴になります。 CUDAプラットフォームにネイティブ統合 : OpenAI Tritonなどサードパーティ製DSLとは異なり、cuTile(Tile IR)はCUDAドライバに組み込まれています。既存のプロファイラやデバッガがそのまま使えます。 Tile IRへのコンパイル : Pythonで書いたカーネルは「Tile IR」という仮想ISAに変換され、ドライバが実行時にターゲットGPUに合わせた最適なマシンコード(SASS)を生成します。 技術スタックの階層構造、TileIRはPTXを置き換えるのではなく共存する(動画[1]より) 従来のCUDA(SIMT)との違い 従来のCUDA(SIMT: Single Instruction, Multiple Threads)とcuTileでは、プログラマが何を書いて何をシステムに任せるかが大きく異なります。 特徴 従来のCUDA (SIMT) cuTile (Tile-based) 実行単位 スレッド単位でデータ処理を記述。WarpやBlockの構成を意識する必要がある データの塊(タイル)と単一の実行単位(ブロック)で思考。スレッドへの分解はシステムが行う データ処理 個々のスレッドへのデータ分配(ストライディングなど)を手動で計算・管理 タイル(配列全体の一部)を一つの単位としてロード・演算・ストア メモリ管理 共有メモリの確保、同期(バリア)、バンクコンフリクト回避などをユーザーが管理 システムが管理。共有メモリの利用や同期は自動化され、ユーザーからは隠蔽 ハードウェア活用 Tensor Coreなどを使うには複雑なPTX命令や特定のレイアウトを意識する必要がある ct.load や演算子を書くだけでTMAやTensor Coreを自動的に活用 文法 cuTile Pythonは、Pythonのデコレータと専用の型システムを使って記述します。詳しくは公式ドキュメント[2]を参照してください。 主な特徴: @ct.kernel デコレータ : Python関数をGPUカーネルとしてマーク。関数内ではcuTile Pythonの文法に従う。 イミュータブルなタイル : カーネル内では、タイルが操作対象となる。タイルは「値」として扱われ、変更不可。演算すると新しいタイルが生成されます Array (Global Memory): 引数から取得、ミュータブル。 ct.load / ct.store でアクセス Tile (Local/Register): イミュータブルで演算対象 以下、ベクトル加算のコード例です。 import cuda.tile as ct # タイルサイズはコンパイル時定数 TILE_SIZE = 16 @ ct.kernel def vector_add_kernel (a, b, result): # 1. 現在のブロックIDを取得 (スレッドIDではない!) block_id = ct.bid( 0 ) # 2. グローバルメモリ(Array)からタイルとしてデータをロード # システムが自動的に最適なメモリ転送(TMA等)を行う a_tile = ct.load(a, index=(block_id,), shape=(TILE_SIZE,)) b_tile = ct.load(b, index=(block_id,), shape=(TILE_SIZE,)) # 3. タイル同士の演算 (要素ごとの加算が一括で行われる) result_tile = a_tile + b_tile # 4. 結果をグローバルメモリにストア ct.store(result, index=(block_id,), tile=result_tile) # ホスト側からの実行 # ct.launch(stream, grid_dim, kernel_func, args) ブロックごとに同一のカーネルが実行され、各ブロックはIDで指定されたデータを担当範囲として、処理を行います。タイル演算は、感覚としてはnumpyの処理に似ています。 TileGymで行列積ベンチマーク 実際に動かします。cuTileはCUDA Toolkit13.1以降が必要で、これはBlackwell世代以降の比較的最新のGPUでしか動かないようです。私は手元に最新のGPUがないので、クラウドサービスを利用したいと思います。今回は、 Modal と呼ばれるGPU特化のクラウドサービスを利用しました。 Modalは関数ベースでGPUインスタンスを立ち上げられるサービスになります。使い勝手がよく、便利です。実行時間に応じた従量課金制で、今回の検証のような少しGPUを試してみたい場合に適しています。 今回は、公式のサンプルレポジトリTileGym[3]をベースに、行列積のコードの実行をしてみます。Modalで走らせる実行コードを以下に示します。imageでDockerイメージを作成し、TileGymのレポジトリをクローン、ライブラリインストールを行います。Modalの詳細は ドキュメント を参照してください。今回対象のGPUはB200です。 # run-tilegym.py import modal image = ( modal.Image.from_registry( "nvidia/cuda:13.1.0-devel-ubuntu24.04" , add_python= "3.13" ) # CUDA 13.1開発環境イメージ .apt_install( "git" ) .run_commands( "pip install --pre torch --index-url https://download.pytorch.org/whl/cu130" ) # PyTorchインストール、比較のため .run_commands( "git clone https://github.com/NVIDIA/TileGym.git && cd TileGym && pip install -e ." ) # cuTile, TileGymインストール .entrypoint([]) ) app = modal.App( "tilegym-test" ) @ app.function (gpu= "B200" , image=image, timeout= 600 ) def run_mma_bench (): import os os.chdir( "/TileGym" ) os.system( "python tests/benchmark/bench_matrix_multiplication.py" ) @ app.local_entrypoint () def main (): run_mma_bench.remote() 上のコードをrun-tilegym.pyとして保存し、 modal run run-tilegym.py で実行します。問題なければ結果は、以下のように出力されるはずです。 matmul-performance-float16-TFLOPS: M N K CuTile PyTorch 0 1024.0 1024.0 1024.0 271.056760 473.522850 1 2048.0 2048.0 2048.0 1129.688506 1199.365877 2 4096.0 4096.0 4096.0 1235.696555 1401.341171 3 8192.0 8192.0 8192.0 1483.030888 1253.946946 4 16384.0 16384.0 16384.0 1356.600018 1536.098446 5 32768.0 32768.0 32768.0 1254.836929 1306.057063 matmul-performance-float8_e5m2-TFLOPS: M N K CuTile 0 1024.0 1024.0 1024.0 277.309352 1 2048.0 2048.0 2048.0 1154.454102 2 4096.0 4096.0 4096.0 2769.415226 3 8192.0 8192.0 8192.0 2981.168986 4 16384.0 16384.0 16384.0 2935.864636 5 32768.0 32768.0 32768.0 2658.604232 CuTileとPyTorchの行列積のベンチマークが出ています。float16とfloat8_e5m2の両方で行列積を実行していますが、PyTorchでは、後者の行列積が未対応のようです。PyTorchは裏側でcuBLASを呼び出しているので実質cuBLASとの比較です。float16では、CuTileはPyTorchに近い性能、一部のサイズでは、PyTorchを上回る性能が出ています。float8_e5m2では、行列サイズが4096以上でfloat16の約2倍の性能が出ています。 以下が TileGym/src/tilegym/ops/cutile/matmul.py の行列積のカーネルコードの抜粋です。 @ ct.kernel (num_ctas=ct.ByTarget(sm_100= 2 )) def matmul_kernel (A, B, C, TILE_SIZE_M: ConstInt, TILE_SIZE_N: ConstInt, TILE_SIZE_K: ConstInt): # 担当タイルのインデックス計算(L2キャッシュ局所性のためswizzle) bidx, bidy = swizzle_2d(A.shape[ 0 ], B.shape[ 1 ], TILE_SIZE_M, TILE_SIZE_N, GROUP_SIZE_M= 8 ) num_tiles_k = ct.num_tiles(A, axis= 1 , shape=(TILE_SIZE_M, TILE_SIZE_K)) # FP32アキュムレータの初期化(FP16入力でも精度維持のためFP32で累積) accumulator = ct.full((TILE_SIZE_M, TILE_SIZE_N), 0 , dtype=ct.float32) # FP32→TF32変換(Tensor Coreを利用するため) dtype = ct.tfloat32 if A.dtype == ct.float32 else A.dtype # K方向にタイル単位でループ for k in range (num_tiles_k): a = ct.load(A, index=(bidx, k), shape=(TILE_SIZE_M, TILE_SIZE_K), padding_mode=ct.PaddingMode.ZERO).astype(dtype) b = ct.load(B, index=(k, bidy), shape=(TILE_SIZE_K, TILE_SIZE_N), padding_mode=ct.PaddingMode.ZERO).astype(dtype) accumulator = ct.mma(a, b, accumulator) # 行列積計算・累積 # 出力型に変換して結果を書き出し ct.store(C, index=(bidx, bidy), tile=ct.astype(accumulator, C.dtype)) A:MxK @ B:KxN -> C:MxN の行列積で、M方向、N方向単位でバッチに切り分けCの部分タイルごとに並行して実行されます。K方向にも部分分割して、順次読み込み(load), 行列積計算(mma), 結果の保存(store)を行っています。cuTile側でメモリの種類やMMA命令の選択は書く必要がなく、コンパイル時に自動的に最適化されます。 このように簡潔に書いても、ゴリゴリにチューニングしているcuBLASに匹敵した性能を出しているというのがcuTileの売りなようです。 ベンチマークを動かしただけでは面白くないので、型の精度を少し上げて同様の計算をしてみます。F32演算の場合、上記コードでは行列をTF32に変換してから計算しています。それと合わせるため、PyTorch側も以下のようにTF32を有効化します。 # TileGym/tests/benchmark/bench_matrix_multiplication.py # Enable TF32 for PyTorch to match Tensor Core behavior torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True また、FP64演算にあたり、累積の型がFP32では精度が足りないため、cutileコード側で累積の型をFP64に変更する処理を追加しています。 # Initialize an accumulator for the current output tile (TILE_SIZE_M x TILE_SIZE_N). # Use float64 for float64 inputs, otherwise float32 for higher precision accumulation. acc_dtype = ct.float64 if A.dtype == ct.float64 else ct.float32 accumulator = ct.full((TILE_SIZE_M, TILE_SIZE_N), 0 , dtype=acc_dtype) 以下が修正後のベンチマーク結果です。 matmul-performance-float32-TFLOPS: M N K CuTile PyTorch 0 1024.0 1024.0 1024.0 208.295471 294.114105 1 2048.0 2048.0 2048.0 665.976324 648.103430 2 4096.0 4096.0 4096.0 698.961883 747.326296 3 8192.0 8192.0 8192.0 783.858756 761.237840 4 16384.0 16384.0 16384.0 856.688401 742.126004 matmul-performance-float64-TFLOPS: M N K CuTile PyTorch 0 1024.0 1024.0 1024.0 0.855789 26.687611 1 2048.0 2048.0 2048.0 1.063844 33.830530 2 4096.0 4096.0 4096.0 1.124713 35.400544 3 8192.0 8192.0 8192.0 1.124824 35.438650 FP32では、PyTorchに近い性能が出ています。一方、FP64では、cuTile側での最適化がまだ不十分なようで、PyTorchに大きく劣る結果となっています。TILE_SIZEをより小さく設定することで、1.6 TFLOPS程度には改善しましたが、まだ大きく劣っています。 原因としては、cuTileの ct.mma がFP64演算に対して効率的な命令へマッピングできていない可能性が高いです。cuBLAS(PyTorch)はFP64 Tensor Coreを含むハードウェアリソースを最大限に活用した成熟した実装を持っており、この差が性能差に直結しています。 ここで、FP64演算の性能を向上させるために、Ozaki Schemeと呼ばれる倍精度行列積エミュレーション手法を試してみます。 倍精度行列積エミュレーション Ozaki Schemeについて Ozaki Schemeは、FP64の行列積をFP64演算なしで高精度にエミュレートする手法です[4][5]。詳しくは元の論文を読んでほしいのですが、概要を説明します。基本的なアイデアは、FP64行列を複数の低精度行列に分解し、Tensor Coreで高速に行列積を計算するというものです。行列の分解、行列の計算、結果の累積の3段階で構成されます。 分解(Split) 論文に従い、以下の型を定義します。 Type1 (FP64): 元の行列の精度。仮数部 $m_{\text{Type1}} = 53$ ビット Type2 : 分解先の低精度型(BF16, FP16, FP8等)。仮数部 $m_{\text{Type2}}$ ビット(隠れビット含む) Type3 (FP32): Tensor Coreの累積精度。仮数部 $m_{\text{Type3}} = 24$ ビット Type1の行列 $\boldsymbol{x}$ を、残差 $\boldsymbol{x}^{(p)}$ がゼロになるまで再帰的にType2スライス $\bar{\boldsymbol{x}}^{(p)}$ に分解します。$\boldsymbol{x}^{(1)} = \boldsymbol{x}$ として、各ステップ $p$ で以下を行います。 $$c_x^{(p)} = \left\lceil \log_2 \left( \max_i \left| x_i^{(p)} \right| \right) \right\rceil \tag{1}$$ $$\sigma = 0.75 \cdot 2^{\rho + c_x^{(p)}} \tag{2}$$ $$v_i = \text{fl}_{\text{Type1}} \left( \left( x_i^{(p)} + \sigma \right) - \sigma \right) \tag{3}$$ $$x_i^{(p+1)} = \text{fl}_{\text{Type1}} \left( x_i^{(p)} - v_i \right) \tag{4}$$ $$\bar{x}_i^{(p)} = \text{cvt}_{\text{Type2}} \left( \text{fl}_{\text{Type1}} \left( 2^{-c_x^{(p)}} v_i \right) \right) \tag{5}$$ ここで $\rho$ は精度パラメータ(Type1, Type2, Type3の仮数部ビット数と内積次元 $k$ から決定)です。$\sigma$ を足して引く操作(式3)がVeltkamp分割の核心で、上位 $m_{\text{Type2}}$ ビットを正確に抽出します。式4で残差を更新し、式5で $2^{c_x^{(p)}}$ で正規化してType2スライスを得ます。 この結果、$\boldsymbol{x}$ は $s_x$ 個のスライスに分解されます。 $$\boldsymbol{x} = \sum_{p=1}^{s_x} 2^{c_x^{(p)}} \cdot \bar{\boldsymbol{x}}^{(p)} \tag{9}$$ $c_x^{(p)}$ が指数部、$\bar{\boldsymbol{x}}^{(p)}$ が仮数部に対応します。スライス数 $s_x$ は $\boldsymbol{x}^{(p)} = 0$ になるまでの反復回数で決まり、理論的には $\lceil m_{\text{Type1}} / m_{\text{Type2}} \rceil$ ステップですが、行列要素のスケールのばらつきにより多くなることがあります。 PyTorchでの実装は以下の通りです。 def ozaki_split_to_type2_slices (x, k, type2, max_slices= 20 ): # 仮数部ビット数(隠れビット含む) m_fp64, m_fp32 = 53 , 24 m_type2 = - int (math.log2(torch.finfo(type2).eps)) + 1 # 精度パラメータ ρ の計算 gamma = math.ceil(m_fp64 - (m_fp32 - math.log2(k)) / 2 ) xi = m_fp64 - m_type2 rho = max (gamma, xi) slices = [] residual = x.clone().to(torch.float64) for _ in range (max_slices): max_abs = residual.abs().max().item() if max_abs == 0 or max_abs < 1e-300 : break c_x = math.ceil(math.log2(max_abs)) # 式(1) sigma = 0.75 * math.ldexp( 1.0 , rho + c_x) # 式(2) v = (residual + sigma) - sigma # 式(3) Veltkamp分割 residual = residual - v # 式(4) 残差更新 scale = math.ldexp( 1.0 , c_x) slice_type2 = (v / scale).to(type2) # 式(5) 正規化 + Type2変換 slices.append((slice_type2, scale)) return slices # [(Type2スライス, 2^c_x), ...] 行列積の計算 行列 $\boldsymbol{x}$, $\boldsymbol{y}$ をそれぞれ分解すると、行列積は以下のように展開できます。 $$\boldsymbol{x}^T \boldsymbol{y} = \sum_{p=1}^{s_x} \sum_{q=1}^{s_y} 2^{c_x^{(p)} + c_y^{(q)}} \cdot \bar{\boldsymbol{x}}^{(p)T} \bar{\boldsymbol{y}}^{(q)} \tag{10}$$ 各 $\bar{\boldsymbol{x}}^{(p)T} \bar{\boldsymbol{y}}^{(q)}$ はType2行列同士の積であり、Tensor CoreのGEMMで計算できます。Ozaki Schemeではρパラメータにより、このGEMMのType3(FP32)での累積が丸め誤差なしで成立するよう設計されています。 $$\bar{\boldsymbol{x}}^{(p)T} \bar{\boldsymbol{y}}^{(q)} = \text{fl}_{\text{Type3}} \left( \bar{\boldsymbol{x}}^{(p)T} \bar{\boldsymbol{y}}^{(q)} \right) \tag{11}$$ 式10の分解自体は数学的な恒等式として厳密に成立します。実装上は、外側の累積(スケール乗算と加算)をType1算術で行うことでType1精度を達成できます。 cuTileでの行列積カーネルの実装は以下の通りです。tilegymのmatmulカーネルとベースは同じで2つのスライス分のouter-loopが追加されています。 @ ct.kernel (num_ctas=ct.ByTarget(sm_100= 2 )) def ozaki_matmul_fused_kernel ( A_slices, # (s_a, M, K) Type2スライス B_slices, # (s_b, K, N) Type2スライス Combined_scales, # (s_a, s_b) 2^{c_x(p)+c_y(q)} のスケール行列 C, # (M, N) FP64 出力 TILE_SIZE_M: ConstInt, TILE_SIZE_N: ConstInt, TILE_SIZE_K: ConstInt, ): # タイルインデックス計算(L2キャッシュ局所性のためswizzle) bidx, bidy = swizzle_2d(M, N, TILE_SIZE_M, TILE_SIZE_N, GROUP_SIZE_M= 8 ) num_tiles_k = ct.cdiv(K, TILE_SIZE_K) # FP64最終アキュムレータ(式10の外側の累積) accumulator = ct.full((TILE_SIZE_M, TILE_SIZE_N), 0.0 , dtype=ct.float64) # 全スライスペア (p, q) をループ for p in range (num_slices_a): for q in range (num_slices_b): # FP32中間アキュムレータ(式11: Type3での丸め誤差なし計算) slice_acc = ct.full((TILE_SIZE_M, TILE_SIZE_N), 0.0 , dtype=ct.float32) # K方向のタイルループ for k in range (num_tiles_k): a_tile = ct.load(A_slices, index=(p, bidx, k), ...) b_tile = ct.load(B_slices, index=(q, k, bidy), ...) slice_acc = ct.mma(a_tile, b_tile, slice_acc) # Type2 Tensor Core MMA # スケーリングしてFP64で累積(式10) scale = ct.load(Combined_scales, index=(p, q), shape=( 1 , 1 )) accumulator = accumulator + ct.astype(slice_acc, ct.float64) * scale ct.store(C, index=(bidx, bidy), tile=accumulator) 素朴な実装と初回結果 上記のようにOzaki Schemeを実装してみます。スライス分割はホスト側のpythonで行い、各ペアのGEMMを順次実行する方式です。以下、2種類のType2で行列積を計算した結果です。 スライス数はA・Bそれぞれの分割数($s_a \times s_b$)、GEMMsはその組み合わせで実行したGEMM回数です。TFLOPSはFP64換算のスループット、Rel ErrorはPyTorch FP64結果を基準とした相対誤差です。 TYPE2 = FP16 行列サイズ スライス数 GEMMs Split(ms) Kernel(ms) 合計(ms) TFLOPS Rel Error 1024 10×10 100 1.48 0.62 2.64 0.81 1.58e-15 2048 12×12 144 2.57 2.66 5.75 2.99 1.98e-15 4096 12×12 144 8.69 21.59 30.70 4.48 6.31e-15 8192 14×14 196 36.00 192.32 231.76 4.74 7.28e-15 16384 14×14 196 135.21 1737.14 1884.24 4.67 3.35e-15 TYPE2 = FP8 (E4M3) 行列サイズ スライス数 GEMMs Split(ms) Kernel(ms) 合計(ms) TFLOPS Rel Error 1024 15×16 240 2.22 1.11 4.03 0.53 1.64e-15 2048 16×16 256 3.48 3.11 7.33 2.34 2.27e-15 4096 16×17 272 12.30 19.17 30.86 4.45 5.69e-15 8192 17×17 289 45.64 179.99 226.64 4.85 8.88e-15 FP16はスライス数が少ない分GEMMも少なくなりますが、FP8はTensor Coreのスループットが高いため、GEMMs数が多いにも関わらず類似の性能が出ています。いずれの型でもcuTile FP64直接計算(約1 TFLOPS)を上回っていますが、PyTorchの性能には大きく劣後しています。Split処理の時間も無視できず、特に小さな行列サイズでボトルネックになっています。また、参照した論文に記載されている必要なGEMM数(スライス数)よりも多くなっている点は気になりましたが、原因はわからずでした。 最適化 初回結果を踏まえ、いくつか改善を試みました。その中で効果があった方法が以下になります。 Fast Mode(GEMMの削減) スライス数が $s$ の場合、全組み合わせで $s^{2}$ 回のGEMMが必要です。しかし、スライスインデックスが大きい組み合わせ($i + j \geq d$)は寄与が小さいため、スキップできます。 [5]で提案されたFast Mode(Algorithm 3)では、確率的誤差限界 $|fl(AB) - AB| \leq 2\sqrt{k} \cdot u_{\text{FP64}} \cdot |A||B|$ を満たす最小の閾値 $d$ を自動決定します。 BF16の場合、典型的には $d = 9$ 程度で、GEMMは49回から39回に削減できます。さらに max_d パラメータで手動上限を設定すれば、精度とのトレードオフで計算量を調整できます。 実装としては、前述の行列積カーネルのスライスペアループに i + j >= D の条件を追加するだけです。 for i in range (num_slices_a): for j in range (num_slices_b): if i + j >= D: # Fast Mode: 寄与の小さい組み合わせをスキップ continue # ... K方向ループでMMA計算 ... Fused Split Kernel(分割の融合) 元の実装では、各スライスの計算ごとに max().item() でGPU→CPU同期が発生していました(BF16で7スライス = 7回の同期)。 改善後は、初回の max_abs 計算で1回だけ同期し、全スライスの $\sigma$ と $2^{-c_i}$(逆スケール)をCPU側で事前計算します。その後、単一カーネルで全スライスを一括計算します。 @ ct.kernel (occupancy= 4 ) def _veltkamp_split_all_slices_kernel ( x_in, # (M, N) FP64 input slices_out, # (num_slices, M, N) TYPE2 output slices sigmas, # (num_slices,) FP64 pre-computed sigma values inv_scales, # (num_slices,) FP64 pre-computed 1/scale values num_slices: ConstInt, TILE_SIZE_M: ConstInt, TILE_SIZE_N: ConstInt, ): bid = ct.bid( 0 ) # ... (タイルインデックス計算) ... # 入力タイルをロード residual = ct.load(x_in, index=(tile_m, tile_n), shape=(TILE_SIZE_M, TILE_SIZE_N), padding_mode=ct.PaddingMode.ZERO) # 全スライスをループで計算 for i in range (num_slices): sigma_tile = ct.load(sigmas, index=(i,), shape=( 1 ,)) inv_scale_tile = ct.load(inv_scales, index=(i,), shape=( 1 ,)) # Veltkamp分割 v = (residual + sigma_tile) - sigma_tile slice_tile = ct.astype(v * inv_scale_tile, slices_out.dtype) ct.store(slices_out, index=(i, tile_m, tile_n), tile=ct.reshape(slice_tile, ( 1 , TILE_SIZE_M, TILE_SIZE_N))) residual = residual - v 最適化後の結果 Fast Mode + Fused Split Kernelを適用した結果です。 TYPE2 = BF16 行列サイズ スライス数 GEMMs Split(ms) Kernel(ms) 合計(ms) TFLOPS Rel Error 1024 7×7 39 0.20 0.25 1.57 1.37 5.75e-15 2048 7×7 39 0.24 0.75 2.16 7.94 1.30e-14 4096 7×7 39 0.43 5.32 7.22 19.04 1.16e-14 8192 7×7 39 1.16 38.93 42.60 25.81 2.39e-14 16384 7×7 39 3.96 323.10 327.78 26.84 2.21e-14 TYPE2 = FP8 (E4M3) 行列サイズ スライス数 GEMMs Split(ms) Kernel(ms) 合計(ms) TFLOPS Rel Error 1024 14×14 130 0.25 0.61 2.99 0.72 3.48e-13 2048 14×14 130 1.27 1.60 6.80 2.52 3.57e-13 4096 14×14 130 2.13 9.31 15.88 8.65 3.41e-13 FP8はスライス数が多く(14×14)GEMMsも130回と多いものの、Fast ModeによるGEMM削減とFused Splitの効果で素朴な実装(4096で4.45 TFLOPS)から改善が見られます。ただしBF16と比較すると、仮数部が4ビットと少ないためスライス数が増え、GEMMs数の差(130 vs 39)がFP8のスループット優位を打ち消しており、BF16の方が総合的に有利になりました。 素朴な実装と比較すると、最適化の効果は顕著です。 Fused Split Kernel : Split時間が大幅短縮(素朴なFP16版 8192: 36.00ms → BF16最適化版: 1.16ms、約31倍) Fast Mode : GEMMsを49→39に削減(BF16の全組み合わせ比) BF16がTYPE2として最適である理由は、Tensor CoreのFP32アキュムレータとの相性にあります。BF16の仮数部は8ビットなので、2つのBF16値の積は16ビットに収まります。FP32の仮数部は24ビットあるため、TILE_SIZE_K=128個の積和(16 + log2(128) = 23 ≤ 24)が 丸め誤差なし で正確に計算できます。一方FP16(11ビット仮数部)では積が22ビットとなり、128個の累積(22 + 7 = 29 > 24)でFP32精度を超えるため、丸め誤差が発生します。 この性質により、BF16では1e-14というFP64に近い精度を維持しつつ、Tensor Coreの高いスループットを活用できています。 上記以外にも、タイルサイズの調整や、カーネル内のsplitループ方向のCTA分散も試みましたが、効果はありませんでした。本来は、プロファイラ(NVIDIA Nsight Compute)を使って、メモリ利用等解析するのが効果的ですが、Modal上ではNsightは使えないようなので断念しました。 dによる精度/速度トレードオフ d パラメータを変えて、16384×16384行列での性能と精度の変化を測定しました。BF16の結果です。 d GEMMs 合計(ms) TFLOPS Rel Error vs PyTorch FP64 9 (default) 39 327.78 26.84 2.21e-14 0.75x 8 34 298.76 29.44 2.31e-14 0.83x 7 28 238.94 36.81 3.50e-13 1.03x 6 21 189.59 46.40 4.39e-11 1.30x 5 15 136.34 64.52 4.51e-09 1.81x PyTorch FP64(cuBLAS)は同サイズで35.62 TFLOPSです。Rel ErrorはPyTorch FP64の結果を基準として計算しています。 d=7 でcuBLASと同等の速度を精度1e-13で達成し、 d=5 では1.8倍の高速化を1e-9精度で実現しています。なお、FP64 GEMM自体も浮動小数点演算の性質上、行列サイズに応じた丸め誤差は避けられないため、 d=8 (2.31e-14)程度の偏差であれば実用上十分でしょう。 まとめ cuTile Pythonの簡単な紹介とOzaki Schemeの実装を通じて、FP64行列積の高速化を試みました。BF16 Ozaki Schemeの最適化後、16384×16384行列で最大26.84 TFLOPS(d=9)を達成しました。dを調整することで精度と速度のトレードオフが可能で、d=7ではcuBLAS FP64(35.62 TFLOPS)と同等の36.81 TFLOPSを精度1e-13で達成し、d=5では64.52 TFLOPS(cuBLASの1.8倍)を1e-9精度で実現しています。 CUDAカーネルをPythonライクに書ける点で、GPUプログラミングの敷居が低くなったと感じます。 一方で、より高度な最適化やチューニングが必要な場合は、cuTile Pythonは抽象化してハード側の詳細を隠蔽している分、制約があるように感じました。今回は、行列積の例でしたが、tilegymにはtransformerの実装例があるので、次回はそちらも試してみたいと思います。 参考文献 [1] Lecture 89: cuTile (from friends at NVIDIA) [2] NVIDIA cuTile Documentation . cuTile Python. [3] NVIDIA TileGym . GPU Tile kernel development examples using cuTile. [4] Markus Höhnerbach, Paolo Bientinesi (2025). "DGEMM without FP64 Arithmetic" . arXiv:2508.00441. [5] Daichi Mukunoki, Katsuhisa Ozaki, Takeshi Ogita, and Toshiyuki Imamura (2020). "DGEMM using Tensor Cores, and Its Accurate and Reproducible Versions". ISC High Performance 2020, Lecture Notes in Computer Science, Vol. 12151. Springer, 230–248. doi:10.1007/978-3-030-50743-5_12
はじめに こんにちは。ZOZOTOWN開発本部フロントエンドの菊地( @hiro0218 )です。 2021年、ZOZOTOWNはフロントエンドリプレイスを開始しました。現在、ホームページや商品一覧ページなど主要なページのNext.js化が完了し、運用フェーズに入っています。詳細は以下の記事を参照してください。 techblog.zozo.com 開始当初、他社事例を参考にしながら、よくある課題を未然に防ぐディレクトリ構成を設計しました。本記事では、約4年にわたる運用で改善を重ねてきたディレクトリの分割戦略について紹介します。 ※本記事は2025年8月にちょっと株式会社との合同勉強会で発表した内容を基にしています。 speakerdeck.com 背景 現在、私が携わっている領域におけるフロントエンド開発は4チーム、合計30名強で運用しています。この規模で効率的に開発を進めるため、ディレクトリ構成は重要な要素となります。 避けたい課題 過去の経験や他社事例から、以下のような課題を未然に防ぐ必要がありました。 配置場所が曖昧:「なんとなくここに置いた」コンポーネントが増え、後から見つけにくい 再利用性が低い:ページ固有の処理を含むコンポーネントは、他のページで使いにくい 影響範囲が不明確:コンポーネントを変更する際、どこで使われているか把握しづらい チーム開発が非効率:メンバーごとに配置ルールの解釈が異なり、コードレビューで迷う 新規開発にあたり、これらを考慮した設計戦略を採用しました。 解決アプローチ:責務分離パターン 役割を明確にした配置(責務分離)を行いました。この設計を選んだ理由は「迷わない分類」です。新しいコンポーネントを作成する際、配置場所で迷う時間を最小化し、メンバー間で判断が分かれないようにすることを重視しました。 この設計では、以下の2点を重視しています。 運用中の移動最小化 : 一度配置したコンポーネントは、基本的に移動が不要 予測可能な構造 : ディレクトリ階層が深くならず、探索しやすい この設計を実現するため、以下の2層構造を採用しました。 コンポーネント層 ( src/components/ ) 役割別に5つのディレクトリに分類 UI インフラ層 ( src/ui/ ) コンポーネントが利用する共通基盤 コンポーネント層の実装を進める中で、UIインフラ層の必要性が見えてきました。 コンポーネント層と UI インフラ層の関係 コンポーネント層の設計 コンポーネントを5つの役割に分類し、それぞれの責務と依存関係を定義しています。 ディレクトリ構成 他社事例を参考にして5つのディレクトリに分割した構成を採用しました。 src/components/ ├── UI/ ├── Models/ ├── Pages/ ├── Layouts/ └── Functional/ Next.jsのルーティング用ディレクトリ( src/pages )と区別するため、頭を大文字としています。 責務ごとの分類 各ディレクトリの役割は以下のように定義されています。 名前 役割 格納するコンポーネント例 UI 純粋なUI要素 Button, Collapse, Image... Models ドメインロジックがある ProductList, BrandList... Pages ページ専用 HomePage, SearchPage... Layouts アプリに関わるレイアウト Header, Footer... Functional UIを伴わないアプリケーション機能 Analytics, GlobalStore... この分類のポイントは「新しくコンポーネントを作るとき、どこに配置するか迷わない」ことです。曖昧な判断基準では、メンバーごとに解釈が分かれ、レビュー時の議論コストが増大します。 コンポーネント作成時の判断フロー コンポーネントを作成する際は、以下の順で判断します。 特定のページでのみ使用するか? YES → src/components/Pages/ に配置 判断基準:他のページでは再利用されない固有の実装 例:HomePage、SearchPage アプリ全体のレイアウト構造に関わるか? YES → src/components/Layouts/ に配置 判断基準:アプリ全体の構造・骨格を担当 例:Header、Footer 特定のドメインロジックを含み複数ページで使用するか? YES → src/components/Models/ に配置 判断基準:特定のドメインに関連する機能を持つコンポーネント 例:ProductList、BrandList UIのみの汎用的なコンポーネントか? YES → src/components/UI/ に配置 判断基準:ビジネスロジックを含まない純粋なUI要素 例:Button、Collapse、Image 重要:ドメイン固有の名前を持つコンポーネントも、データを表示するだけならUIに配置。Modelsとの違いは データ取得やビジネスルールを含むかどうか UIを伴わないアプリケーション機能か? YES → src/components/Functional/ に配置 判断基準:直接ユーザーに表示されないアプリケーション機能 例:Analytics、GlobalStore 実装例 UIは純粋な表示、Modelsはドメインロジックという責務の分離を、実際のコードで確認します。 UI コンポーネント propsで受け取ったデータを表示するだけのシンプルな実装です。 // UI/ProductCard - UI コンポーネント export const ProductCard = ( { name , price , image } ) => ( < Card > < Image src = { image } /> < Title > { name } </ Title > < Price > { price } </ Price > </ Card > ); Models コンポーネント データを取得し、UIコンポーネントを組み合わせて表示します。 // Models/ProductList - ドメインロジック + UI を利用 export const ProductList = ( props ) => { const { products } = useProductData(props); return ( < Grid > { products?. map (( product ) => ( < ProductCard key = { product. id } { ...product } /> )) } </ Grid > ); } ; ProductCard (UI)は純粋な表示、 ProductList (Models)はデータ取得とUIの組み合わせという責務の違いが分かります。 テストファイルの配置 コンポーネントの分類と同様に、テストファイルの配置もルールが必要です。テストやStorybookのファイルは、対象のコンポーネントファイルと同じディレクトリに配置することで、関連するファイルを一箇所にまとめて管理しています。 components/UI/Button/ ├── Button.tsx ├── Button.test.tsx ├── Button.stories.tsx └── Button.module.css この配置により、実装とテストの対応関係が分かりやすくなり、ファイル間の移動もスムーズになります。 依存関係のルール コンポーネント間の依存関係は「自分の横か下にある分類のコンポーネントのみ参照してよい」という原則に従います。 依存の基本原則:上位から下位への一方向のみ許可。 Pages ( src/components/Pages ): Models、UI、Functionalを参照可能 Models・Layouts : UIとFunctionalを参照可能 UI : Functionalのみ参照可能 Functional : 外部依存なし(最下位) 各ディレクトリは、同じディレクトリ内のコンポーネント同士も参照可能です(Pagesを除く)。 注記 : ここでの「Pages」は src/components/Pages (ページ専用コンポーネント)を指します。Next.jsのルーティング用ディレクトリである src/pages は、アプリケーションのエントリーポイントとして特別な役割を持つため、Layoutsを含むすべてのコンポーネントを参照可能です。 この依存関係により、循環参照を防ぎ、変更の影響範囲を予測しやすくなります。 依存関係のチェック 設計したディレクトリ構成のルールが守られるよう、以下のツールを導入しています。 ディレクトリ間の依存ルール eslint-plugin-strict-dependencies により、誤った依存関係を自動的に検出し、コードレビュー時の負担を軽減しています。 'strict-dependencies/strict-dependencies' : [ 'error' , [ // Pages コンポーネントの依存ルール { module : 'src/components/Pages' , allowReferenceFrom : [ 'src/pages' ] , // Next.jsのルーティング用ディレクトリからのみ参照可能 allowSameModule : false , } , // Models コンポーネントの依存ルール { module : 'src/components/Models' , allowReferenceFrom : [ 'src/components/Pages' ] , allowSameModule : true , } , // Layouts コンポーネントの依存ルール { module : 'src/components/Layouts' , allowReferenceFrom : [ 'src/pages' ] , // Next.jsのルーティング用ディレクトリからの参照を許可 allowSameModule : true , } , // UI コンポーネントの依存ルール { module : 'src/components/UI' , allowReferenceFrom : [ 'src/pages' , // Next.jsのルーティング用ディレクトリ 'src/components/Pages' , // ページ専用コンポーネント 'src/components/Layouts' , 'src/components/Models' , ] , allowSameModule : true , } , // (省略)他にも多数のルールを定義... ] ] Pages ディレクトリの特殊な設定 上記の設定において、Pagesディレクトリ( src/components/Pages )のみ allowSameModule: false を採用しています。これは、ページ間の独立性を保証するための設計です。 Pagesディレクトリは Cart/ 、 Home/ 、 Search/ のようにページごとにサブディレクトリが分かれており、各ページ専用のコンポーネントが配置されています。 allowSameModule: false により、あるページのコンポーネントが別のページのコンポーネントを参照することを禁止しています。 // NG: Cart ページが Home ページのコンポーネントを参照 import { HomeComponent } from "../Home/HomeComponent" ; もし複数のページで使いたいコンポーネントが出てきた場合、本来はModels、UI、Layoutsのいずれかに配置すべきコンポーネントである可能性が高いため、適切なディレクトリへの移動を検討します。 循環参照のチェック dependency-cruiser を導入し、PR上で循環参照を自動検出しています。これにより、複雑な依存関係による保守性の低下を未然に防いでいます。 現在の運用規模 この設計で約4年間運用した結果、執筆時点では以下のような規模でコンポーネントが配置されています。 ディレクトリ 割合 UI 約 50% Functional 約 20% Models 約 13% Pages 約 11% Layouts 約 6% UI インフラ層の設計 コンポーネント層で使用する共通基盤として、スタイル定義やテーマシステムを管理しています。 ディレクトリ構成 src/ui/ ├── themes/ # 色・mixin・関数・ブランドテーマ定義 │ ├── mixin/ # Mixin ヘルパー │ ├── function/ # 関数ヘルパー │ └── themeVariants/ # ブランド別テーマ ├── styled/ # Emotion のスタイル関数群 ├── libs/ # UI ユーティリティ ├── constants/ # UI 共通定数 └── stylelint-plugins/ # カスタム Stylelint ルール 統一されたスタイル定義 ZOZOTOWNではCSS in JS(Emotion)を採用しています。SassやPostCSSのようにmixinやfunctionを用意し、ホバーエフェクトやタイポグラフィ計算など、スタイリングの共通処理を提供しています。 const Button = styled.button ` ${ ( { theme } ) => theme.mixin.hoverOpacityEffect() } ; ` ; const Label = styled.span ` font-weight: ${ ( { theme } ) => theme.function.fontWeight( "bold" ) } ; ` ; 実装者はスタイルの詳細を意識せず、一貫性のあるUIを構築できます。 品質担保 Stylelintプラグインにより、 EmotionのSSR制約 ( :first-child 等)や line-height など、プロジェクト固有ルールを自動検証しています。 テーマシステムの活用 これらの基盤を活用した具体例として、ZOZOTOWNではThemeProviderを用いたテーマシステムを導入しています。 ブランドテーマ ZOZOTOWNには、特定のブランド向けにブランドカラーを反映しているページがあります。対象ブランド数は多くありませんが、拡張性を考慮してテーマシステムを利用しています。 const BRAND_COLOR = "#000" ; // ブランドカラー export const colors: ColorTheme = { button : { primary : { background : BRAND_COLOR } , } , text : { red : BRAND_COLOR, blue : BRAND_COLOR, } , } ; 同じ「カートに入れる」ボタンでも、ブランドページでは自動的にそのブランド固有のカラーが適用されます。 サイトジャック ブランド出店の施策で「サイトジャック」と称して、ZOZOTOWNのカラーを期間限定でブランドカラーに置き換えることがあります。 const JACK_COLOR = "#FF1493" ; // サイトジャックカラー export const siteJackTheme: ColorTheme = { header : { background : JACK_COLOR } , navigation : { border : JACK_COLOR } , button : { primary : { background : JACK_COLOR } } , // ... } ; このテーマを適用すると、ZOZOTOWNのヘッダーやナビゲーションなどの主要な要素が、期間限定でブランド固有のカラーへ一括変更できます。 まとめ 本記事では、ZOZOTOWNフロントエンドにおけるディレクトリの分割戦略について紹介しました。コンポーネントを5つの役割に分類し、判断フローと依存関係のルールを設けることで、配置場所で迷わない設計を実現しています。また、自動チェックの仕組みやUIインフラ層の整備により、一貫性のある開発環境を構築しています。 もちろん、ファイル数の増加により見直しを検討している箇所も出てきています。しかしながら、当初の設計から大きく変えずとも大規模開発に耐えうる基盤が維持できており、新規メンバーもスムーズに開発に参加できる状況です。責務分離を意識したディレクトリ設計は、長期的な開発において有効なアプローチであると実感しています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
ごあいさつ レバレジーズ株式会社 アジャイルエフェクトチームの田代です。 我々アジャイルエフェクトチームは、 スクラムにおける様々なプロセスを可視化し、生産性改善に繋げるためのSaaSプロダクト「 Agile Effect 」 を開発しています。 当プロダクトは、 とあるPMの社員が社長に直接事業企画を提案したことがきっかけで誕生しました。 昨年4月に事業が正式に立ち上がり、そこからわずか1年後の今年4月に正式リリースを迎えることができました。(レバレジーズは、アイデアを積極的に取り入れられる風土があり、意思決定の速さと挑戦を歓迎するカルチャーが色濃く根付いています!) アジャイルエフェクトチームの特徴は 「どのチームよりアジャイルを体現した組織」 であることです。「スクラム改善のプロダクトを開発する上でアジャイルを体現し効率的にプロジェクトを進められるチームであることは大前提」というプライドの下、5名で事業をリードしています。 それぞれの得意分野や主な役割こそありますが、明確な役割分担はありません。 日々の業務の中で、POが開発や設計に関わることもありますし、開発メンバーが企画・営業・マーケに関わることもあります。また、Agile Effectを活用することで全メンバーが主体となって改善サイクルを高速で回すことができています。 これにより高いアジリティを発揮し、 「定常的な週2〜3回ペースでのリリース」 「ユーザーから要望があった機能の1週間以内リリース」 などが実現できています。 我々がプロデュースする 「Agile Effect」 には、社内外でご利用いただいているスクラムチームの知見が詰め込まれており、今後も要望を取り入れながら、プロダクトを漸次的にアップデートし続けていきますので、ぜひ無料でご試用ください。 皆様のご利用をお待ちしております! はじめに 早速ですが、エンジニアの皆様。 「フロントエンド開発」やっていますか? 「CSS」書いてますか? 当記事では、TypeScriptベースのスタイリングライブラリである 「Panda CSS」 のご紹介と、実運用で得た知見を共有いたします。 これらの問いに対して「YES」と回答した方であれば、学びに繋がる内容となっていますのでぜひご一読ください。 対象読者 アプリケーションのフロントエンド開発に携わるエンジニア 特に、スタイリングに関する内部品質で悩んでいる方 Reactで新しくアプリケーションを作ろうとしている方 Panda CSSの実運用事例について詳しく知りたい方 記事を通して得られること TypeScriptベースのCSSライブラリ「Panda CSS」について 他のスタイリング手法(CSS Modules/SCSS/Tailwindなど)とPanda CSSの比較 型安全なCSSやデザインシステム構築に役立つPanda CSSの基本知識 実際のプロダクトで運用する際に生じる課題と対処方法 Panda CSSを使った際の開発効率向上や保守性アップの具体的なイメージ 以上の内容をカバーすることで、読者の皆様がPanda CSSの導入メリットを把握し、プロダクトのスタイリング設計をより快適に進められるようになることを目指しています。今後のフロントエンド開発に、少しでもお役に立てれば幸いです。 どう幸せになったのか まず結論をお伝えすると、Panda CSSを採用したことで以下のような改善が実現し、開発体験が劇的に向上しました: 型安全なCSS :TypeScriptによるスタイリングで開発速度と保守性が向上。 宣言的かつ画一的なスタイリング :動的な値に基づくスタイリングが宣言的かつ1箇所に集約され、保守性/可読性が向上。 DOMとスタイリングの責務分離 :「Typography」「Flex」など、スタイリング用のコンポーネントがDOMに影響してしまう悩みからの解放。 デザインシステムの踏襲 :デザイントークンを型レベルで組み込み、デザインとの乖離を排除。 ご存じのとおり、Reactのスタイリングには「Sass/SCSS」「CSS Modules」「Styled Components」「Tailwind CSS」「Emotion」「StyleX」など様々な選択肢があります。それぞれ一長一短があるため、どれを採用するのか悩ましいところですが、今回紹介するPanda CSSは、 これらの手段の“いいとこ取り”を叶えてくれる存在 だと感じています。 実運用を通じても開発体験の良さを実感しており、個人的にはPanda CSSが 「Reactのスタイリング手法における最適解」 だと思っています。 ここで挙げた以外にもPanda CSSを使うことで得られる恩恵は様々であり、記事全体を通してご紹介出来ればと思います。 目次 ごあいさつ はじめに 対象読者 記事を通して得られること どう幸せになったのか 目次 導入背景 Panda CSSの特徴と、他のスタイリング手法との比較 Panda CSSを使った基本のスタイリング 基本のスタイリング構文 スタイル生成の流れ まとめ Panda CSSで「書きやすさ」と「最適化」を両立 ※記事全体で分量が多くなってしまったため、記事を3つに分けて順次公開していきます。 今回はPart1となります。 Part1:Panda CSSとの出会いとその魅力 導入背景 Panda CSSの特徴と、他のスタイリング手法との比較 Panda CSSを使った基本のスタイリング Part2:実践!Panda CSSの使い方(Coming soon) Part3:Panda CSSの課題と未来への展望(Coming soon) Part1では、Panda CSSの採用に至った背景とその魅力についてお話しします。 後日公開するPart2/Part3では、実際にどのようにプロダクトで運用しているかを、設計事例とともに紹介する予定です。 導入背景 「Agile Effect」では、Reactを用いてフロントエンドを開発しています。 立ち上げから約1年間はCSS Modulesを使っていましたが、次第に以下のような課題が出てきました: 静的型付けが効かない 単純に開発体験の悪さを感じ、エラー発生やコミュニケーション面など、将来的な開発スピードへの影響が懸念されました。 デザインシステムの作成開始 デザイン自体がまだプロトタイプの状態でしたが、本格的なデザインシステム制作がスタート。スタイリング設計を見直す大きな要因に。 汎用スタイルコンポーネントの責務拡大 「Typography」「Flex」など、スタイル適用のためのコンポーネントが肥大化し、SRP原則から逸脱しはじめていました。 デザイントークンの取扱い 基本的なスタイリングはCSS Modulesで行いましたが、デザイントークンに限ってはTypescriptの定数を経由し、Inline Styleで管理していました。この影響で、スタイルの記述が.module.cssと.tsxの双方に散在していました。 これらを踏まえ、スタイリング設計をゼロから見直すことに。 調査と比較検討の末、我々が採用したのが Panda CSS でした。 Panda CSSの特徴と、他のスタイリング手法との比較 CSSは、TypeScriptベースの“ゼロランタイム”CSS-in-JSライブラリです(セットアップなどの基本知識はここでは割愛します)。公式ドキュメントの「なぜPandaを選ぶのか?」にあるとおり、主に以下のような特徴が挙げられます。 静的解析 : ビルド時にスタイルを解析・分析し、フレームワークに依存しない純粋なCSSファイルを出力します。 PostCSS : 静的解析の後、PandaはPostCSSプラグインのセットを使用して、ビルド時に解析されたデータをアトミックCSSに変換します。これにより、PandaはPostCSSをサポートする任意のフレームワークと互換性があります。 Codegen : ブラウザ実行時にスタイルを注入せず、ビルド時に軽量なランタイムJSコードを生成。結果としてランタイム負荷が低くなります。 型安全性 : Pandaは、cssプロパティとデザイントークンの型安全性を提供するために、csstypeと自動生成された型付けを組み合わせます。 パフォーマンス : 必要なアトミックCSSのみを最適化して出力。不要なCSSが入らずバンドルサイズが抑えられます。 開発者エクスペリエンス : Pandaは、レシピ、パターン、デザイントークン、JSXスタイルプロップなどの豊富な機能により、優れた開発者体験を提供します。 モダンCSS : カスケードレイヤー、CSS変数、:where/:is等、最新のCSS仕様に対応。 また、私なりに他のスタイリング手法とのおおまかな比較表も作成してみました。 項目 静的型解析 実行タイミング パフォーマンス 型安全DesignTokenサポート 動的スタイリング Sass/SCSS (Pure CSS) × ビルド時 ⚪︎ (肥大化しやすい) × △ (JSが必要) CSS Modules △ (型生成を支援するライブラリ使用で一部可) ビルド時 ⚪︎ (肥大化しやすい) × △ (JSが必要) Styled Components / Emotion △ (プロパティレベルの解析不可) ランタイム時 × △ (型レベルの厳密性は薄い) ◎ Tailwind CSS △ (プロパティレベルの解析不可) ビルド時 ◎ △ (型レベルの厳密性は薄い) ⚪︎ Panda CSS ◎ ビルド時 ◎ ◎ ⚪︎ 表から分かる通り、 Panda CSSにはこれといった大きなデメリットが見当たらず、静的解析や型安全性、ゼロランタイムなどの特色はどれも非常に魅力的でした。 もちろんプロジェクトとの相性等はあるかと思いますが、我々の場合は移行負荷も低く大きな懸念が無かったため、全面的にPanda CSSへ移行することにしました。 Panda CSSを使った基本のスタイリング ここからは、Panda CSSにおけるスタイリングの一連の流れを見ていきます。 公式ドキュメントにあるように、Panda CSSは「TypeScriptベースのコード定義をビルド時に解析し、最終的に純粋なCSSファイルを出力する」フローが大きな特徴です。 ポイント: Panda CSSでは、最新のカスケードレイヤーが活用されているのも大きな特徴の一つです。 CSSレイヤーを活用することで、複数のレイヤーにまたがるスタイルの優先順位が明確化され、アトミックCSSやデザイントークンとの相性がより良くなっています。詳しくは、公式ドキュメントのカスケードレイヤーのページをご確認ください。この仕組みを前提に、次章からはPanda CSSでどのようにスタイルを定義し、ビルドしているのかをご紹介していきます。 基本のスタイリング構文 Panda CSSのスタイリングは、styled-system/css が提供する css() や cva() などの関数を用いて定義します。まずはもっともシンプルなcss()を使った例を見てみましょう。 こちらが最も基本的な定義の例です。 import { css } from '../styled-system/css' const styles = css( { backgroundColor : 'gainsboro' , borderRadius : '9999px' , fontSize : '13px' , padding : '10px 15px' } ) // 生成されるクラス名: // --> bg_gainsboro rounded_9999px fs_13px p_10px_15px < div className = { styles } > < p > Hello World </ p > </ div > この定義を基にビルドした結果、CSS出力は次のようになります。 @layer utilities { .bg_gainsboro { background-color : gainsboro ; } .rounded_9999px { border-radius : 9999px ; } .fs_13px { font-size : 13px ; } .p_10px_15px { padding : 10px 15px ; } } 上記のように、TypeScriptのオブジェクト構文でCSSプロパティを記述するだけでOKです(プロパティ名や値は型安全性が効きます)。実行時ではなくビルド時にCSSに変換され、ブラウザに読み込まれるころには純粋なCSSファイルとして提供されます。 ポイント: css()以外にも、バリエーションを定義するためのcva()や、汎用レイアウトなどをRecipeとしてまとめるdefineRecipe()など、複数のアプローチがあります。 どれも「TypeScriptでCSSを記述する → ビルド時に最適化されたCSSが生成される」という点は共通です。(参考) スタイル生成の流れ Panda CSSを利用すると、TypeScriptで記述したスタイルがビルド時に最適化され、最終的には純粋なCSSファイルとして出力されます。ここでは、その一連の流れを順に確認していきます。 1.「型付きのスタイル定義」を記述する まず、開発者が行うのはTypeScriptによるスタイル定義です。 css() や cva() に渡すオブジェクトや変数には 型安全性が適用されるため、プロパティ名や値の誤りが早期に検知できます。 2.ビルド時に「原子化」と「不要なものの排除」 ビルドプロセスでは、Panda CSSがソースコードを走査し、同じプロパティや重複する宣言をまとめて Atomic CSSへ変換します。 Atomic CSS: CSSプロパティ単位でクラスを生成する考え方です(例: .p_8px_16px, .rounded_4px など)。 使われていないクラスは自動的に削除され、必要最小限のCSSだけがビルド成果物として残ります。 3.純粋なCSSファイルとしてブラウザへ 最終的な変換結果は、特定のJSライブラリに依存しない 純粋なCSSファイルとして出力されます。 つまり、ランタイムでスタイルを注入する必要はありません。ページ読み込み時には、既に最適化されたCSSが供給されているため、 “ゼロランタイム”を実現できます。 ポイント ・型安全にCSSが書ける ・バンドルサイズが肥大化しにくい ・実行時のオーバーヘッドが少ない ・クラス名の競合を最小限に抑えられる このように、 型安全性と最適化済みのCSS出力を同時に得られるところが、Panda CSSの大きな特長です。 まとめ Panda CSSで「書きやすさ」と「最適化」を両立 書きやすさ TypeScriptでのオブジェクト記述なので、補完や型チェックが効き、スタイル記述のミスを減らせます。 最適化 ビルド時にスタイルを集約・最適化して出力するため、ランタイムで注入する仕組みよりもパフォーマンス面のメリットがあります。 Part2以降では、これらの基盤を活かしてデザインシステムをどのように型安全に落とし込んでいくか、さらに踏み込んだ使い方を紹介します。 今回紹介した「基本のスタイリング」の部分だけでも十分恩恵がありますが、 Panda CSSには、Design TokenやRecipe機能など、より高度なスタイリングパターンをサポートする仕組みが豊富に用意されています。  これらを組み合わせることで「デザインシステムと型安全性の融合」を実現し、プロダクト全体のスタイリングを一貫性のあるものへと導いてくれます。 次章ではさらに踏み込んで、Panda CSSが提供する「Recipe(CVA)」や」Config Recipe」を活用した共通スタイルの定義や、デザイントークンを組み込んだデザインシステム構築など、 より実践的な活用事例を紹介していきます。 加えて、テスト環境やStorybookでスタイルが反映されない際のトラブルシューティングなど、 リアルな運用Tipsについても触れます ので、ぜひご期待ください。 ご一読いただき、ありがとうございました!

動画

該当するコンテンツが見つかりませんでした

書籍