@@ -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%。
0 commit comments