Skip to content

Commit 8dd57ef

Browse files
committed
feat: Computing Elements use MaxRAM
1 parent 92382a1 commit 8dd57ef

14 files changed

Lines changed: 208 additions & 55 deletions

File tree

docs/source/UserGuide/Tutorials/JobManagementAdvanced/index.rst

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,16 +335,15 @@ Jobs that can (or should) run using more than 1 processor should be described as
335335
using the "setNumberOfProcessors" method of the API::
336336

337337
j = Job()
338-
j.setCPUTime(500)
339-
j.setExecutable('echo',arguments='hello')
340-
j.setExecutable('ls',arguments='-l')
341-
j.setExecutable('echo', arguments='hello again')
342-
j.setName('MP test')
338+
...
343339
j.setNumberOfProcessors(16)
344340

345341
Calling ``Job().setNumberOfProcessors()``, with a value bigger than 1,
346342
will translate into adding also the "MultiProcessor" tag to the job description.
347343

344+
``Job().setNumberOfProcessors()`` takes at most 3 arguments, in this order:
345+
``numberOfProcessors`` is the exact number of requested processors, ``minNumberOfProcessors`` is the minimum allowed, ``maxNumberOfProcessors`` the maximum.
346+
348347
Users can specify in the job descriptions NumberOfProcessors and WholeNode parameters, e.g.::
349348

350349
NumberOfProcessors = 16;
@@ -356,6 +355,20 @@ This will be translated internally into 16Processors and WholeNode tags.
356355
This would allow resources (WN's) to put flexibly requirements on jobs to be taken, for example, avoiding single-core jobs on a multi-core nodes.
357356

358357

358+
Setting memory limits
359+
@@@@@@@@@@@@@@@@@@@@@
360+
361+
Jobs can (and probably should) set RAM limits.
362+
using the "setRAMRequirements" method of the API::
363+
364+
j = Job()
365+
...
366+
j.setRAMRequirements(2, 4)
367+
368+
Calling ``Job().setRAMRequirements()`` takes 2 values, where the first is the minimum required amount of RAM (in GB) that the job requests.
369+
The second value instead specifies the limit that should not be surpassed.
370+
371+
359372
Submitting jobs with specifc requirements (e.g. GPU)
360373
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
361374

src/DIRAC/Core/Utilities/CGroups2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ def systemCall(self, *args, **kwargs):
300300
if "ceParameters" in kwargs:
301301
if cpuLimit := kwargs["ceParameters"].get("CPULimit", None):
302302
cores = float(cpuLimit)
303+
# MemoryLimitMB should be the job upper limit
303304
if memoryMB := int(kwargs["ceParameters"].get("MemoryLimitMB", 0)):
304305
memory = memoryMB * 1024 * 1024
305306
if kwargs["ceParameters"].get("MemoryNoSwap", "no").lower() in ("yes", "true"):

src/DIRAC/Interfaces/API/Job.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,18 +522,49 @@ def setDestination(self, destination):
522522
return S_OK()
523523

524524
#############################################################################
525-
def setRAMRequirements(self, ramRequired: int = 0):
525+
def setRAMRequirements(self, ramRequired: int = 0, maxRAM: int = 0):
526526
"""Helper function.
527-
Specify the RAM requirements for the job in GB. 0 (default) means no specific requirements.
527+
Specify the RAM requirements for the job. 0 (default) means no specific requirements.
528+
529+
Example usage:
530+
531+
>>> job = Job()
532+
>>> job.setRAMRequirements(ramRequired=2)
533+
means that the job needs at least 2 GBs of RAM to work. This is taken into consideration at job's matching time.
534+
The job definition does not specify an upper limit.
535+
From a user's point of view this is fine (normally, not for admins).
536+
537+
>>> job.setRAMRequirements(ramRequired=2, maxRAM=4)
538+
means that the job needs 2 GBs of RAM to work. 4 GBs will then be the upper limit for CG2 limits.
539+
540+
>>> job.setRAMRequirements(ramRequired=4, maxRAM=4)
541+
means that we should match this job if there is at least 4 available GBs of run. At the same time, CG2 will not allow to use more than that.
542+
543+
>>> job.setRAMRequirements(maxRAM=4)
544+
means that the job does not set a min amount of RAM (so can match--run "everywhere"), but the 4 GBs will then be the upper limit for CG2 limits.
545+
546+
>>> job.setRAMRequirements(ramRequired=8, maxRAM=4)
547+
Makes no sense, an error will be raised
528548
"""
549+
if ramRequired and maxRAM and ramRequired > maxRAM:
550+
return self._reportError("Invalid settings, ramRequired is higher than maxRAM")
551+
529552
if ramRequired:
530553
self._addParameter(
531554
self.workflow,
532-
"MaxRAM",
555+
"MinRAM",
533556
"JDL",
534557
ramRequired,
535558
"GBs of RAM requested",
536559
)
560+
if maxRAM:
561+
self._addParameter(
562+
self.workflow,
563+
"MaxRAM",
564+
"JDL",
565+
maxRAM,
566+
"Max GBs of RAM to be used",
567+
)
537568

538569
def setNumberOfProcessors(self, numberOfProcessors=None, minNumberOfProcessors=None, maxNumberOfProcessors=None):
539570
"""Helper function.

src/DIRAC/Resources/Computing/ComputingElement.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,14 +236,14 @@ def setParameters(self, ceOptions):
236236
generalCEDict.update(self.ceParameters)
237237
self.ceParameters = generalCEDict
238238

239-
# If NumberOfProcessors/GPUs/RAM is present in the description but is equal to zero
239+
# If NumberOfProcessors/GPUs/MaxRAM is present in the description but is equal to zero
240240
# interpret it as needing local evaluation
241-
if self.ceParameters.get("NumberOfProcessors", -1) == 0:
241+
if int(self.ceParameters.get("NumberOfProcessors", -1)) == 0:
242242
self.ceParameters["NumberOfProcessors"] = getNumberOfProcessors()
243-
if self.ceParameters.get("NumberOfGPUs", -1) == 0:
243+
if int(self.ceParameters.get("NumberOfGPUs", -1)) == 0:
244244
self.ceParameters["NumberOfGPUs"] = getNumberOfGPUs()
245-
if self.ceParameters.get("RAM", -1) == 0:
246-
self.ceParameters["RAM"] = getAvailableRAM()
245+
if int(self.ceParameters.get("MaxRAM", 0)) == 0:
246+
self.ceParameters["MaxRAM"] = getAvailableRAM()
247247

248248
for key in ceOptions:
249249
if key in INTEGER_PARAMETERS:
@@ -289,6 +289,7 @@ def available(self, jobIDList=None):
289289
waitingJobs = result["WaitingJobs"]
290290
submittedJobs = result["SubmittedJobs"]
291291
availableProcessors = result.get("AvailableProcessors")
292+
292293
ceInfoDict = dict(result)
293294

294295
maxTotalJobs = int(self.ceParameters.get("MaxTotalJobs", 0))
@@ -492,6 +493,7 @@ def getDescription(self):
492493
result = self.getCEStatus()
493494
if result["OK"]:
494495
ceDict["NumberOfProcessors"] = result.get("AvailableProcessors", result.get("NumberOfProcessors", 1))
496+
ceDict["MaxRAM"] = result.get("AvailableRAM", result.get("MaxRAM", 1))
495497
else:
496498
self.log.error(
497499
"Failure getting CE status", "(we keep going without the number of waiting and running pilots/jobs)"

src/DIRAC/Resources/Computing/InProcessComputingElement.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def __init__(self, ceUniqueID):
2222
self.runningJobs = 0
2323

2424
self.processors = int(self.ceParameters.get("NumberOfProcessors", 1))
25+
self.maxRAM = int(self.ceParameters.get("MaxRAM", 0))
2526
self.ceParameters["MaxTotalJobs"] = 1
2627

2728
def submitJob(self, executableFile, proxy=None, inputs=None, **kwargs):
@@ -118,4 +119,6 @@ def getCEStatus(self):
118119
result["WaitingJobs"] = 0
119120
# processors
120121
result["AvailableProcessors"] = self.processors
122+
# RAM
123+
result["AvailableRAM"] = self.maxRAM
121124
return result

src/DIRAC/Resources/Computing/PoolComputingElement.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
from DIRAC.Resources.Computing.SingularityComputingElement import SingularityComputingElement
3030

3131

32-
def executeJob(executableFile, proxy, taskID, inputs, **kwargs):
33-
"""wrapper around ce.submitJob: decides which CE to use (InProcess or Singularity)
32+
def executeJob(executableFile, proxy, taskID, inputs, innerCEParameters, **kwargs):
33+
"""wrapper around ce.submitJob: decides which inner CE to use (InProcess or Singularity)
3434
3535
:param str executableFile: location of the executable file
3636
:param str proxy: proxy file location to be used for job submission
@@ -46,6 +46,9 @@ def executeJob(executableFile, proxy, taskID, inputs, **kwargs):
4646
else:
4747
ce = InProcessComputingElement("Task-" + str(taskID))
4848

49+
# adding the number of processors to use and the RAM
50+
ce.ceParameters["NumberOfProcessors"] = innerCEParameters["NumberOfProcessors"]
51+
ce.ceParameters["MaxRAM"] = innerCEParameters["MaxRAM"]
4952
return ce.submitJob(executableFile, proxy, inputs=inputs, **kwargs)
5053

5154

@@ -60,8 +63,9 @@ def __init__(self, ceUniqueID):
6063
self.pPool = None
6164
self.taskID = 0
6265
self.processorsPerTask = {}
63-
self.userNumberPerTask = {}
64-
self.ram = 1024 # Default RAM in GB (this is an arbitrary large value in case of no limit)
66+
self.ram = (
67+
1024 # Available RAM for the node, in GB. The default value is an arbitrary large value in case of no limit
68+
)
6569
self.ramPerTask = {}
6670

6771
# This CE will effectively submit to another "Inner"CE
@@ -76,10 +80,8 @@ def _reset(self):
7680

7781
self.processors = int(self.ceParameters.get("NumberOfProcessors", self.processors))
7882
self.ceParameters["MaxTotalJobs"] = self.processors
79-
max_ram = int(self.ceParameters.get("MaxRAM", 0))
80-
if max_ram > 0:
81-
self.ram = max_ram // 1024 # Convert from MB to GB
82-
self.ceParameters["MaxRAM"] = self.ram
83+
if self.ceParameters.get("MaxRAM", 0):
84+
self.ram = int(self.ceParameters["MaxRAM"])
8385
# Indicates that the submission is done asynchronously
8486
# The result is not immediately available
8587
self.ceParameters["AsyncSubmission"] = True
@@ -128,12 +130,19 @@ def submitJob(self, executableFile, proxy=None, inputs=None, **kwargs):
128130
if not res["OK"]:
129131
self.log.error("Could not dump cfg to pilot.cfg", res["Message"])
130132

133+
# Define the innerCEParameters
134+
innerCEParameters = {}
135+
innerCEParameters["NumberOfProcessors"] = processorsForJob
136+
innerCEParameters["MaxRAM"] = memoryForJob
137+
131138
# Here we define task kwargs: adding complex objects like thread.Lock can trigger errors in the task
132139
taskKwargs = {"InnerCESubmissionType": self.innerCESubmissionType}
133140
taskKwargs["jobDesc"] = kwargs.get("jobDesc", {})
134141

135142
# Submission
136-
future = self.pPool.submit(executeJob, executableFile, proxy, self.taskID, inputs, **taskKwargs)
143+
future = self.pPool.submit(
144+
executeJob, executableFile, proxy, self.taskID, inputs, innerCEParameters, **taskKwargs
145+
)
137146
self.processorsPerTask[future] = processorsForJob
138147
self.ramPerTask[future] = memoryForJob
139148
future.add_done_callback(functools.partial(self.finalizeJob, self.taskID))
@@ -190,14 +199,22 @@ def _getMemoryForJobs(self, kwargs):
190199
"""
191200

192201
# # job requirements
193-
requestedMemory = kwargs.get("MaxRAM", 0)
202+
requestedMemory = kwargs.get("MinRAM", 0)
194203

195204
# # now check what the slot can provide
196205
# Do we have enough memory?
197206
availableMemory = self.ram - sum(self.ramPerTask.values())
198207
if availableMemory < requestedMemory:
199208
return None
200209

210+
# if there's a MaxRAM requested, we allocate it all (if it fits),
211+
# and if it doesn't, we stop here instead of using MinRAM
212+
if kwargs.get("MaxRAM", 0):
213+
if availableMemory >= kwargs.get("MaxRAM"):
214+
requestedMemory = kwargs.get("MaxRAM")
215+
else:
216+
return None
217+
201218
return requestedMemory
202219

203220
def finalizeJob(self, taskID, future):
@@ -206,23 +223,26 @@ def finalizeJob(self, taskID, future):
206223
:param future: evaluating the future result
207224
"""
208225
nProc = self.processorsPerTask.pop(future)
226+
ram = self.ramPerTask.pop(future, None)
209227

210228
result = future.result() # This would be the result of the e.g. InProcess.submitJob()
211229
if result["OK"]:
212-
self.log.info("Task finished successfully:", f"{taskID}; {nProc} processor(s) freed")
230+
self.log.info("Task finished successfully:", f"{taskID}; {nProc} processor(s) and {ram}GB freed")
213231
else:
214232
self.log.error("Task failed submission:", f"{taskID}; message: {result['Message']}")
215233
self.taskResults[taskID] = result
216234

217235
def getCEStatus(self):
218236
"""Method to return information on running and waiting jobs,
219-
as well as the number of processors (used, and available).
237+
as well as the number of processors (used, and available),
238+
and the RAM (used, and available)
220239
221240
:return: dictionary of numbers of jobs per status and processors (used, and available)
222241
"""
223242

224243
result = S_OK()
225244
nJobs = 0
245+
226246
for _j, value in self.processorsPerTask.items():
227247
if value > 0:
228248
nJobs += 1
@@ -234,6 +254,10 @@ def getCEStatus(self):
234254
processorsInUse = sum(self.processorsPerTask.values())
235255
result["UsedProcessors"] = processorsInUse
236256
result["AvailableProcessors"] = self.processors - processorsInUse
257+
# dealing with RAM
258+
result["UsedRAM"] = sum(self.ramPerTask.values())
259+
result["AvailableRAM"] = self.ram - sum(self.ramPerTask.values())
260+
237261
return result
238262

239263
def getDescription(self):

src/DIRAC/Resources/Computing/SingularityComputingElement.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def __init__(self, ceUniqueID):
115115
self.__installDIRACInContainer = False
116116

117117
self.processors = int(self.ceParameters.get("NumberOfProcessors", 1))
118+
self.maxRAM = int(self.ceParameters.get("MaxRAM", 0))
118119

119120
@staticmethod
120121
def __findInstallBaseDir():
@@ -413,6 +414,12 @@ def submitJob(self, executableFile, proxy=None, **kwargs):
413414

414415
self.log.debug(f"Execute singularity command: {cmd}")
415416
self.log.debug(f"Execute singularity env: {self.__getEnv()}")
417+
# systemCall below uses ceParameters["MemoryLimitMB"] as CG2 upper memory limit
418+
# if there's a max RAM available to the job, use that
419+
if self.maxRAM:
420+
self.ceParameters["MemoryLimitMB"] = min(
421+
self.maxRAM * 1024, self.ceParameters.get("MemoryLimitMB", 1024 * 1024)
422+
) # 1024 * 1024 is an arbitrary large number
416423
result = CG2Manager().systemCall(
417424
0, cmd, callbackFunction=self.sendOutput, env=self.__getEnv(), ceParameters=self.ceParameters
418425
)
@@ -449,4 +456,6 @@ def getCEStatus(self):
449456
result["WaitingJobs"] = 0
450457
# processors
451458
result["AvailableProcessors"] = self.processors
459+
# RAM
460+
result["AvailableRAM"] = self.maxRAM
452461
return result

src/DIRAC/Resources/Computing/test/Test_InProcessComputingElement.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def test_submitJob():
5252
maxNumberOfProcessors=8,
5353
wholeNode=False,
5454
mpTag=True,
55+
MinRAM=2,
56+
MaxRAM=4,
5557
jobDesc={"jobParams": jobParams, "resourceParams": resourceParams, "optimizerParams": optimizerParams},
5658
)
5759
assert res["OK"] is True

0 commit comments

Comments
 (0)