Skip to content

Commit 6ce87d8

Browse files
committed
doc: ri_rccsdt
1 parent afb2ad2 commit 6ce87d8

File tree

2 files changed

+154
-3
lines changed

2 files changed

+154
-3
lines changed

docs/src/ri_rccsdt.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,154 @@ fn ccsd_t_energy_contribution(abc: [usize; 3], mol_info: &RCCSDInfo, intermediat
320320
- CPU 性能利用率不低于 21%。
321321

322322
## 5. RI-RCCSD(T) 高性能实现
323+
324+
### 5.1 效率改进方案与实现
325+
326+
上述程序的程序性能主要受制于下面几个因素:
327+
- 低效的张量索引;
328+
- 较为低效的 elementwise 运算;
329+
- 过分频繁的内存分配;
330+
331+
作为解决方案,
332+
333+
A. 整个程序经常处理张量转置;注意到每次循环时,张量的转置顺序都是固定的,那么可以在外循环前先将转置顺序储存为数组,以避免每次循环内再对转置的位置作计算。
334+
335+
```rust
336+
// ri_rccsdt.rs
337+
338+
pub struct TransposedIndices {
339+
pub tr_012: Vec<usize>,
340+
pub tr_021: Vec<usize>,
341+
pub tr_102: Vec<usize>,
342+
pub tr_120: Vec<usize>,
343+
pub tr_201: Vec<usize>,
344+
pub tr_210: Vec<usize>,
345+
}
346+
347+
fn prepare_transposed_indices(nocc: usize) -> TransposedIndices {
348+
let device = DeviceTsr::default();
349+
let base = rt::arange((nocc * nocc * nocc, &device)).into_shape([nocc, nocc, nocc]);
350+
let tr_012 = base.transpose([0, 1, 2]).reshape(-1).to_vec();
351+
let tr_021 = base.transpose([0, 2, 1]).reshape(-1).to_vec();
352+
let tr_102 = base.transpose([1, 0, 2]).reshape(-1).to_vec();
353+
let tr_120 = base.transpose([1, 2, 0]).reshape(-1).to_vec();
354+
let tr_201 = base.transpose([2, 0, 1]).reshape(-1).to_vec();
355+
let tr_210 = base.transpose([2, 1, 0]).reshape(-1).to_vec();
356+
357+
TransposedIndices { tr_012, tr_021, tr_102, tr_120, tr_201, tr_210 }
358+
}
359+
```
360+
361+
B. 为了避免频繁的内存分配,在计算 $W_{ijk}^{[abc]}$ 时,应在调用函数 `get_w` 时重复使用先前的内存 buffer,输出张量应尽量避免新分配内存。下述代码也引入了 `tr_indices`,这就是上面提到的预先存储好的转置顺序。
362+
363+
```rust
364+
// ri_rccsdt.rs
365+
366+
fn get_w(abc: [usize; 3], mut w: TsrMut, mut wbuf: TsrMut, intermediates: &RCCSDTIntermediates, tr_indices: &[usize]) {
367+
let t2_t = intermediates.t2_t.as_ref().unwrap();
368+
let eri_vvov_t = intermediates.eri_vvov_t.as_ref().unwrap();
369+
let eri_vooo_t = intermediates.eri_vooo_t.as_ref().unwrap();
370+
let device = t2_t.device().clone();
371+
let nvir = t2_t.shape()[0];
372+
let nocc = t2_t.shape()[2];
373+
374+
let [a, b, c] = abc;
375+
376+
// hack for TsrMut to reshape to nocc, nocc * nocc
377+
let mut wbuf = rt::asarray((wbuf.raw_mut(), [nocc, nocc * nocc].c(), &device));
378+
wbuf.matmul_from(&eri_vvov_t.i([a, b]), &t2_t.i(c).reshape([nvir, nocc * nocc]), 1.0, 0.0);
379+
wbuf.matmul_from(&t2_t.i([a, b]).t(), &eri_vooo_t.i(c).reshape([nocc, nocc * nocc]), -1.0, 1.0);
380+
381+
// add to w with transposed indices
382+
let w_raw = w.raw_mut();
383+
let wbuf_raw = wbuf.raw();
384+
w_raw.iter_mut().zip(tr_indices).for_each(|(w_elem, &tr_idx)| {
385+
*w_elem += wbuf_raw[tr_idx];
386+
});
387+
}
388+
```
389+
390+
```rust
391+
// ri_rccsdt.rs, in fn `ccsd_t_energy_contribution`
392+
393+
let mut w = rt::zeros(([nocc, nocc, nocc], &device));
394+
let mut wbuf = unsafe { rt::empty(([nocc, nocc, nocc], &device)) };
395+
396+
get_w([a, b, c], w.view_mut(), wbuf.view_mut(), intermediates, &tr_indices.tr_012);
397+
get_w([a, c, b], w.view_mut(), wbuf.view_mut(), intermediates, &tr_indices.tr_021);
398+
get_w([b, c, a], w.view_mut(), wbuf.view_mut(), intermediates, &tr_indices.tr_201);
399+
get_w([b, a, c], w.view_mut(), wbuf.view_mut(), intermediates, &tr_indices.tr_102);
400+
get_w([c, a, b], w.view_mut(), wbuf.view_mut(), intermediates, &tr_indices.tr_120);
401+
get_w([c, b, a], w.view_mut(), wbuf.view_mut(), intermediates, &tr_indices.tr_210);
402+
```
403+
404+
C. 涉及到的 elementwise 运算中,计算 $V_{ijk}^{[abc]}$ 时使用到了 broadcast 数乘;但目前 RSTSR 的 broadcast elementwise 运算的实现效率仅仅满足一般需求,其性能没有达到极限。如果确实是 broadcast elementwise 导致性能低下,建议手动展开 for 循环。
405+
406+
D. 用于计算 $V_{ijk}^{[abc]}$ 的 broadcast elementwise 运算,若手动展开 for 循环,则需要对张量进行索引。为更高效地进行张量索引,一般需要做三件事:
407+
- 在确保索引不会越界的情况下,使用 `index_uncheck` 以避免越界检查 (这是决定性因素);
408+
- 低维度的索引代价总是小于高维度;尽可能先取出子张量,对更低维度的子张量进行索引;
409+
- 若在索引前能确保张量维度的大小,则尽量通过 `into_dim` 转换到静态维度。
410+
411+
E. 如果是多步计算,譬如计算 $Z_{ijk}^{[abc]}$ 时涉及到多个张量的求和,对于这种情形最好使用迭代器以避免多次写入:对于特定的指标 $(i, j, k)$ 多次读入张量并求和、但只写入一次到 $Z_{ijk}^{[abc]}$[^2]
412+
413+
[^2]: 这也是 RSTSR 目前不擅长的部分,即它不支持延迟计算 (lazy evaluation)。不引入延迟计算,既是出于使用者方便的角度,也可以减少 RSTSR 开发者维护的难度。对于 C++ 中支持延迟计算的库,可以参考 Eigen。RSTSR 的张量支持各种迭代器 (依元素迭代或依特定维度迭代),并支持并行迭代。利用这些迭代器,也可以提升计算效率,避免多次写入;当然,这里高性能的 RI-RCCSD(T) 的实现是依预先存储的指针位置进行迭代,这个实现策略的性能更高但技巧性太强。
414+
415+
F. 实际上,当 $W_{ijk}^{[abc]}$ 计算完毕后,后面的计算中,内循环的指标 $(i, j, k)$ 之间其实没有关联。这意味着不需要对张量 $V_{ijk}^{[abc]}$、$Z_{ijk}^{[abc]}$、$\Delta_{ijk}^{[abc]}$ 作新的内存分配或存储;他们的数值随内循环 $(i, j, k)$ 的迭代生成求得能量的贡献 (通过 `fold` 函数实现能量贡献的累加),就可以直接消亡。
416+
417+
```rust
418+
// ri_rccsdt.rs, in fn `ccsd_t_energy_contribution`
419+
420+
let eri_vvoo_t_ab = eri_vvoo_t.i([a, b]).into_dim::<Ix2>();
421+
let eri_vvoo_t_bc = eri_vvoo_t.i([b, c]).into_dim::<Ix2>();
422+
let eri_vvoo_t_ca = eri_vvoo_t.i([c, a]).into_dim::<Ix2>();
423+
let t1_t_a = t1_t.i(a).into_dim::<Ix1>();
424+
let t1_t_b = t1_t.i(b).into_dim::<Ix1>();
425+
let t1_t_c = t1_t.i(c).into_dim::<Ix1>();
426+
let iter_ijk = (0..nocc).cartesian_product(0..nocc).cartesian_product(0..nocc);
427+
let d_abc = -(ev[[a]] + ev[[b]] + ev[[c]]);
428+
let w_raw = w.raw();
429+
430+
let e_sum = izip!(
431+
iter_ijk,
432+
w.raw().iter(),
433+
d_ooo.raw().iter(),
434+
tr_indices.tr_012.iter(),
435+
tr_indices.tr_120.iter(),
436+
tr_indices.tr_201.iter(),
437+
tr_indices.tr_210.iter(),
438+
tr_indices.tr_021.iter(),
439+
tr_indices.tr_102.iter()
440+
)
441+
.fold(0.0, |acc, (((i, j), k), &w_val, &d_ijk, &tr_012, &tr_120, &tr_201, &tr_210, &tr_021, &tr_102)| unsafe {
442+
let v_val = w_val
443+
+ t1_t_a.raw()[i] * eri_vvoo_t_bc.index_uncheck([j, k])
444+
+ t1_t_b.raw()[j] * eri_vvoo_t_ca.index_uncheck([k, i])
445+
+ t1_t_c.raw()[k] * eri_vvoo_t_ab.index_uncheck([i, j]);
446+
let z_val =
447+
4.0 * w_raw[tr_012] + w_raw[tr_120] + w_raw[tr_201] - 2.0 * (w_raw[tr_210] + w_raw[tr_021] + w_raw[tr_102]);
448+
let d_val = d_abc + d_ijk;
449+
acc + z_val * v_val / d_val
450+
});
451+
452+
let fac = if a == c {
453+
1.0 / 3.0
454+
} else if a == b || b == c {
455+
1.0
456+
} else {
457+
2.0
458+
};
459+
460+
fac * e_sum
461+
```
462+
463+
### 5.2 性能评价
464+
465+
| 计算表达式 | FLOPs 解析式 | FLOPs 实际数值 |
466+
|--|--|--:|
467+
| $W'{}_{ijk}^{abc} \leftarrow \sum_{d} g_{abid} t_{cdjk}$ | $2 n_\mathrm{occ}^3 n_\mathrm{vir}^4$ | 296 TFLOPs |
468+
| $W'{}_{ijk}^{abc} \leftarrow - \sum_{l} t_{abli} g_{cljk}$ | $2 n_\mathrm{occ}^4 n_\mathrm{vir}^3$ | 78 TFLOPs |
469+
| 总计 | | 374 TFLOPs |
470+
471+
- 这里我们没有统计其他 $O(N^6)$ 的计算步骤;
472+
- 计算耗时约 1003 sec,实际运行效率不低于 0.37 TFLOP/sec;
473+
- CPU 性能利用率不低于 34%。

src/ri_rccsdt.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ fn ccsd_t_energy_contribution(
7676
let eri_vvoo_t_ab = eri_vvoo_t.i([a, b]).into_dim::<Ix2>();
7777
let eri_vvoo_t_bc = eri_vvoo_t.i([b, c]).into_dim::<Ix2>();
7878
let eri_vvoo_t_ca = eri_vvoo_t.i([c, a]).into_dim::<Ix2>();
79-
let t1_t_a = t1_t.i(a).into_dim::<Ix1>().to_owned();
80-
let t1_t_b = t1_t.i(b).into_dim::<Ix1>().to_owned();
81-
let t1_t_c = t1_t.i(c).into_dim::<Ix1>().to_owned();
79+
let t1_t_a = t1_t.i(a).into_dim::<Ix1>();
80+
let t1_t_b = t1_t.i(b).into_dim::<Ix1>();
81+
let t1_t_c = t1_t.i(c).into_dim::<Ix1>();
8282
let iter_ijk = (0..nocc).cartesian_product(0..nocc).cartesian_product(0..nocc);
8383
let d_abc = -(ev[[a]] + ev[[b]] + ev[[c]]);
8484
let w_raw = w.raw();

0 commit comments

Comments
 (0)