왜 지금 GPU 네이티브 분자 시뮬레이션인가?
전산화학은 오랫동안 정확성과 속도 사이의 줄다리기를 겪어왔습니다. DFT(Density Functional Theory) 같은 ab initio 방법은 높은 신뢰도를 제공하지만 계산 비용이 엄청나서 수백 개 원자 수준의 시스템으로 제한됩니다. 반면 고전적 포텐셜(Classical Force Field)은 빠르지만 복잡한 결합 절단이나 전이 상태 분석에 필요한 화학적 정밀도가 부족합니다.
**Machine Learning Interatomic Potentials (MLIPs)**이 이 간극을 메우는 다리 역할을 하며, 양자 수준의 정확도를 고전 역학 속도로 제공합니다. 하지만 문제는 소프트웨어 생태계에 있었습니다. MLIP 모델 자체는 GPU에서 돌아가지만, 주변 시뮬레이션 인프라는 여전히 CPU 중심의 레거시 코드에 의존하는 경우가 많았죠.
NVIDIA ALCHEMI Toolkit은 이러한 병목을 해결하기 위해 설계된 GPU 가속 시뮬레이션 빌딩 블록 모음입니다. PyTorch 네이티브 환경에서 이웃 리스트 구성, 구조 최적화, 분자 동역학(MD) 적분 단계까지 모두 GPU 위에서 처리함으로써 호스트-디바이스 간 메모리 전송 오버헤드를 제거합니다.
참고: 이 글은 NVIDIA Developer 블로그의 공식 자료를 바탕으로 재구성했습니다. 근거자료에서 더 자세한 내용을 확인할 수 있습니다.

핵심 기능 1: 커스터마이징 가능한 배치 시뮬레이션 워크플로우
ALCHEMI Toolkit의 가장 큰 특징은 GPU 네이티브 배치 다이내믹스 엔진입니다. 단일 MLIP 모델이 모든 화학 환경에 완벽할 수는 없습니다. 특히 비국소적(nonlocal) 장거리 상호작용을 다룰 때 더 그렇죠.
Toolkit은 연구자가 모듈식 화학/재료과학 커널과 딥러닝 모델을 조합하여 커스텀 시뮬레이션 워크플로우를 구성할 수 있게 해줍니다. 아래는 FIRE2 구조 최적화 후 Velocity Verlet 동역학을 실행하는 기본 예제입니다.
from ase import Atoms
from nvalchemi.data import AtomicData, Batch
from nvalchemi.dynamics import ConvergenceHook
from nvalchemi.dynamics.optimizers import FIRE2
from nvalchemi.dynamics.integrator import VelocityVerlet
# 1. 배치 원자 구조 데이터 준비 (GPU에 직접 로드)
atomic_data = [AtomicData.from_atoms(Atoms(...), device="cuda") for _ in range(16)]
batch = Batch.from_data_list(atomic_data)
# 2. MLIP 모델 설정 (MACE, TensorNet 등)
mlip = ...
# 3. 수렴 기준 설정: force norm 0.05 eV/Å 이하, max 0.1 eV/Å 이하
conv_criteria = ConvergenceHook(
criteria=[
{"key": "forces", "threshold": 0.05, "reduce_op": "norm"},
{"key": "forces", "threshold": 0.1, "reduce_op": "max"}
]
)
# 4. 최적화기와 적분기 정의
optimizer = FIRE2(mlip, convergence_hook=conv_criteria, n_steps=200)
velverlet = VelocityVerlet(mlip, n_steps=1000)
단일 GPU에서 파이프라인 실행
FusedStage 클래스를 사용하면 두 다이내믹스 객체를 더하기 연산자로 결합하고 torch.compile로 감쌀 수 있습니다.
fused = optimizer + velverlet
with fused:
# 200 step 최적화 + 1000 step MD 자동 실행
fused.run(batch)
멀티 GPU 분산 실행
파이프 연산자(|)를 사용하면 각 단계를 서로 다른 GPU에 할당할 수 있습니다.
pipeline = optimizer | velverlet
# FIRE2는 GPU 0, Velocity Verlet은 GPU 1에서 실행
with pipeline:
pipeline.run(batch)
더 나아가, 16대의 GPU를 사용해 8대는 구조 최적화, 나머지 8대는 Langevin 동역학을 수행하는 것도 가능합니다.
from torch import distributed as dist
from torch.utils.data.distributed import DistributedSampler
from nvalchemi.data.datapipes import Dataset, DataLoader
dist.initialize_process_group()
dataset = Dataset(...)
data_sampler = DistributedSampler(dataset, num_replicas=dist.get_world_size(), rank=dist.get_rank())
loader = DataLoader(dataset, batch_size=128, sampler=data_sampler, use_stream=True)
# 8개 rank는 최적화, 8개 rank는 Langevin 동역학
optimizers = [FIRE2(mlip, ..., next_rank=index + 8) for index in range(8)]
dynamics = [Langevin(mlip, ..., prior_rank=index) for index in range(8)]
pipeline = DistributedPipeline(
{index: stage for index, stage in enumerate(optimizers + dynamics)}
)
with pipeline:
for batch in loader:
pipeline.run(batch)
이처럼 추상화 덕분에 사용자는 복잡한 분산 시스템 세부 사항을 몰라도 손쉽게 스케일업할 수 있습니다.

핵심 기능 2: 나만의 다이내믹스 클래스 만들기
ALCHEMI Toolkit은 모듈식 아키텍처를 제공하여 완전히 새로운 다이내믹스 클래스를 처음부터 구축할 수 있게 해줍니다. 아래는 모의 담금질(Simulated Annealing) 훅을 포함한 Velocity Verlet 구현 예제입니다.
from enum import Enum
import torch
from nvalchemi.data import Batch
from nvalchemi.dynamics.base import BaseDynamics, DynamicsStage
from nvalchemi.hooks import Hook, HookContext
# 커스텀 훅: MD 스텝마다 온도를 서서히 낮춤
class MySimulatedAnnealer(Hook):
def __init__(self, t_start: float, t_end: float, cooldown_steps: int, frequency: int):
self.frequency = frequency
self.t_start = t_start
self.t_end = t_end
self.cooldown_steps = cooldown_steps
self.stage = DynamicsStage.BEFORE_STEP
self.decay = (t_end / t_start) ** (1.0 / cooldown_steps)
self._current_temp = t_start
def __call__(self, ctx: HookContext, stage: Enum) -> None:
dynamics = ctx.workflow
dynamics.target_temperature = max(
dynamics.target_temperature * self.decay, self.t_end
)
# 커스텀 Velocity Verlet (온도 조절 포함)
class VelocityVerlet(BaseDynamics):
__needs_keys__ = {"energies", "forces", "masses", "velocities"}
__provides_keys__ = {"positions"}
def __init__(self, model, n_steps, dt=1.0, target_temperature=300.0, tau=10.0, hooks=None):
super().__init__(model=model, n_steps=n_steps, hooks=hooks)
self.dt = dt
self.target_temperature = target_temperature
self.tau = tau
self._prev_accelerations = None
def pre_update(self, batch: Batch) -> None:
with torch.no_grad():
accelerations = batch.forces / batch.masses
self._prev_accelerations = accelerations.clone()
batch.positions.add_(batch.velocities * self.dt + 0.5 * accelerations * self.dt**2.0)
def post_update(self, batch: Batch) -> None:
with torch.no_grad():
new_accelerations = batch.forces / batch.masses
batch.velocities.add_(0.5 * (self._prev_accelerations + new_accelerations) * self.dt)
# 온도 스케일링 (Berendsen thermostat)
ke_per_atom = 0.5 * batch.masses * (batch.velocities**2).sum(dim=-1, keepdim=True)
total_ke = scatter_add_(...) # 시스템별 운동에너지 합
current_temp = 2.0 * total_ke / (batch.num_atoms * 3.0)
ratio = self.target_temperature / current_temp
lam = torch.sqrt(torch.tensor(1.0 + (self.dt / self.tau) * (ratio - 1.0))).clamp(min=0.8, max=1.2)
batch.velocities.mul_(lam)
# 사용 예: 900K에서 300K까지 1000스텝 동안 냉각
my_vv = VelocityVerlet(
mlip,
n_steps=1000,
hooks=[MySimulatedAnnealer(t_start=900.0, t_end=300.0, cooldown_steps=10, frequency=100)]
)
핵심 기능 3: 모델 래퍼 (Model Wrappers)
자체 사전 학습 모델을 ALCHEMI 파이프라인에 연결하려면 BaseModelMixin을 상속받아 adapt_input과 adapt_output 메서드만 구현하면 됩니다.
from beartype import beartype
from nvalchemi._typing import ModelOutputs
from nvalchemi.models.base import BaseModelMixin, ModelConfig, NeighborConfig
class BestMLIPWrapper(nn.Module, BaseModelMixin):
def __init__(self, model):
super().__init__()
self.model = model
self.model_config = ModelConfig(
outputs=frozenset({"energy", "forces"}),
required_inputs=frozenset({"positions", "atomic_numbers"}),
neighbor_config=NeighborConfig(cutoff=5.0, format="coo")
)
def adapt_input(self, data: Batch, **kwargs) -> dict:
return {
"atom_numbers": data.atomic_numbers,
"coords": data.positions
}
def adapt_output(self, model_output, data: Batch) -> ModelOutputs:
output = ModelOutputs()
output["energies"] = model_output["energies"]
if "forces" in self.model_config.active_outputs:
output["forces"] = model_output["forces"]
return output
@beartype
def forward(self, data: Batch, **kwargs) -> ModelOutputs:
model_inputs = self.adapt_input(data, **kwargs)
model_outputs = self.model(**model_inputs)
return self.adapt_output(model_outputs, data)
핵심 기능 4: 고급 데이터 관리
전통적으로 CPU와 GPU 간 데이터 이동은 AI 기반 발견의 주요 병목이었습니다. ALCHEMI Toolkit은 AtomicData와 Batch 객체를 통해 데이터를 GPU에 상주시키는 방식으로 이 문제를 해결합니다.
from nvalchemi import AtomicData, Batch
from nvalchemi import data
from ase.build import slab
# ASE Atoms 객체 -> AtomicData (바로 GPU에)
atoms = slab(...)
data = AtomicData.from_atoms(atoms, device="cuda")
# 여러 구조를 하나의 배치로
batch = Batch.from_data_list([data, data, data])
# Zarr 기반 데이터셋에 쓰기/읽기
writer = data.AtomicDataZarrWriter("atom_dataset.zarr")
writer.write(batch)
reader = data.AtomicDataZarrReader("atom_dataset.zarr")
dataset = data.Dataset(reader, device="cuda", num_workers=4)
dataloader = data.DataLoader(dataset, batch_size=16)
for batch in dataloader:
# 여기서 배치 처리
pass

국내 개발 생태계에서의 적용 맥락
국내에서는 주로 반도체 소재, 배터리 전해질, 촉매 설계 등 소재 빅데이터 기반 연구가 활발합니다. 기존에는 VASP, LAMMPS 같은 CPU 기반 코드를 사용했지만, 최근 MACE, TensorNet 같은 MLIP 모델을 도입하는 사례가 늘고 있습니다. ALCHEMI Toolkit을 사용하면:
- PyTorch 생태계와의 통합이 자연스러워 기존 파이썬 코드를 크게 바꾸지 않고 GPU 가속을 적용할 수 있습니다.
- 배치 시뮬레이션을 통해 동시에 수백만 개의 구조를 최적화할 수 있어, 고처리량 스크리닝(High-throughput screening)에 적합합니다.
- 다만, 아직 CUDA 12+와 특정 GPU 세대(RTX 20xx 이상)에 의존적이므로, 구형 GPU 클러스터를 사용하는 연구실은 환경 호환성을 먼저 확인해야 합니다.
이 기술의 한계 및 주의사항
- 모델 의존성: ALCHEMI Toolkit은 MLIP 모델의 래퍼 역할을 하므로, MLIP 자체의 정확도가 시뮬레이션 품질을 결정합니다. DFT 수준의 정확도를 원한다면 MACE, TensorNet 등 검증된 모델을 사용해야 합니다.
- 메모리 제한: 배치 크기가 너무 커지면 GPU 메모리가 부족할 수 있습니다.
batch_size를 조절하거나 그래디언트 체크포인팅 기법을 병행하는 것이 좋습니다. - 운영체제: 공식 지원은 Linux가 주력이며, macOS는 제한적으로 동작합니다. Windows는 아직 공식 지원되지 않습니다.
다음 단계 학습 방향
- ALCHEMI Toolkit GitHub 저장소를 클론하고 제공된 Jupyter 노트북을 실행해보세요.
- MACE, TensorNet, AIMNet2 등 다양한 MLIP 모델을 자체 데이터에 적용해보며 배치 시뮬레이션 성능을 측정해보는 것을 추천합니다.
- 분자 동역학 결과 분석을 위해
nvalchemi.hooks모듈의 다양한 분석 훅을 활용해보세요.
함께 보면 좋은 글