ecalj を Claude (LLM) で開発する際のガイドライン。 コーディング規約、ビルドの罠、GPU 開発の教訓、アーキテクチャ方針をまとめる。
ecalj は Fortran module を singleton class として使う 設計パターンを採用している。 長年の Fortran 開発で type (derived type) ベースの設計が引き起こす問題 (データ多重化、サイズ不整合、OpenACC 非互換、可読性劣化) と格闘した経験から、 module の言語特性を活かした引き算の設計に到達したもの。
Fortran 2003/2008 の機能は選択的に取り込む:
- 使う:
allocatable,module,protected,use only,associate,block - 使わない:
class,type-bound procedure,select type,inheritance
判断基準は「Fortran の配列計算言語としての強みを活かすか、OOP の後付け機能で 複雑さを持ち込むか」。前者を取り、後者を捨てる。
Fortran の module は言語仕様上、プログラム内で唯一のインスタンスを持つ。 これは OOP の singleton と同じ性質であり、科学計算の多くのサブシステム (ハミルトニアン、波動関数ストレージ、自己エネルギー計算器など) は 複数インスタンスが不要なため、module = singleton が自然に適合する。
C++ や Python では singleton pattern を「わざわざ」実装する必要があるが、 Fortran では module 自体がそれを提供する。ecalj はこの言語特性を 意識的・体系的に活用している。
- 1 module = 1 責務 = 1 状態セット: 複数インスタンス不要なら
typeを被せない - 状態は module 変数で公開:
protected, publicで読み取り公開、書き換えは module 内 subroutine 経由のみ - import は明示: 必ず
use m_foo, only: bar, bazで必要な symbol だけ取る - subroutine 規約: 入力=引数、出力=module 変数で返す
type(derived type) は積極的に避ける: Fortran の type には以下の罠があるintent(out)で allocatable components が暗黙に deallocate されるpresent(state%alloc_member)が nvfortran + OpenACC で silent fail- 合成 type の copy semantics が不明瞭
use onlyでデータフローが見えない (type の中身は呼出側から不透明)
singleton module 間のデータ受け渡しは use の階層で表現する。
各 module は自分より下位の module だけを use し、循環依存を禁止する。
これにより依存関係が DAG (有向非巡回グラフ) になり、データの流れが一方向に追える。
m_struct_from_lmf (構造: plat, alat, pos, z)
↑ use
m_gw_product_basis (積基底: ndima, nlnx)
↑ use
m_sxcf_sc (自己エネルギー計算)
↑ use
main_hrcxq / hgw_combined (メインプログラム)
データフローの規約:
- 大量データの入力:
use m_foo, only: barで下位 module のprotected変数を読む。subroutine 引数では渡さない - subroutine 引数: フェルミエネルギー
ef、q 点qp、スピンisp、反復回数niterなど、指示的・局所的な値に限る。配列データを引数で渡し回さない - 出力: subroutine が計算した結果は module 変数に格納し
protectedで公開。caller はuse onlyで読む
この規約により:
type(state)を引数で渡し回す必要がない (type を避けられる)- どの module がどのデータを提供し、誰が消費するかが
use onlyで完全に見える grep 'use m_foo' SRC/subroutines/*.f90で依存関係を即座にトレースできる- module の差し替え (例: FILE → MEMORY backend) が caller に影響しない
Fortran 2003 で class, type-bound procedure, select type, inheritance が導入されたが、
これらは C++/Java のために設計された OOP 概念を配列計算特化言語に後付けしたもので、
実用上の問題が多い:
select typeによる polymorphism は冗長で可読性が低いtype-bound procedureは module procedure への syntax sugar に過ぎない- inheritance は Fortran のメモリモデル (allocatable component) と相性が悪い
- コンパイラのバグが多い (特に nvfortran + OpenACC + derived type の組み合わせ)
ecalj の singleton + 階層 use は、Fortran の元々の強み (module, allocatable, 配列操作) だけで 設計を完結させる 言語に逆らわない アプローチ。
- OpenACC/GPU との相性: device 変数の管理が単純。module 変数は全 subroutine から直接アクセスでき、
!$accディレクティブで素直に扱える。type のメンバを device に置くと nvfortran の制約に次々ぶつかる - データフローが grep で追える:
use m_foo, only: barを grep すれば誰が何を使っているか一目瞭然。type だとstate%barを追う必要があり、複数の type が絡むとトレースが困難 - re-entrant 化が楽: module 変数を dealloc-realloc すればいい。type のネストした copy semantics を考えなくていい
- backend 切替が caller 透過: module 内部で FILE / MEMORY_3D 等の実装を切り替えても、caller は同じ module 変数を
useするだけ - LLM にも優しい: module のヘッダ (変数宣言部) を見れば状態が全部わかる。type の定義を辿って中身を理解する必要がない
従来の subroutine 引数渡し方式では、同じ変数を caller と callee の両方で宣言する 必要がある (二重記述):
! 呼ぶ側
call calc_sigma(ndim, nqibz, nspinmx, qibz, wk, ekc, ...)
! 呼ばれる側 — 同じ変数を再宣言、サイズも再指定
subroutine calc_sigma(ndim, nqibz, nspinmx, qibz, wk, ekc, ...)
integer, intent(in) :: ndim, nqibz, nspinmx
real(8), intent(in) :: qibz(3,nqibz), wk(nqibz), ekc(ndim)引数が 10〜20 個になると caller と callee の対応がずれてバグになる。 配列サイズの整合性も人間が保証しなければならない。二重記述は百害あって一利なし。
singleton + use only なら変数宣言は module 内の一箇所だけ:
! 呼ばれる側 — use で取るだけ、再宣言不要
subroutine calc_sigma(ef, qp, isp) ! 指示値のみ引数
use m_read_bzdata, only: qibz, wk, nqibz
use m_struct_from_lmf, only: nspinmx- 変数の生成元が唯一 — 二重記述ゼロ
- サイズ不整合が原理的に起きない
- 引数リストが短い (指示値のみ) ので caller-callee の対応ミスが起きない
深い呼び出し階層での追加データ問題: 5段の call 階層の末端で natom が1つ必要になった場合:
- 引数渡し: 途中の全 subroutine (5箇所) の引数リストに
natomを追加。大量の変更、リグレッションリスク - type:
state%natomとして渡すと、データが本来の出自 (生成元 module) から切り離されコピーが増える。実際に起きるのは、たまたま 5 段目まで通っている type にnbasを便乗で放り込んでしまうこと。やがてcrystal_state%nbas、gw_state%nbas、basis_state%nbasと複数の type に同じ値のコピーが散在し、どれが正でいつ同期されるか誰にもわからなくなる — データの唯一性が簡単に消失する - singleton use:
use m_struct_from_lmf, only: natomを末端の1箇所に追加するだけ。途中の subroutine は変更不要、データの出自はm_struct_from_lmfと明確
- subroutine の出力がシグネチャに現れない: 出力は module 変数に書かれるため、関数定義だけ見ても「何を返すか」がわからない。ただし caller 側の
use m_foo, only: barを見れば出力は特定でき、LLM なら module 宣言部と caller の use only を同時に見て追跡できるため、実質的な問題は小さい - module 状態の一括クリーンが面倒: type なら
deallocate(state)で一発だが、module 変数は個別に deallocate する subroutine が必要。ただし LLM は module ヘッダの変数宣言を見て漏れなく dealloc subroutine を書けるため、LLM 時代にはこの弱点の実害が縮小している
type(wv_storage) / type(sxcf_state) を捨て、module-level singleton 化した結果:
- 行数減: type 定義、constructor、accessor が不要に
- OpenACC 制約回避: structured data region 内の BLOCK 禁止等を自然に回避
- FILE ↔ MEMORY_3D backend 切替が caller 透過に: m_wv_storage 内部のフラグ切替だけで、m_sxcf_sc や main_hrcxq は変更不要
- hgw_combined 統合が容易に: hrcxq + hsfp0_sc の状態が module 変数として共有できるため、2つのプログラムを1プロセスに統合する際の状態受け渡しが自然
commのような caller ごとに異なる値は subroutine 引数の optional として直交化
Fortran には「allocatable 配列の配列」(ragged array) を直接書く構文がない。
real(8), allocatable :: a(:) の配列 a(n) は作れない。
m_struc_def.f90 はこの言語制約を回避するための最小限の wrapper type を定義している:
type s_rv1
real(8),allocatable:: v(:)
end type s_rv1
type s_rv2
real(8),allocatable:: v(:,:)
end type s_rv2
! ... s_rv4, s_rv5, s_cv3, s_cv4, s_cv5, s_nv2, s_sblock使用例: type(s_rv2) :: basis(natom) で、basis(1)%v は (10,5)、basis(2)%v は (8,3) のように各原子で異なるサイズの配列を持てる (C の double **a に相当)。
これは singleton 規約の例外ではなく、状態を持たない純粋なコンテナ型。 振る舞い (subroutine) を持たず、module 間のデータフローを隠蔽しない。 singleton が禁じている「状態管理 type」とは明確に区別される。
新規モジュールを設計する時:
- まず module 変数 (state) と公開する subroutine を決める
typeを導入したくなったら一度立ち止まる — 本当に複数インスタンス必要か?- caller に
type(...) :: xを持たせるくらいなら module-level singleton にする - subroutine 引数は scalar/array of intrinsic types に限定 (state は use で渡す)
- comm のような「caller ごとに異なる値」は subroutine 引数の optional として直交化
- ポインタ配列 (ragged array) が必要な場合のみ m_struc_def のコンテナ型を使用
singleton module 設計は Python module と 1:1 対応する:
| Fortran | Python |
|---|---|
module m_foo |
m_foo.py |
use m_foo, only: bar |
from m_foo import bar |
protected :: bar |
module-level 変数 (慣習的に _ prefix や getter) |
subroutine init(ef) |
def init(ef): + global bar |
type ベースの設計を Python に移植すると class 設計をゼロから考え直す必要があるが、
singleton なら Fortran module → Python module にほぼ機械的に変換できる。
GPU 計算も torch.tensor + .cuda() で module 変数を device に置けるため、
OpenACC の !$acc ディレクティブと構造が対応する。
将来的に考えている方向:
- Python ラッパー: 各 Fortran singleton module を Python module でラップし、 外部 (ユーザー、ワークフロー管理、機械学習パイプライン) には Python インターフェースを見せる。 Fortran は module 内部の高速計算カーネルを担う
- 段階的移行: module 単位で Fortran → Python/PyTorch に置換可能。
singleton 間の依存が
use only/importで明示されてい���ため、 1 module ずつ差し替えても全体が壊れない - 既にライブラリ化済み: ecalj は
libecaljF.so(共有ライブラリ) + 薄い main プログラムの構成。各 main (lmf, hsfp0_sc, hgw_combined 等) は数十行で、 singleton module の subroutine を呼ぶだけ。Python からctypes/f2pyでlibecaljF.soを直接呼べるため、main を Python で書き直すのは容易 - gwsc のライブラリ直接呼び出し化: 現在の gwsc は subprocess で Fortran バイナリ
を起動しているが、将来は
libecaljF.soの subroutine を Python から直接呼ぶ構成に 移行可能。プロセス起動オーバーヘッド (MPI_Init, 入力ファイル再読み込み、共有ライブラリ 再ロード) が消え、singleton module 変数がプロセス内で共有されるため、ステップ間の 状態受け渡しがゼロコストになる。hgw_combined が hrcxq + hsfp0_sc を 1 プロセスに 統合したのと同じ原理を、gwsc 全体に拡張するもの
- nvfortran 26.1 (NVIDIA HPC SDK)
FC=nvfortran cmake .. -DBUILD_GPU=ON -DBUILD_MP=OFF -DBUILD_MP_GPU=ON
BUILD_MP=ONは使わない: rdsigm2.f90 で nvfortran が signal 11 (コンパイラクラッシュ) を起こす。BUILD_MP=OFF,BUILD_MP_GPU=ONのみ使用- nvfortran の間欠的 signal 11:
-j4ビルドでランダムにコンパイラがクラッシュする。make -j4を数回リトライすれば通る。特定ファイルではなく毎回異なるファイルで起きる - cmake の file(GLOB): 新規 .f90 ファイル追加時は必ず cmake 再 configure が必要。make だけでは検出されない
- GPU FP 非決定性: 同コードで GPU0 と GPU1 の run-to-run で md5 が異なる。回帰比較は
CUDA_VISIBLE_DEVICES固定で同条件統一すること
~/ecaljdeveloper/SRC/ Fortran ソース
/SRC/exec/build/ ビルド (GPU=ON, MP=OFF, MP_GPU=ON)
/SRC/exec/ gwsc, ctrlgenToml.py 等のスクリプト
~/bin/ インストール先 (汎用)
~/bin2/ GW1500 production 用 (slot_scheduler 対応 run_cmd.py)
m_sxcf_sc.f90 に !$acc async(1) を追加して GPU kernel を非同期実行する最適化を入れたところ、hsfp0_sc_mp_gpu が生成する sigm が完全に壊れ、全物質の lmf が zhev_tk4: nev/=nevout で即死した。
教訓:
- testecalj PASS は GPU 信頼性の証明にならない — 2 原子系ではタイミング依存バグが顕在化しない。実物質 (6-8 原子系) でのテスト必須
- GPU async は同期ポイントの設計が極めて難しい —
!$acc waitが 1 箇所でも不足すると sigm が完全破壊 (中間的劣化ではなく NaN/overflow) - ベンチマーク未実施で production に入ってはいけない — 性能ゲイン未計測のままリスクを取った
- async 再導入時の必須テスト: GW1500 実物質で sigm の bit-wise 一致テスト、
!$acc waitの網羅性検証 (全 data region exit 前、全 host_data 前、全 CPU-GPU 境界)
- nvfortran は
!$acc data(structured) スコープ内の FortranBLOCKを許可しない →!$acc enter/exit data(unstructured) で回避 attributes(device)変数は Fortran スコープで自動解放される → スコープを跨ぐ場合は外部スコープに移動!$acc wait(1)は goto パス (エラーハンドリング) でも必要
Fortran バイナリは以下のみを読む:
ctrlg.<sname>.toml— ctrl + GW driver sections + PB cut-offs の統合 TOMLPB.toml— per-atom product-basis tables (sname-free)
# POSCAR から新規生成
ctrlgenToml.py <sname> # writes ctrlg.<sname>.toml + PB.toml
# 旧形式 (ctrl + GWinput) からの移行
Legacy2toml.py <sname> # writes ctrlg.<sname>.toml + PB.tomlOLD: lmf si -vnk=8 -vmetal=3
NEW: lmf si --ctrlg:bz.nkabc=[8,8,8] --ctrlg:bz.metal=3
ctrlg.foobar.toml(foobar = 物質ID, mp-1234 等)- ディレクトリで識別可能でもファイル名に ID を残す — HTP バッチで取り違え防止、LLM のクロスリファレンス誤認防止
hrcxq (screened interaction W) + hsfp0_sc (exchange Sx + correlation Sc) を 1 MPI プロセスに統合。
__WVR/__WVI ファイル中継 (30-90 GB/物質) を排除し、module-level buffer で in-memory 受け渡し。
gwsc での呼び出し:
# 旧 (3ステップ、ファイル中継)
hsfp0_sc --job=1 # Sx
hrcxq --jobgw=1 # W → __WVR/__WVI 書き出し
hsfp0_sc --job=2 # Sc ← __WVR/__WVI 読み込み
# 新 (1ステップ、in-memory)
hgw_combined --jobgw=1 # Sx + W + Sc (メモリ内)段階的に singleton リファクタ + ストレージ抽象化 + streaming 統合を進めた:
| Step | 内容 | 主要ファイル |
|---|---|---|
| WB.1 | m_wv_storage: FILE/MEMORY_3D 両 backend のストレージ抽象化 | m_wv_storage.f90 |
| WB.2a | m_sxcf_sc: sxcf_scz_correlation を init/step_kx/finalize に分割 | m_sxcf_sc.f90 |
| WB.2b | Phase 1-B 4D in-memory モード削除 (WB.1 の MEMORY_3D に統一) | m_llw, m_w0w0i, main_hrcxq |
| WB.3a | m_wv_storage: type(wv_storage) → module-level singleton 化 | m_wv_storage.f90 |
| WB.3b | m_sxcf_sc: type(sxcf_state) → module-level singleton 化 | m_sxcf_sc.f90 |
| WB.3c | m_hsfp0_sc: hsfp0_sc を setup/consume/writeout 三相に分割 | main_hsfp0.sc.f90 |
| WB.3d | m_hgw_iq_loop: iq production loop を main_hrcxq から抽出 | m_hgw_iq_loop.f90 |
| WB.3e | streaming 統合: per-iq W 生成と per-kx sxcf 消費をインターリーブ | main_hrcxq.f90 |
main_hrcxq.f90 (= hgw_combined のエントリポイント)
│
├── m_hgw_iq_loop: iq ループ (W の生成、各 iq で)
│ ├── m_llw: W(iq) を計算 → m_wv_storage に書く
│ └── m_w0w0i: W(iq=1) の修正 (effective W(0))
│
├── m_sxcf_sc: kx ループ (self-energy の消費、各 kx で)
│ ├── sxcf_correlation_init()
│ ├── sxcf_correlation_step_kx(kx) ← m_wv_storage から W を読む
│ └── sxcf_correlation_finalize()
│
└── m_hsfp0_sc: writeout (SEC/SEX ファイル書き出し)
m_wv_storage: W データの橋渡し
├── FILE backend: __WVR/__WVI ファイル (旧方式、fallback)
└── MEMORY_3D backend: module-level 3D buffer (hgw_combined 用)
この統合は singleton 設計なしには困難だった:
- m_wv_storage が singleton だから、hrcxq (producer) と sxcf (consumer) が同じ module 変数を共有できる。type で渡すなら巨大な W バッファの所有権管理が必要
- m_sxcf_sc が singleton だから、init/step_kx/finalize の3相に分割しても状態が module 変数に保持される。type なら caller が state を管理しなければならない
- m_hsfp0_sc が singleton だから、hrcxq の後に skip_init で呼べる。独立バイナリだった hsfp0_sc を「関数呼び出し」に変換できた
- backend 切替が caller 透過: m_wv_storage 内部で FILE → MEMORY_3D に切り替えても、m_sxcf_sc と main_hrcxq のコードは変更不要
WB.3e の上に GPU async overlap (stream 1 で imagaxis を非同期実行) を追加した WB.4 は、 testecalj では PASS したが実物質で sigm を破壊した。詳細は「GPU 開発の教訓」セクション参照。 現在は WB.3e ベース (同期実行) に戻して production 稼働中。
- Unix socket ベース (
/tmp/slot_scheduler.sock) - CPU slot ×2 (lmf, np=30 each) + GPU slot ×2
- 6 workers が FIFO 順にスロット取得
~/bin2/run_cmd.pyが自動でバイナリ種別を検出し slot request~/bin2/slot_scheduler_daemon.pyが割当管理- 詳細:
ecalj_auto/README_slot_scheduler.md
- worker.sh が 5 分ごとに
lsc,lsx,lqpe,llmfの NaN をチェック - 8 時間ハードタイムアウト
- 検出時は gwsc + 子プロセスを kill
- NaN in lsc/lqpe: 物質固有の計算困難性 (リトライ不要)
- TIMEOUT 8h: 8 原子系の VRAM 不足等
- bmix minimum: SCF 収束不良
/dev/shm/sem.OMPIO*残骸: kill 後に残存 → 次の hrcxq がハング。rmで即解決