Coverage for python/lsst/ctrl/bps/parsl/sites/ccin2p3.py: 77%

60 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-17 09:09 +0000

1import copy 

2import platform 

3from typing import TYPE_CHECKING, Any 

4 

5import parsl.config 

6from parsl.dataflow.memoization import BasicMemoizer 

7from parsl.executors import HighThroughputExecutor 

8from parsl.executors.base import ParslExecutor 

9from parsl.providers import SlurmProvider 

10 

11from ..configuration import get_bps_config_value 

12from ..site import SiteConfig 

13 

14if TYPE_CHECKING: 

15 from ..job import ParslJob 

16 

17__all__ = ("Ccin2p3",) 

18 

19Kwargs = dict[str, Any] 

20 

21 

22class Ccin2p3(SiteConfig): 

23 """Configuration for executing Parsl jobs in CC-IN2P3 Slurm batch farm. 

24 

25 This class provides four job slot sizes each with its specific 

26 requirements, in particular in terms of memory. Those slot sizes are named 

27 "small", "medium", "large" and "xlarge". 

28 

29 Sensible default values for those requirements are provided for each 

30 job slot but you can overwrite those defaults either in the 

31 the BPS submission file or in a site configuration file that you 

32 include in your BPS submission file. 

33 

34 If you don't need to modify the default requirements for the job slot 

35 sizes, use the site specification below in your BPS configuration 

36 file: 

37 

38 .. code-block:: yaml 

39 

40 wmsServiceClass: lsst.ctrl.bps.parsl.ParslService 

41 computeSite: ccin2p3 

42 

43 site: 

44 ccin2p3: 

45 class: lsst.ctrl.bps.parsl.sites.ccin2p3.Ccin2p3 

46 

47 If you do need to modify those defaults, you can overwrite them for 

48 all job slots or for specific each job slots. Requirements specified 

49 for a job slot take priority over those specified for all job slots 

50 at the level of entry ``.site.ccin2p3:``. 

51 

52 This is an example of how to overwrite selected requirements in your BPS 

53 submission file: 

54 

55 .. code-block:: yaml 

56 

57 wmsServiceClass: lsst.ctrl.bps.parsl.ParslService 

58 computeSite: ccin2p3 

59 

60 site: 

61 ccin2p3: 

62 class: lsst.ctrl.bps.parsl.sites.ccin2p3.Ccin2p3 

63 walltime: "72:00:00" 

64 scheduler_options: 

65 - "--licenses=sps" 

66 - "--qos=normal" 

67 small: 

68 memory: 6 

69 partition: "flash" 

70 medium: 

71 memory: 10 

72 partition: "lsst,htc" 

73 large: 

74 memory: 80 

75 xlarge: 

76 memory: 180 

77 partition: "lsst" 

78 scheduler_options: 

79 - "--constraint=el7" 

80 - "--licenses=my_product" 

81 - "--reservation=my_reservation" 

82 

83 At the level of entry ``site.ccin2p3:`` in the BPS submission file, the 

84 following configuration parameters are accepted, which apply to all slot 

85 sizes: 

86 

87 - ``partition`` (`str`): name of the one or more configured partitions. If 

88 more than one, separate them with comma (','). 

89 (Default: "lsst,htc") 

90 - ``walltime`` (`str`): walltime to require for the job 

91 (Default: "72:00:00") 

92 - ``scheduler_options`` (`list` [`str`] ): scheduler options to send to 

93 Slurm for scheduling purposes. 

94 (Default: "--licenses=sps") 

95 

96 In addition, as shown in the previous example, for each job slot (i.e. 

97 "small", "medium", etc.) you can specify the requirements above as well as 

98 the following: 

99 

100 - ``max_blocks`` (`int`): maximum number of Slurm jobs that your workflow 

101 can simultaneously use. 

102 - ``memory`` (`int`): required amount of memory for each job, in Gigabytes. 

103 (Defaults: 4 for "small", 10 for "medium", 50 for "large" and 

104 150 for "xlarge"). 

105 

106 Parameters 

107 ---------- 

108 *args 

109 Arguments to initialize the super-class. 

110 **kwargs 

111 Keyword arguments to initialize the super-class. 

112 

113 Returns 

114 ------- 

115 Ccin2p3 : `lsst.ctrl.bps.parsl.SiteConfig` 

116 Concrete instance of a `~lsst.ctrl.bps.parsl.SiteConfig` specific for 

117 the CC-IN2P3 Slurm farm. 

118 """ 

119 

120 DEFAULT_ACCOUNT: str = "lsst" 

121 DEFAULT_WALLTIME: str = "72:00:00" 

122 DEFAULT_SCHEDULER_OPTIONS: list[str] = [ 

123 "--licenses=sps", 

124 ] 

125 

126 def __init__(self, *args, **kwargs): 

127 # Have BPS-defined resource requests for each job passed to executor. 

128 kwargs["resource_list"] = ["priority"] 

129 super().__init__(*args, **kwargs) 

130 self._account = get_bps_config_value(self.site, ".account", str, self.DEFAULT_ACCOUNT) 

131 self._scheduler_options = get_bps_config_value( 

132 self.site, ".scheduler_options", list, self.DEFAULT_SCHEDULER_OPTIONS 

133 ) 

134 self._slot_size = { 

135 "small": { 

136 "memory": get_bps_config_value(self.site, ".small.memory", int, 4), 

137 "walltime": self._get_walltime_for_slot("small"), 

138 "partition": self._get_partition_for_slot("small"), 

139 "max_blocks": get_bps_config_value(self.site, ".small.max_blocks", int, 3_000), 

140 "scheduler_options": get_bps_config_value(self.site, ".small.scheduler_options", list, []), 

141 }, 

142 "medium": { 

143 "memory": get_bps_config_value(self.site, ".medium.memory", int, 10), 

144 "walltime": self._get_walltime_for_slot("medium"), 

145 "partition": self._get_partition_for_slot("medium"), 

146 "max_blocks": get_bps_config_value(self.site, ".medium.max_blocks", int, 1_000), 

147 "scheduler_options": get_bps_config_value(self.site, ".medium.scheduler_options", list, []), 

148 }, 

149 "large": { 

150 "memory": get_bps_config_value(self.site, ".large.memory", int, 50), 

151 "walltime": self._get_walltime_for_slot("large"), 

152 "partition": self._get_partition_for_slot("large"), 

153 "max_blocks": get_bps_config_value(self.site, ".large.max_blocks", int, 100), 

154 "scheduler_options": get_bps_config_value(self.site, ".large.scheduler_options", list, []), 

155 }, 

156 "xlarge": { 

157 "memory": get_bps_config_value(self.site, ".xlarge.memory", int, 150), 

158 "walltime": self._get_walltime_for_slot("xlarge"), 

159 "partition": self._get_partition_for_slot("xlarge"), 

160 "max_blocks": get_bps_config_value(self.site, ".xlarge.max_blocks", int, 10), 

161 "scheduler_options": get_bps_config_value(self.site, ".xlarge.scheduler_options", list, []), 

162 }, 

163 } 

164 

165 def _get_partition_for_slot(self, slot: str) -> str: 

166 """Return the Slurm partition Parsl must use to submit jobs for the 

167 job slot `slot`. Values of `slot` can be "small", "medium", "large" 

168 or "xlarge". 

169 """ 

170 # The target Slurm partition must be selected according to the type of 

171 # the job slot but also according to the CPU architecture of the 

172 # compute node. 

173 # 

174 # Parsl requires that the CPU architecture of its orchestrator to 

175 # be identical to the architecture of its executors. Therefore, 

176 # we need to ensure that Slurm schedules our Parsl executors on 

177 # compute nodes with the same architecture as the host where this 

178 # orchestrator runs. 

179 

180 # Default target Slurm partitions per CPU architecture 

181 default_partition = { 

182 "aarch64": { 

183 "small": "htc_arm", 

184 "medium": "htc_arm", 

185 "large": "htc_arm", 

186 "xlarge": "htc_arm", 

187 }, 

188 "x86_64": { 

189 "small": "lsst,htc", 

190 "medium": "lsst", 

191 "large": "lsst", 

192 "xlarge": "lsst", 

193 }, 

194 } 

195 architecture = platform.machine() 

196 if architecture not in default_partition: 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true

197 raise ValueError(f"architecture {architecture} is not supported") 

198 

199 # If a partition was specified in the workflow description file 

200 # specifically for this job slot, use that partition. For instance: 

201 # 

202 # site: 

203 # ccin2p3: 

204 # class: lsst.ctrl.bps.parsl.sites.ccin2p3.Ccin2p3 

205 # small: 

206 # partition: htc 

207 slot_partition = get_bps_config_value(self.site, f".{slot}.partition", str, "") 

208 if slot_partition != "": 208 ↛ 209line 208 didn't jump to line 209 because the condition on line 208 was never true

209 return slot_partition 

210 

211 # If a partition was specified in the workflow description file at 

212 # the site level, use that partition. For instance: 

213 # 

214 # site: 

215 # ccin2p3: 

216 # class: lsst.ctrl.bps.parsl.sites.ccin2p3.Ccin2p3 

217 # partition: htc 

218 # 

219 # Otherwise, use the default for this slot on this architecture. 

220 return get_bps_config_value(self.site, ".partition", str, default_partition[architecture][slot]) 

221 

222 def _get_walltime_for_slot(self, slot: str) -> str: 

223 """Return the value for walltime Parsl must use to submit jobs for the 

224 job slot `slot`. Values of `slot` can be "small", "medium", "large" 

225 or "xlarge". 

226 """ 

227 # If a specific walltime value was specified for this job slot in the 

228 # configuration use that value. For instance: 

229 # 

230 # site: 

231 # ccin2p3: 

232 # class: lsst.ctrl.bps.parsl.sites.ccin2p3.Ccin2p3 

233 # small: 

234 # walltime: "3:00:00" 

235 slot_walltime = get_bps_config_value(self.site, f".{slot}.walltime", str, "") 

236 if slot_walltime != "": 236 ↛ 237line 236 didn't jump to line 237 because the condition on line 236 was never true

237 return slot_walltime 

238 

239 # If a walltime value was specified for the site use that value. 

240 # Otherwise, use the default walltime. For instance: 

241 # 

242 # site: 

243 # ccin2p3: 

244 # class: lsst.ctrl.bps.parsl.sites.ccin2p3.Ccin2p3 

245 # walltime: "3:00:00" 

246 return get_bps_config_value(self.site, ".walltime", str, self.DEFAULT_WALLTIME) 

247 

248 def get_executors(self) -> list[ParslExecutor]: 

249 """Get a list of Parsl executors that can be used for processing a 

250 workflow. 

251 

252 Each executor must have a unique ``label``. 

253 """ 

254 executors: list[ParslExecutor] = [] 

255 for label, slot in self._slot_size.items(): 

256 # Compute the scheduler options for this job slot. Options 

257 # specified at the slot level in the configuration file 

258 # overwrite those specified at the site level. 

259 scheduler_options = copy.deepcopy(self._scheduler_options) 

260 if slot_scheduler_options := slot.get("scheduler_options", []): 260 ↛ 261line 260 didn't jump to line 261 because the condition on line 260 was never true

261 scheduler_options = copy.deepcopy(slot_scheduler_options) 

262 

263 options = f"#SBATCH {' '.join(opt for opt in scheduler_options)}" if scheduler_options else "" 

264 

265 executor = HighThroughputExecutor( 

266 label, 

267 provider=SlurmProvider( 

268 # Slurm partition to request blocks from. 

269 partition=slot["partition"], 

270 # Slurm account to which to charge resources used by the 

271 # job. 

272 account=self._account, 

273 # Nodes to provision per block (1 block = 1 CPU core). 

274 nodes_per_block=1, 

275 # Number of CPU cores to provision per node. 

276 cores_per_node=1, 

277 # Memory per node (GB) for each Slurm job. 

278 mem_per_node=slot["memory"], 

279 # Initial number of blocks. 

280 init_blocks=0, 

281 # Minimum number of blocks to maintain. 

282 min_blocks=0, 

283 # Maximum number of blocks to maintain. 

284 max_blocks=slot["max_blocks"], 

285 # Time limit for each Slurm job. 

286 walltime=slot["walltime"], 

287 # '#SBATCH' directives to prepend to the Slurm submission 

288 # script. 

289 scheduler_options=options, 

290 # Set the number of file descriptors and processes to 

291 # the maximum allowed. 

292 worker_init="ulimit -n hard && ulimit -u hard", 

293 # Requests nodes which are not shared with other running 

294 # jobs. 

295 exclusive=False, 

296 ), 

297 # Address to connect to the main Parsl process. 

298 address=self.get_address(), 

299 # GB of memory required per worker. If specified the node 

300 # manager will check the available memory at startup and limit 

301 # the number of workers such that the there’s sufficient memory 

302 # for each worker. 

303 mem_per_worker=None, 

304 # Caps the number of workers launched per node. 

305 max_workers_per_node=1, 

306 # Timeout period (in milliseconds) to be used by the 

307 # executor components. 

308 poll_period=1_000, 

309 # Retry submitting to Slurm in case of submission error. 

310 block_error_handler=False, 

311 ) 

312 executors.append(executor) 

313 

314 return executors 

315 

316 def select_executor(self, job: "ParslJob") -> str: 

317 """Get the ``label`` of the executor to use to execute ``job``. 

318 

319 Parameters 

320 ---------- 

321 job : `lsst.ctrl.bps.parsl.ParslJob` 

322 Job to be executed. 

323 

324 Returns 

325 ------- 

326 label : `str` 

327 Label of executor to use to execute ``job``. 

328 """ 

329 # We choose the executor to use based only on the memory required 

330 # by the job. 

331 memory = job.generic.request_memory / 1024 # Convert to GB 

332 for label in ("small", "medium", "large"): 

333 if memory <= self._slot_size[label]["memory"]: 

334 return label 

335 

336 return "xlarge" 

337 

338 def get_parsl_config(self) -> parsl.config.Config: 

339 """Get Parsl configuration for using CC-IN2P3 Slurm farm as a 

340 Parsl execution site. 

341 

342 Returns 

343 ------- 

344 config : `parsl.config.Config` 

345 The configuration to be used to initialize Parsl for this site. 

346 """ 

347 executors = self.get_executors() 

348 monitor = self.get_monitor() 

349 

350 # Number of retries in case of job failure. 

351 retries = get_bps_config_value(self.site, ".retries", int, 1) 

352 

353 # Path to run directory. 

354 run_dir = get_bps_config_value(self.site, ".run_dir", str, "parsl_runinfo") 

355 memoizer = BasicMemoizer(checkpoint_mode="task_exit") 

356 

357 # Strategy for scaling blocks according to workflow needs. 

358 # Use a strategy that allows for scaling up and down Parsl workers. 

359 strategy = get_bps_config_value(self.site, ".strategy", str, "htex_auto_scale") 

360 

361 return parsl.config.Config( 

362 executors=executors, 

363 memoizer=memoizer, 

364 monitoring=monitor, 

365 retries=retries, 

366 run_dir=run_dir, 

367 strategy=strategy, 

368 )