Skip to content

Commit f7a48c9

Browse files
authored
Merge pull request #2918 from alicevision/dev/delayChunkEvaluation
[core] Add support for dynamic chunks
2 parents 70458d8 + 38211d6 commit f7a48c9

22 files changed

Lines changed: 2117 additions & 498 deletions

.git-blame-ignore-revs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Linting: Remove trailing whitespaces
2+
04a425decc1b80f0c67e8c4c98c0062d73836684
3+
# [core] Linting: Remove all trailing whitespaces
4+
8be302115edca60c93b1e97de3f457d91c271666
15
# [tests] Linting: Remove trailing whitespaces
26
5fe886b6b08fa19082dc0e1bf837fa34c2e2de2d
37
# [core] Linting: Remove remaining trailing whitespaces

bin/meshroom_compute

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import argparse
33
import logging
44
import os
55
import sys
6+
from typing import NoReturn
67

78
try:
89
import meshroom
@@ -16,7 +17,7 @@ meshroom.setupEnvironment()
1617

1718
import meshroom.core
1819
import meshroom.core.graph
19-
from meshroom.core.node import Status, ExecMode
20+
from meshroom.core.node import Status
2021

2122

2223
parser = argparse.ArgumentParser(description='Execute a Graph of processes.')
@@ -63,12 +64,28 @@ else:
6364

6465
meshroom.core.initPlugins()
6566
meshroom.core.initNodes()
67+
meshroom.core.initSubmitters()
6668

6769
graph = meshroom.core.graph.loadGraph(args.graphFile)
6870
if args.cache:
6971
graph.cacheDir = args.cache
7072
graph.update()
7173

74+
75+
def killRunningJob(node) -> NoReturn:
76+
""" Kills current job and try to avoid job restarting """
77+
jobInfo = node.nodeStatus.jobInfo
78+
submitterName = jobInfo.get("submitterName")
79+
if not submitterName:
80+
sys.exit(meshroom.MeshroomExitStatus.ERROR_NO_RETRY)
81+
from meshroom.core import submitters
82+
for subName, sub in submitters.items():
83+
if submitterName == subName:
84+
sub.killRunningJob()
85+
break
86+
sys.exit(meshroom.MeshroomExitStatus.ERROR_NO_RETRY)
87+
88+
7289
if args.node:
7390
node = graph.findNode(args.node)
7491
submittedStatuses = [Status.RUNNING]
@@ -83,6 +100,15 @@ if args.node:
83100
# If running as "extern", the task is supposed to have the status SUBMITTED.
84101
# If not running as "extern", the SUBMITTED status should generate a warning.
85102
submittedStatuses.append(Status.SUBMITTED)
103+
104+
if not node._chunksCreated:
105+
print(f"Error: Node {node} has been submitted before chunks have been created." \
106+
"See file: \"{node.nodeStatusFile}\".")
107+
sys.exit(-1)
108+
109+
if node._isInputNode():
110+
print(f"InputNode: No computation to do.")
111+
86112
if not args.forceStatus and not args.forceCompute:
87113
if args.iteration != -1:
88114
chunks = [node.chunks[args.iteration]]
@@ -91,10 +117,11 @@ if args.node:
91117
for chunk in chunks:
92118
if chunk.status.status in submittedStatuses:
93119
# Particular case for the local isolated, the node status is set to RUNNING by the submitter directly.
94-
# We ensure that no other instance has started to compute, by checking that the sessionUid is empty.
95-
if chunk.node.getMrNodeType() == meshroom.core.MrNodeType.NODE and not chunk.status.sessionUid and chunk.status.submitterSessionUid:
120+
# We ensure that no other instance has started to compute, by checking that the computeSessionUid is empty.
121+
if chunk.node.getMrNodeType() == meshroom.core.MrNodeType.NODE and \
122+
not chunk.status.computeSessionUid and node._nodeStatus.submitterSessionUid:
96123
continue
97-
print(f'Warning: Node is already submitted with status "{chunk.status.status.name}". See file: "{chunk.statusFile}". ExecMode: {chunk.status.execMode.name}, SessionUid: {chunk.status.sessionUid}, submitterSessionUid: {chunk.status.submitterSessionUid}')
124+
print(f'Warning: Node is already submitted with status "{chunk.status.status.name}". See file: "{chunk.statusFile}". ExecMode: {chunk.status.execMode.name}, computeSessionUid: {chunk.status.computeSessionUid}, submitterSessionUid: {node._nodeStatus.submitterSessionUid}')
98125
# sys.exit(-1)
99126

100127
if args.extern:
@@ -105,8 +132,14 @@ if args.node:
105132
node.preprocess()
106133
if args.iteration != -1:
107134
chunk = node.chunks[args.iteration]
135+
if chunk._status.status == Status.STOPPED:
136+
print(f"Chunk {chunk} : status is STOPPED")
137+
killRunningJob(node)
108138
chunk.process(args.forceCompute, args.inCurrentEnv)
109139
else:
140+
if node.nodeStatus.status == Status.STOPPED:
141+
print(f"Node {node} : status is STOPPED")
142+
killRunningJob(node)
110143
node.process(args.forceCompute, args.inCurrentEnv)
111144
node.postprocess()
112145
node.restoreLogger()

bin/meshroom_createChunks

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
This is a script used to wrap the process of processing a node on the farm
5+
It will handle chunk creation and create all the jobs for these chunks
6+
If the submitter cannot create chunks, then it will process the chunks serially
7+
in the current process
8+
"""
9+
10+
import argparse
11+
import logging
12+
import os
13+
import sys
14+
try:
15+
import meshroom
16+
except Exception:
17+
# If meshroom module is not in the PYTHONPATH, add our root using the relative path
18+
import pathlib
19+
meshroomRootFolder = pathlib.Path(__file__).parent.parent.resolve()
20+
sys.path.append(meshroomRootFolder)
21+
import meshroom
22+
meshroom.setupEnvironment()
23+
24+
import meshroom.core
25+
import meshroom.core.graph
26+
from meshroom.core import submitters
27+
from meshroom.core.submitter import SubmitterOptionsEnum
28+
from meshroom.core.node import Status
29+
30+
31+
parser = argparse.ArgumentParser(description='Execute a Graph of processes.')
32+
parser.add_argument('graphFile', metavar='GRAPHFILE.mg', type=str,
33+
help='Filepath to a graph file.')
34+
35+
parser.add_argument('--submitter', type=str, required=True,
36+
help='Name of the submitter used to create the job.')
37+
parser.add_argument('--node', metavar='NODE_NAME', type=str, required=True,
38+
help='Process the node. It will generate an error if the dependencies are not already computed.')
39+
parser.add_argument('--inCurrentEnv', help='Execute process in current env without creating a dedicated runtime environment.',
40+
action='store_true')
41+
parser.add_argument('--forceStatus', help='Force computation if status is RUNNING or SUBMITTED.',
42+
action='store_true')
43+
parser.add_argument('--forceCompute', help='Compute in all cases even if already computed.',
44+
action='store_true')
45+
parser.add_argument('--extern', help='Use this option when you compute externally after submission to a render farm from meshroom.',
46+
action='store_true')
47+
parser.add_argument('--cache', metavar='FOLDER', type=str,
48+
default=None,
49+
help='Override the cache folder')
50+
parser.add_argument('-v', '--verbose',
51+
help='Set the verbosity level for logging:\n'
52+
' - fatal: Show only critical errors.\n'
53+
' - error: Show errors only.\n'
54+
' - warning: Show warnings and errors.\n'
55+
' - info: Show standard informational messages.\n'
56+
' - debug: Show detailed debug information.\n'
57+
' - trace: Show all messages, including trace-level details.',
58+
default=os.environ.get('MESHROOM_VERBOSE', 'info'),
59+
choices=['fatal', 'error', 'warning', 'info', 'debug', 'trace'])
60+
61+
args = parser.parse_args()
62+
63+
# For extern computation, we want to focus on the node computation log.
64+
# So, we avoid polluting the log with general warning about plugins, versions of nodes in file, etc.
65+
logging.getLogger().setLevel(level=logging.INFO)
66+
67+
meshroom.core.initPlugins()
68+
meshroom.core.initNodes()
69+
meshroom.core.initSubmitters() # Required to spool child job
70+
71+
graph = meshroom.core.graph.loadGraph(args.graphFile)
72+
if args.cache:
73+
graph.cacheDir = args.cache
74+
graph.update()
75+
76+
# Execute the node
77+
node = graph.findNode(args.node)
78+
submittedStatuses = [Status.RUNNING]
79+
80+
# Find submitter
81+
submitter = None
82+
# It's required if we want to spool chunks on different machines
83+
for subName, sub in submitters.items():
84+
if args.submitter == subName:
85+
submitter = sub
86+
break
87+
88+
if node._nodeStatus.status in (Status.STOPPED, Status.KILLED):
89+
logging.error("Node status is STOPPED or KILLED.")
90+
if submitter:
91+
submitter.killRunningJob()
92+
sys.exit(meshroom.MeshroomExitStatus.ERROR_NO_RETRY)
93+
94+
if not node._chunksCreated:
95+
# Create node chunks
96+
# Once created we don't have to do it again even if we relaunch the job
97+
node.createChunks()
98+
# Set the chunks statuses
99+
for chunk in node._chunks:
100+
if args.forceCompute or chunk._status.status != Status.SUCCESS:
101+
hasChunkToLaunch = True
102+
chunk._status.setNode(node)
103+
chunk._status.initExternSubmit()
104+
chunk.upgradeStatusFile()
105+
106+
# Get chunks to process in the current process
107+
chunksToProcess = []
108+
if submitter:
109+
if not submitter._options.includes(SubmitterOptionsEnum.EDIT_TASKS):
110+
chunksToProcess = node.chunks
111+
else:
112+
# Cannot retrieve job -> execute process serially
113+
chunksToProcess = node.chunks
114+
115+
logging.info(f"[MeshroomCreateChunks] Chunks to process here : {chunksToProcess}")
116+
117+
if not args.forceStatus and not args.forceCompute:
118+
for chunk in chunksToProcess:
119+
if chunk.status.status in submittedStatuses:
120+
# Particular case for the local isolated, the node status is set to RUNNING by the submitter directly.
121+
# We ensure that no other instance has started to compute, by checking that the sessicomputeSessionUidonUid is empty.
122+
if chunk.node.getMrNodeType() == meshroom.core.MrNodeType.NODE and \
123+
not chunk.status.computeSessionUid and node._nodeStatus.submitterSessionUid:
124+
continue
125+
logging.warning(
126+
f"[MeshroomCreateChunks] Node is already submitted with status " \
127+
f"\"{chunk.status.status.name}\". See file: \"{chunk.statusFile}\". " \
128+
f"ExecMode: {chunk.status.execMode.name}, computeSessionUid: {chunk.status.computeSessionUid}, " \
129+
f"submitterSessionUid: {node._nodeStatus.submitterSessionUid}")
130+
131+
if chunksToProcess:
132+
node.prepareLogger()
133+
node.preprocess()
134+
for chunk in chunksToProcess:
135+
logging.info(f"[MeshroomCreateChunks] process chunk {chunk}")
136+
chunk.process(args.forceCompute, args.inCurrentEnv)
137+
node.postprocess()
138+
node.restoreLogger()
139+
else:
140+
logging.info(f"[MeshroomCreateChunks] -> create job to process chunks {node.chunks}")
141+
submitter.createChunkTask(node, graphFile=args.graphFile, cache=args.cache,
142+
forceStatus=args.forceStatus, forceCompute=args.forceCompute)
143+
144+
# Restore the log level
145+
logging.getLogger().setLevel(meshroom.logStringToPython[args.verbose])

meshroom/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from enum import Enum
1+
from enum import Enum, IntEnum
22
import logging
33
import os
44
import sys
@@ -75,6 +75,18 @@ def logToRoot(message, *args, **kwargs):
7575
logging.getLogger().setLevel(logStringToPython[os.environ.get('MESHROOM_VERBOSE', 'warning')])
7676

7777

78+
class MeshroomExitStatus(IntEnum):
79+
""" In case we want to catch some special case from the parent process
80+
We could use 3-125 for custom exist codes :
81+
https://tldp.org/LDP/abs/html/exitcodes.html
82+
"""
83+
SUCCESS = 0
84+
ERROR = 1
85+
# In some farm tools jobs are automatically re-tried,
86+
# using ERROR_NO_RETRY will try to prevent that
87+
ERROR_NO_RETRY = -999 # It's actually -999 % 256 => 25
88+
89+
7890
def setupEnvironment(backend=Backend.STANDALONE):
7991
"""
8092
Setup environment for Meshroom to work in a prebuilt, standalone configuration.

meshroom/core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def loadClasses(folder: str, packageName: str, classType: type) -> list[type]:
119119
classes.append(p)
120120
except Exception as exc:
121121
if classType == BaseSubmitter:
122-
logging.warning(f" Could not load submitter {pluginName} from package '{package.__name__}'")
122+
logging.warning(f" Could not load submitter {pluginName} from package '{package.__name__}'\n{exc}")
123123
else:
124124
tb = traceback.extract_tb(exc.__traceback__)
125125
last_call = tb[-1]

meshroom/core/desc/computation.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import math
2-
from enum import Enum
2+
from enum import IntEnum
33

44
from .attribute import ListAttribute, IntParam
55

66

7-
class Level(Enum):
7+
class Level(IntEnum):
8+
SCRIPT=-1
89
NONE = 0
910
NORMAL = 1
1011
INTENSIVE = 2
12+
EXTREME = 3
1113

1214

1315
class Range:
@@ -46,6 +48,9 @@ def toDict(self):
4648
"rangeBlocksCount": self.nbBlocks
4749
}
4850

51+
def __repr__(self):
52+
return f"<Range {self.iteration}({self.blockSize})/{self.nbBlocks}({self.fullSize})>"
53+
4954

5055
class Parallelization:
5156
def __init__(self, staticNbBlocks=0, blockSize=0):

meshroom/core/desc/node.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import shlex
66
import shutil
77
import sys
8+
import signal
9+
import subprocess
810

911
import psutil
1012

@@ -20,6 +22,34 @@
2022
_MESHROOM_COMPUTE_DEPS = ["psutil"]
2123

2224

25+
# Handle cleanup
26+
class ExitCleanup:
27+
"""
28+
Make sure we kill child subprocesses when the main process exits receive SIGTERM.
29+
"""
30+
31+
def __init__(self):
32+
self._subprocesses = []
33+
signal.signal(signal.SIGTERM, self.exit)
34+
35+
def addSubprocess(self, process):
36+
logging.debug(f"[ExitCleanup] Register subprocess {process}")
37+
self._subprocesses.append(process)
38+
39+
def exit(self, signum, frame):
40+
for proc in self._subprocesses:
41+
logging.debug(f"[ExitCleanup] Kill subprocess {proc}")
42+
try:
43+
if proc.is_running():
44+
proc.terminate()
45+
proc.wait(timeout=5)
46+
except subprocess.TimeoutExpired:
47+
proc.kill()
48+
sys.exit(0)
49+
50+
exitCleanup = ExitCleanup()
51+
52+
2353
class MrNodeType(enum.Enum):
2454
NONE = enum.auto()
2555
BASENODE = enum.auto()
@@ -90,6 +120,9 @@ class BaseNode(object):
90120
documentation = ""
91121
category = "Other"
92122
plugin = None
123+
# Licenses required to run the plugin
124+
# Only used to select machines on the farm when the node is submitted
125+
_licenses = []
93126

94127
def __init__(self):
95128
super(BaseNode, self).__init__()
@@ -158,7 +191,7 @@ def processChunk(self, chunk):
158191

159192
def executeChunkCommandLine(self, chunk, cmd, env=None):
160193
try:
161-
with open(chunk.logFile, 'a') as logF:
194+
with open(chunk.getLogFile(), 'a') as logF:
162195
chunk.status.commandLine = cmd
163196
chunk.saveStatusFile()
164197
cmdList = shlex.split(cmd)
@@ -167,7 +200,7 @@ def executeChunkCommandLine(self, chunk, cmd, env=None):
167200

168201
print(f"Starting Process for '{chunk.node.name}'")
169202
print(f" - commandLine: {cmd}")
170-
print(f" - logFile: {chunk.logFile}")
203+
print(f" - logFile: {chunk.getLogFile()}")
171204
if prog:
172205
cmdList[0] = Path(prog).as_posix()
173206
print(f" - command full path: {cmdList[0]}")
@@ -193,6 +226,7 @@ def executeChunkCommandLine(self, chunk, cmd, env=None):
193226
text=True,
194227
**platformArgs,
195228
)
229+
exitCleanup.addSubprocess(chunk.subprocess)
196230

197231
if hasattr(chunk, "statThread"):
198232
# We only have a statThread if the node is running in the current process
@@ -213,7 +247,7 @@ def executeChunkCommandLine(self, chunk, cmd, env=None):
213247
pass
214248

215249
if chunk.subprocess.returncode != 0:
216-
with open(chunk.logFile, "r") as logF:
250+
with open(chunk.getLogFile(), "r") as logF:
217251
logContent = "".join(logF.readlines())
218252
raise RuntimeError(f'Error on node "{chunk.name}":\nLog:\n{logContent}')
219253
finally:

0 commit comments

Comments
 (0)