Skip to content

Commit 461645c

Browse files
committed
Add quadratic gradient-label training for ABACUS
1 parent e3f385c commit 461645c

30 files changed

Lines changed: 1198 additions & 86 deletions

File tree

deepks/config/defaults.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ def get_default_device():
319319
"cal_stress": 0, # Compute stress flag.
320320
"deepks_bandgap": 0, # DeePKS bandgap output flag.
321321
"deepks_v_delta": 0, # DeePKS v_delta output flag.
322+
"deepks_grad": 0, # DeePKS grad output flag.
322323
"deepks_out_labels": 1, # DeePKS label output flag.
323324
"deepks_scf": 0, # DeePKS in-SCF switch.
324325
"out_wfc_lcao": 0, # Output LCAO wfc flag.
@@ -367,7 +368,10 @@ def get_default_device():
367368
"objective": {
368369
"losses": [], # Structured loss definitions.
369370
"energy_per_atom": None, # Energy-per-atom option.
370-
"grad_penalty": None, # Gradient penalty setting.
371+
"force_grad_penalty": None, # Force-gradient penalty (requires grad_veg/eg0 data).
372+
"hessian_penalty": None, # Network Hessian curvature penalty weight.
373+
"hessian_penalty_method": None, # "exact" (default) or "hutchinson".
374+
"hessian_n_probes": None, # Hutchinson probe count (default 1).
371375
"vd_divide_by_nlocal": False, # Normalize vd by nlocal.
372376
"vd_masked_loss": 0, # Masked vd loss mode.
373377
"vd_masked_S_threshold": 1e-6, # S threshold for masked vd loss.
@@ -385,6 +389,7 @@ def get_default_device():
385389
"display_detail_test": 0, # Detail level for test output.
386390
"display_natom_loss": False, # Print natom-wise loss.
387391
"fix_embedding": False, # Freeze embedding layers.
392+
"trainable_patterns": None, # Optional shell patterns selecting trainable parameters.
388393
"stage_schedule": [], # In-run staged training schedule.
389394
"optimizer": {
390395
"lr": 0.01, # Initial learning rate.
@@ -469,6 +474,7 @@ def get_default_backend_input(backend_name):
469474
"cal_stress",
470475
"deepks_bandgap",
471476
"deepks_v_delta",
477+
"deepks_grad",
472478
"deepks_out_labels",
473479
"deepks_scf",
474480
"out_wfc_lcao",

deepks/config/docs.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@ def _fmt_default(value):
202202
"default": "optional",
203203
"description": "Optimizer and scheduler settings. The current recipes use optimizer.lr, optimizer.weight_decay, scheduler.decay_steps, scheduler.decay_rate, and scheduler.stop_lr.",
204204
},
205+
{
206+
"name": "ml.train.trainable_patterns",
207+
"type": "list[str] | str | null",
208+
"tasks": "train, iterate",
209+
"default": "null",
210+
"description": "Optional shell-style model-parameter name patterns for layer-wise continuation, for example densenet.layers.3.* for a final-head-only stage.",
211+
},
205212
{
206213
"name": "ml.fit_elem",
207214
"type": "bool",
@@ -311,6 +318,7 @@ def _fmt_default(value):
311318
"cal_stress": "Whether to compute stress.",
312319
"deepks_bandgap": "ABACUS DeePKS bandgap flag.",
313320
"deepks_v_delta": "ABACUS DeePKS v_delta flag.",
321+
"deepks_grad": "ABACUS DeePKS grad output flag.",
314322
"deepks_out_labels": "ABACUS DeePKS label dump flag.",
315323
"deepks_scf": "ABACUS DeePKS SCF switch.",
316324
"out_wfc_lcao": "ABACUS out_wfc_lcao switch.",
@@ -356,6 +364,64 @@ def render_input_parameter_doc():
356364
f"| `{row['name']}` | `{row['type']}` | `{row['tasks']}` | `{row['default']}` | {description} |"
357365
)
358366

367+
lines.extend(
368+
[
369+
"",
370+
"## Descriptor-gradient supervision",
371+
"",
372+
"`physics.backend.input.gradient_label` combines current-run property operators",
373+
"as `M=sum_p c_p A_p^T A_p` and `b=sum_p c_p A_p^T Delta X_p`. The coefficients",
374+
"are effective coefficients: DeePKS-L performs no hidden unit conversion or",
375+
"loss-size normalization.",
376+
"",
377+
"| Parameter | Type | Default | Description |",
378+
"| --- | --- | --- | --- |",
379+
"| `force` | `float >= 0` | `0` | Effective force coefficient `c_F`; zero disables force. |",
380+
"| `stress` | `float >= 0` | `0` | Effective stress coefficient `c_S`; nonzero stress automatically adds six local cell-strain coordinates. |",
381+
"| `hr` | `float >= 0` | `1` | Effective real-space Hamiltonian coefficient `c_H`; zero disables HR. |",
382+
"| `ridge` | `float >= 0` | `1e-8` | Tikhonov ridge used only when solving the direct target `g*=(M+ridge I)^-1 b`. It does not alter quadratic `M,b`. |",
383+
"| `fallback` | `lstsq or pinv` | `lstsq` | Singular-solve fallback used only for the direct target. |",
384+
"| `eigen_filter` | `dict or null` | `null` | Optional direct-target filter. Specify exactly one of `{rcond: value}` or `{min_eig: value}`. It does not alter quadratic `M,b`. |",
385+
"",
386+
"All ABACUS DeePKS energy, force, stress, Hamiltonian, and projected-Hamiltonian",
387+
"labels are required to be in Hartree. Legacy Ry real-space Hamiltonian data",
388+
"must be regenerated or converted once before use.",
389+
"",
390+
"To reproduce direct-property reductions, include them in the coefficients.",
391+
"For a two-atom Si frame with direct force weight 1, stress weight 1, HR weight",
392+
"0.005, six force/stress components, HR range 9, and `nlocal=26`, use:",
393+
"",
394+
"```yaml",
395+
"gradient_label:",
396+
" force: 0.16666666666666666 # 1/6",
397+
" stress: 0.16666666666666666 # 1/6",
398+
" hr: 2.136752136752137e-5 # 0.005/(9*26)",
399+
"```",
400+
"",
401+
"The `g_label` objective accepts exactly two loss types:",
402+
"",
403+
"```yaml",
404+
"- name: g_label",
405+
" weight: 1.0",
406+
" loss:",
407+
" type: direct # MSE(g, g*)",
408+
"```",
409+
"",
410+
"or",
411+
"",
412+
"```yaml",
413+
"- name: g_label",
414+
" weight: 1.0",
415+
" loss:",
416+
" type: quadratic # mean_frame(g^T M g - 2 g^T b)",
417+
"```",
418+
"",
419+
"The quadratic form is inverse-free and is the recommended property-equivalent",
420+
"path. Stress-coordinate augmentation is detected from the saved label width;",
421+
"there is no separate loss-side switch.",
422+
]
423+
)
424+
359425
lines.extend(
360426
[
361427
"",

deepks/config/packager.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
_OBJECTIVE_SHARED_KEYS = {
1919
"losses",
2020
"energy_per_atom",
21-
"grad_penalty",
21+
"force_grad_penalty",
22+
"hessian_penalty",
23+
"hessian_penalty_method",
24+
"hessian_n_probes",
2225
"vd_divide_by_nlocal",
2326
"vd_masked_loss",
2427
"vd_masked_S_threshold",
@@ -395,17 +398,8 @@ def pack_child(child_type, child_config):
395398
resolved_terms = _resolve_hierarchical_terms(ml)
396399
if resolved_terms:
397400
main_train.setdefault("ml", {}).setdefault("objective", {})["terms"] = resolved_terms
398-
elif use_profile_scf:
399-
main_train["data"]["train"] = [
400-
f"../00.scf/level.{i:02d}/data_train/*" for i in range(len(scf_profiles))
401-
]
402401
if data.get("test") is not None:
403-
if use_profile_scf:
404-
main_train["data"]["test"] = [
405-
f"../00.scf/level.{i:02d}/data_test/*" for i in range(len(scf_profiles))
406-
]
407-
else:
408-
main_train["data"]["test"] = "data_test/*"
402+
main_train["data"]["test"] = "data_test/*"
409403
if isinstance(runtime.get("io"), dict):
410404
main_train["runtime"]["io"] = deepcopy(runtime["io"])
411405
if child_proj_basis:

deepks/interface/objectives/config.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55

66
_OBJECTIVE_OPTION_KEYS = (
7-
"grad_penalty",
7+
"force_grad_penalty",
8+
"hessian_penalty",
9+
"hessian_penalty_method",
10+
"hessian_n_probes",
811
"energy_per_atom",
912
"vd_divide_by_nlocal",
1013
"vd_masked_loss",
@@ -28,6 +31,7 @@
2831
"density_m": ("density_m_factor", "density_m_lossfn", "density_m_occ"),
2932
"phi_align": ("phi_align_factor", "phi_align_lossfn", "phi_align_occ"),
3033
"density": ("density_factor", None, None),
34+
"g_label": ("grad_factor", "grad_lossfn", None),
3135
}
3236

3337
_FACTOR_KEYS = tuple(mapped[0] for mapped in _LOSS_NAME_MAP.values())
@@ -92,7 +96,8 @@ def build_descriptor_property_eval_args(objective_args, *, detailed=False):
9296
"energy_factor": 1.0,
9397
"force_factor": 0.0,
9498
"density_factor": 0.0,
95-
"grad_penalty": 0.0,
99+
"force_grad_penalty": 0.0,
100+
"hessian_penalty": 0.0,
96101
"energy_per_atom": energy_per_atom,
97102
}
98103

deepks/interface/objectives/descriptor_properties.py

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ def __init__(
3131
phi_align_factor=0.0,
3232
phi_align_occ=0,
3333
density_factor=0.0,
34-
grad_penalty=0.0,
34+
force_grad_penalty=0.0,
35+
hessian_penalty=0.0,
36+
grad_factor=0.0,
37+
hessian_penalty_method="exact",
38+
hessian_n_probes=1,
3539
energy_lossfn=None,
3640
force_lossfn=None,
3741
stress_lossfn=None,
@@ -43,6 +47,7 @@ def __init__(
4347
band_lossfn=None,
4448
bandgap_lossfn=None,
4549
density_m_lossfn=None,
50+
grad_lossfn=None,
4651
energy_per_atom=0,
4752
vd_divide_by_nlocal=False,
4853
vd_masked_loss=0,
@@ -98,7 +103,11 @@ def __init__(
98103
self.density_m_factor = density_m_factor
99104
self.phi_align_factor = phi_align_factor
100105
self.d_factor = density_factor
101-
self.g_penalty = grad_penalty
106+
self.fg_penalty = force_grad_penalty
107+
self.h_penalty = hessian_penalty
108+
self.grad_factor = grad_factor
109+
self.hessian_method = hessian_penalty_method
110+
self.hessian_n_probes = hessian_n_probes
102111
self.energy_per_atom = energy_per_atom
103112
self.vd_divide_by_nlocal = vd_divide_by_nlocal
104113
self.vd_masked_loss = vd_masked_loss
@@ -137,7 +146,11 @@ def __init__(
137146
"phi_align_factor": phi_align_factor,
138147
"phi_align_occ": phi_align_occ,
139148
"density_factor": density_factor,
140-
"grad_penalty": grad_penalty,
149+
"force_grad_penalty": force_grad_penalty,
150+
"hessian_penalty": hessian_penalty,
151+
"grad_factor": grad_factor,
152+
"hessian_penalty_method": hessian_penalty_method,
153+
"hessian_n_probes": hessian_n_probes,
141154
"energy_lossfn": energy_lossfn,
142155
"force_lossfn": force_lossfn,
143156
"stress_lossfn": stress_lossfn,
@@ -149,6 +162,7 @@ def __init__(
149162
"band_lossfn": band_lossfn,
150163
"bandgap_lossfn": bandgap_lossfn,
151164
"density_m_lossfn": density_m_lossfn,
165+
"grad_lossfn": grad_lossfn,
152166
"energy_per_atom": self.energy_per_atom,
153167
"vd_divide_by_nlocal": vd_divide_by_nlocal,
154168
"vd_masked_loss": vd_masked_loss,
@@ -179,6 +193,18 @@ def compute_losses(self, model, batch):
179193
if dropped:
180194
self._warn_dropped_properties(dropped)
181195
derivative_spec = self.property_engine.required_model_derivatives(requested_properties)
196+
# Adapter-level uses of input_grad (grad_factor, fg_penalty, h_penalty,
197+
# d_factor) bypass the property engine, so they don't appear in
198+
# requested_properties and derivative_spec["input"] stays False even
199+
# when they are active. Force it True so model_input.requires_grad_
200+
# is set and _compute_input_grad actually runs.
201+
_adapter_needs_input_grad = (
202+
self.grad_factor > 0 or self.fg_penalty > 0
203+
or self.h_penalty > 0 or self.d_factor > 0
204+
)
205+
if _adapter_needs_input_grad and not derivative_spec.get("input"):
206+
derivative_spec = dict(derivative_spec)
207+
derivative_spec["input"] = True
182208

183209
# R1 + R2: call the model in dict-in / dict-out form, then apply
184210
# interface-side reducers to obtain the supervision-ready primary
@@ -203,9 +229,15 @@ def compute_losses(self, model, batch):
203229
model_derivatives=model_derivatives,
204230
context=calc_context,
205231
)
206-
if self.g_penalty > 0 and input_grad is not None and "eg0" in batch.context:
232+
if self.fg_penalty > 0 and input_grad is not None and "eg0" in batch.context:
207233
eg_base, gveg = batch.context["eg0"], batch.context["gveg"]
208234
predictions["grad_total"] = torch.einsum("...apg,...ap->...g", gveg, input_grad) + eg_base
235+
if self.h_penalty > 0 and input_grad is not None:
236+
predictions["hessian_penalty"] = self._compute_hessian_penalty(
237+
input_grad, model_input, self.hessian_method, self.hessian_n_probes
238+
)
239+
if self.grad_factor > 0 and input_grad is not None:
240+
predictions["input_grad"] = input_grad
209241
if self.d_factor > 0 and input_grad is not None and "gldv" in batch.context:
210242
predictions["density_regularizer"] = (batch.context["gldv"] * input_grad).mean(0).sum()
211243

@@ -308,6 +340,60 @@ def _compute_input_grad(primary_scalar, model_input, needs_grad):
308340
)
309341
return grad
310342

343+
@staticmethod
344+
def _compute_hessian_penalty(input_grad, model_input, method, n_probes):
345+
"""Compute ||d2E/dlambda2||_F^2 for the Hessian curvature penalty.
346+
347+
Two methods:
348+
"exact" -- row-by-row Jacobian of input_grad; O(ndesc_per_atom) backward
349+
passes. Exploits per-atom independence: iterates over descriptor
350+
dims only (not natom*ndesc). Default.
351+
"hutchinson" -- stochastic Frobenius-norm estimate via n_probes Rademacher
352+
vectors; O(n_probes) backward passes. Cheaper for large systems.
353+
"""
354+
if method == "hutchinson":
355+
return DescriptorPropertyObjectiveAdapter._hutchinson_hessian(
356+
input_grad, model_input, n_probes
357+
)
358+
return DescriptorPropertyObjectiveAdapter._exact_hessian(input_grad, model_input)
359+
360+
@staticmethod
361+
def _exact_hessian(input_grad, model_input):
362+
# input_grad: (batch, natom, ndesc_per_atom) -- already create_graph=True
363+
# Exploit per-atom independence: iterate over ndesc_per_atom only.
364+
ndesc = input_grad.shape[-1]
365+
frob_sq = input_grad.new_tensor(0.0)
366+
for d in range(ndesc):
367+
row = torch.autograd.grad(
368+
input_grad[..., d].sum(), # scalar -- sum over batch and atoms
369+
model_input,
370+
retain_graph=(d < ndesc - 1),
371+
create_graph=False,
372+
only_inputs=True,
373+
)[0] # (batch, natom, ndesc_per_atom) -- d-th Hessian row per atom
374+
frob_sq = frob_sq + row.pow(2).mean() # mean over batch*atoms, sum over row
375+
# Both methods compute ||H||_F^2 / (batch*natom*ndesc_per_atom).
376+
return frob_sq
377+
378+
@staticmethod
379+
def _hutchinson_hessian(input_grad, model_input, n_probes):
380+
# Unbiased estimator: E[||Hv||^2] = ||H||_F^2 for Rademacher v.
381+
penalty = input_grad.new_tensor(0.0)
382+
for _ in range(n_probes):
383+
# Rademacher vector (+-1 with equal probability)
384+
v = torch.randint(0, 2, input_grad.shape, dtype=input_grad.dtype,
385+
device=input_grad.device) * 2 - 1
386+
Hv = torch.autograd.grad(
387+
(input_grad * v).sum(),
388+
model_input,
389+
retain_graph=True,
390+
create_graph=False,
391+
only_inputs=True,
392+
)[0]
393+
penalty = penalty + Hv.pow(2).mean()
394+
return penalty / n_probes
395+
396+
311397
def _requested_properties(self):
312398
requested = set()
313399
if self.primary_property:
@@ -338,8 +424,10 @@ def _warn_dropped_properties(self, dropped_properties):
338424
def print_head(self, name, data_keys, align_len=20):
339425
data_keys = self._normalize_field_keys(data_keys)
340426
info = f"{name}_energy".rjust(align_len)
341-
if self.g_penalty > 0 and "eg0" in data_keys:
342-
info += f"{name}_grad".rjust(align_len)
427+
if self.fg_penalty > 0 and "eg0" in data_keys:
428+
info += f"{name}_force_grad".rjust(align_len)
429+
if self.h_penalty > 0:
430+
info += f"{name}_hessian".rjust(align_len)
343431
if self.f_factor > 0 and "force" in data_keys:
344432
info += f"{name}_force".rjust(align_len)
345433
if self.s_factor > 0 and "stress" in data_keys:
@@ -362,6 +450,12 @@ def print_head(self, name, data_keys, align_len=20):
362450
info += f"{name}_phi_align".rjust(align_len)
363451
if self.d_factor > 0 and "gldv" in data_keys:
364452
info += f"{name}_density".rjust(align_len)
453+
has_gradient_supervision = (
454+
"g_label" in data_keys
455+
or {"g_label_metric", "g_label_projection"}.issubset(data_keys)
456+
)
457+
if self.grad_factor > 0 and has_gradient_supervision:
458+
info += f"{name}_grad".rjust(align_len)
365459
print(info, end="")
366460

367461
@staticmethod
@@ -375,6 +469,7 @@ def _normalize_field_keys(data_keys):
375469
"lb_vd": "v_delta",
376470
"lb_vdr": "vdr",
377471
"lb_phi": "phi",
472+
"lb_g": "g_label",
378473
"lb_band": "band",
379474
}
380475
return {aliases.get(key, key) for key in data_keys}

0 commit comments

Comments
 (0)