Coverage for pySDC/tutorial/step_9/C_paradiag_in_pySDC.py: 100%

95 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 12:30 +0000

1# --- 

2# jupyter: 

3# jupytext: 

4# formats: py:percent 

5# kernelspec: 

6# display_name: Python 3 

7# name: python3 

8# --- 

9 

10# %% [markdown] 

11# # Part C: ParaDiag in pySDC 

12# 

13# Here we leave the hand-written linear algebra behind and set ParaDiag up through pySDC's controllers, comparing it 

14# with single-level PFASST in Jacobi mode and with serial time stepping. In PFASST, we use a diagonal preconditioner, 

15# which allows for the same amount of parallelism as ParaDiag. We show iteration counts per step, but both schemes 

16# have further concurrency across the nodes. 

17# 

18# Two examples: a linear advection problem, discretized with finite differences, and the nonlinear van der Pol 

19# oscillator, with `mu` chosen such that the problem is not overly stiff. Neither setup is optimized: with a 

20# different choice of $\alpha$ in ParaDiag, or with inexactness and coarsening in PFASST, both schemes could be 

21# improved significantly. This is not meant to show that one parallelization scheme is better than the other. It 

22# does show that both, without optimization, need fewer iterations per task than serial time stepping. Kindly refrain 

23# from computing parallel efficiency from these numbers, though. ;) 

24# 

25# ## The setups 

26# 

27# ParaDiag needs its own sweeper, `QDiagonalization`, and its own controller, `controller_ParaDiag_nonMPI`. Its 

28# controller parameters set $\alpha$, and whether to average the Jacobian (for the nonlinear problem only, as it 

29# costs communication). 

30 

31# %% 

32import matplotlib.pyplot as plt 

33import numpy as np 

34 

35from pySDC.helpers.stats_helper import get_sorted 

36 

37 

38def get_description(problem='advection', mode='ParaDiag'): 

39 level_params = {} 

40 level_params['dt'] = 0.1 

41 level_params['restol'] = 1e-6 

42 

43 sweeper_params = {} 

44 sweeper_params['quad_type'] = 'RADAU-RIGHT' 

45 sweeper_params['num_nodes'] = 3 

46 sweeper_params['initial_guess'] = 'copy' 

47 

48 if mode == 'ParaDiag': 

49 from pySDC.implementations.sweeper_classes.ParaDiagSweepers import QDiagonalization as sweeper_class 

50 

51 # we only want to use the averaged Jacobian and do only one Newton iteration per ParaDiag iteration! 

52 else: 

53 from pySDC.implementations.sweeper_classes.generic_implicit import generic_implicit as sweeper_class 

54 

55 # need diagonal preconditioner for same concurrency as ParaDiag 

56 sweeper_params['QI'] = 'MIN-SR-S' 

57 

58 if problem == 'advection': 

59 from pySDC.implementations.problem_classes.AdvectionEquation_ND_FD import advectionNd as problem_class 

60 

61 problem_params = {'nvars': 64, 'order': 8, 'c': 1, 'solver_type': 'GMRES', 'lintol': 1e-8} 

62 elif problem == 'vdp': 

63 from pySDC.implementations.problem_classes.Van_der_Pol_implicit import vanderpol as problem_class 

64 

65 # need to not raise an error when Newton has not converged because we do only one iteration 

66 problem_params = {'newton_maxiter': 99, 'crash_at_maxiter': False, 'mu': 1, 'newton_tol': 1e-9} 

67 

68 step_params = {} 

69 step_params['maxiter'] = 99 

70 

71 description = {} 

72 description['problem_class'] = problem_class 

73 description['problem_params'] = problem_params 

74 description['sweeper_class'] = sweeper_class 

75 description['sweeper_params'] = sweeper_params 

76 description['level_params'] = level_params 

77 description['step_params'] = step_params 

78 

79 return description 

80 

81 

82def get_controller_params(problem='advection', mode='ParaDiag'): 

83 from pySDC.implementations.hooks.log_errors import LogGlobalErrorPostRun 

84 from pySDC.implementations.hooks.log_work import LogWork, LogSDCIterations 

85 

86 controller_params = {} 

87 controller_params['logger_level'] = 30 

88 controller_params['hook_class'] = [LogGlobalErrorPostRun, LogWork, LogSDCIterations] 

89 

90 if mode == 'ParaDiag': 

91 controller_params['alpha'] = 1e-4 

92 

93 # For nonlinear problems, we need to communicate the average solution, which allows to compute the average 

94 # Jacobian locally. For linear problems, we do not want the extra communication. 

95 if problem == 'advection': 

96 controller_params['average_jacobian'] = False 

97 elif problem == 'vdp': 

98 controller_params['average_jacobian'] = True 

99 else: 

100 # We do Block-Jacobi multi-step SDC here. It's a bit silly but it's better for comparing "speedup" 

101 controller_params['mssdc_jac'] = True 

102 

103 return controller_params 

104 

105 

106def run_problem( 

107 n_steps=4, 

108 problem='advection', 

109 mode='ParaDiag', 

110): 

111 if mode == 'ParaDiag': 

112 from pySDC.implementations.controller_classes.controller_ParaDiag_nonMPI import ( 

113 controller_ParaDiag_nonMPI as controller_class, 

114 ) 

115 else: 

116 from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI as controller_class 

117 

118 if mode == 'serial': 

119 num_procs = 1 

120 else: 

121 num_procs = n_steps 

122 

123 description = get_description(problem, mode) 

124 controller_params = get_controller_params(problem, mode) 

125 

126 controller = controller_class(num_procs=num_procs, description=description, controller_params=controller_params) 

127 

128 for S in controller.MS: 

129 S.levels[0].prob.init = tuple([*S.levels[0].prob.init[:2]] + [np.dtype('complex128')]) 

130 

131 P = controller.MS[0].levels[0].prob 

132 

133 t0 = 0.0 

134 uinit = P.u_exact(t0) 

135 

136 uend, stats = controller.run(u0=uinit, t0=t0, Tend=n_steps * controller.MS[0].levels[0].dt) 

137 return uend, stats 

138 

139 

140# %% [markdown] 

141# The solution becomes complex, because the diagonalization is: `run_problem` switches the data type of the problems 

142# to complex numbers before the run. 

143# 

144# ## The comparison 

145# 

146# ParaDiag, PFASST in Jacobi mode and serial time stepping, for 16 steps. Besides the iterations, we compare the work 

147# of the inner solvers: GMRES iterations for advection, Jacobian solves for van der Pol. 

148 

149 

150# %% 

151def compare_ParaDiag_and_PFASST(n_steps, problem): 

152 print(f'Running {problem} with {n_steps} steps') 

153 

154 uend_PD, stats_PD = run_problem(n_steps, problem, mode='ParaDiag') 

155 uend_PF, stats_PF = run_problem(n_steps, problem, mode='PFASST') 

156 uend_S, stats_S = run_problem(n_steps, problem, mode='serial') 

157 

158 assert np.allclose(uend_PD, uend_PF) 

159 assert np.allclose(uend_S, uend_PD) 

160 assert ( 

161 abs(uend_PD - uend_PF) > 0 

162 ) # two different iterative methods should not give identical results for non-zero tolerance 

163 

164 k_PD = get_sorted(stats_PD, type='k') 

165 k_PF = get_sorted(stats_PF, type='k') 

166 

167 print( 

168 f'Needed {max(me[1] for me in k_PD)} ParaDiag iterations and {max(me[1] for me in k_PF)} single-level PFASST iterations' 

169 ) 

170 if problem == 'advection': 

171 k_GMRES_PD = get_sorted(stats_PD, type='work_GMRES') 

172 k_GMRES_PF = get_sorted(stats_PF, type='work_GMRES') 

173 k_GMRES_S = get_sorted(stats_S, type='work_GMRES') 

174 print( 

175 f'Maximum GMRES iterations on each step: {max(me[1] for me in k_GMRES_PD)} in ParaDiag, {max(me[1] for me in k_GMRES_PF)} in single-level PFASST and {sum(me[1] for me in k_GMRES_S)} total GMRES iterations in serial' 

176 ) 

177 elif problem == 'vdp': 

178 k_Jac_PD = get_sorted(stats_PD, type='work_jacobian_solves') 

179 k_Jac_PF = get_sorted(stats_PF, type='work_jacobian_solves') 

180 k_Jac_S = get_sorted(stats_S, type='work_jacobian_solves') 

181 print( 

182 f'Maximum Jacobian solves on each step: {max(me[1] for me in k_Jac_PD)} in ParaDiag, {max(me[1] for me in k_Jac_PF)} in single-level PFASST and {sum(me[1] for me in k_Jac_S)} total Jacobian solves in serial' 

183 ) 

184 print() 

185 return max(me[1] for me in k_PD), max(me[1] for me in k_PF) 

186 

187 

188# %% 

189iterations = {problem: compare_ParaDiag_and_PFASST(n_steps=16, problem=problem) for problem in ['advection', 'vdp']} 

190 

191# %% tags=["hide-input"] 

192fig, ax = plt.subplots(figsize=(6, 3)) 

193x = np.arange(len(iterations)) 

194ax.bar(x - 0.2, [k[0] for k in iterations.values()], width=0.4, label='ParaDiag') 

195ax.bar(x + 0.2, [k[1] for k in iterations.values()], width=0.4, label='single-level PFASST') 

196ax.set_xticks(x) 

197ax.set_xticklabels(['advection', 'van der Pol']) 

198ax.set_ylabel('iterations') 

199ax.legend(frameon=False) 

200fig.tight_layout() 

201 

202# %% [markdown] 

203# ParaDiag converges in very few iterations for the hyperbolic advection problem (3), where PFASST struggles (36). 

204# For van der Pol, ParaDiag needs fewer iterations as well (10 against 24), with a much smaller margin. Remember 

205# that ParaDiag does only one Newton iteration per ParaDiag iteration, so per node, its number of Newton iterations 

206# equals the number of ParaDiag iterations, while PFASST solves the systems to some accuracy in every iteration. That 

207# makes the difference in Jacobian solves per step much larger than the one in iterations: 30 against 143. Again, 

208# inexactness could improve PFASST. 

209# 

210# :::{admonition} Important things to note 

211# - ParaDiag needs its own sweeper (`QDiagonalization`) and its own controller. 

212# - The solution becomes complex, because the diagonalization is. 

213# - ParaDiag converges in very few iterations for the hyperbolic advection example, where PFASST struggles. For the 

214# van der Pol oscillator, the gap in iterations is much smaller. 

215# ::: 

216# 

217# The checks the tests run are inside `compare_ParaDiag_and_PFASST`: all three methods agree, and the two iterative 

218# ones are not identical.