siliconcompiler 0.33.2__py3-none-any.whl → 0.34.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- siliconcompiler/__init__.py +2 -0
- siliconcompiler/_metadata.py +1 -1
- siliconcompiler/apps/_common.py +1 -1
- siliconcompiler/apps/sc.py +1 -1
- siliconcompiler/apps/sc_issue.py +6 -4
- siliconcompiler/apps/sc_remote.py +3 -20
- siliconcompiler/apps/sc_show.py +2 -2
- siliconcompiler/apps/utils/replay.py +4 -4
- siliconcompiler/checklist.py +202 -1
- siliconcompiler/core.py +62 -293
- siliconcompiler/data/templates/email/general.j2 +3 -3
- siliconcompiler/data/templates/email/summary.j2 +1 -1
- siliconcompiler/data/templates/issue/README.txt +1 -1
- siliconcompiler/data/templates/report/sc_report.j2 +7 -7
- siliconcompiler/dependencyschema.py +392 -0
- siliconcompiler/design.py +758 -0
- siliconcompiler/flowgraph.py +79 -13
- siliconcompiler/optimizer/vizier.py +2 -2
- siliconcompiler/package/__init__.py +383 -223
- siliconcompiler/package/git.py +75 -77
- siliconcompiler/package/github.py +70 -97
- siliconcompiler/package/https.py +77 -93
- siliconcompiler/packageschema.py +260 -0
- siliconcompiler/pdk.py +5 -5
- siliconcompiler/remote/client.py +33 -15
- siliconcompiler/remote/server.py +2 -2
- siliconcompiler/report/dashboard/cli/__init__.py +6 -6
- siliconcompiler/report/dashboard/cli/board.py +4 -4
- siliconcompiler/report/dashboard/web/components/__init__.py +5 -5
- siliconcompiler/report/dashboard/web/components/flowgraph.py +4 -4
- siliconcompiler/report/dashboard/web/components/graph.py +2 -2
- siliconcompiler/report/dashboard/web/state.py +1 -1
- siliconcompiler/report/dashboard/web/utils/__init__.py +5 -5
- siliconcompiler/report/html_report.py +1 -1
- siliconcompiler/report/report.py +4 -4
- siliconcompiler/report/summary_table.py +2 -2
- siliconcompiler/report/utils.py +5 -5
- siliconcompiler/scheduler/__init__.py +3 -1382
- siliconcompiler/scheduler/docker.py +263 -0
- siliconcompiler/scheduler/run_node.py +10 -21
- siliconcompiler/scheduler/scheduler.py +311 -0
- siliconcompiler/scheduler/schedulernode.py +944 -0
- siliconcompiler/scheduler/send_messages.py +3 -3
- siliconcompiler/scheduler/slurm.py +149 -163
- siliconcompiler/scheduler/taskscheduler.py +45 -57
- siliconcompiler/schema/__init__.py +3 -3
- siliconcompiler/schema/baseschema.py +234 -11
- siliconcompiler/schema/editableschema.py +4 -0
- siliconcompiler/schema/journal.py +210 -0
- siliconcompiler/schema/namedschema.py +55 -2
- siliconcompiler/schema/parameter.py +14 -1
- siliconcompiler/schema/parametervalue.py +1 -34
- siliconcompiler/schema/schema_cfg.py +210 -349
- siliconcompiler/tool.py +412 -148
- siliconcompiler/tools/__init__.py +2 -0
- siliconcompiler/tools/builtin/_common.py +5 -5
- siliconcompiler/tools/builtin/concatenate.py +7 -7
- siliconcompiler/tools/builtin/minimum.py +4 -4
- siliconcompiler/tools/builtin/mux.py +4 -4
- siliconcompiler/tools/builtin/nop.py +4 -4
- siliconcompiler/tools/builtin/verify.py +8 -9
- siliconcompiler/tools/execute/exec_input.py +1 -1
- siliconcompiler/tools/genfasm/genfasm.py +1 -6
- siliconcompiler/tools/openroad/_apr.py +5 -1
- siliconcompiler/tools/openroad/antenna_repair.py +1 -1
- siliconcompiler/tools/openroad/macro_placement.py +1 -1
- siliconcompiler/tools/openroad/power_grid.py +1 -1
- siliconcompiler/tools/openroad/scripts/common/procs.tcl +32 -25
- siliconcompiler/tools/opensta/timing.py +26 -3
- siliconcompiler/tools/slang/__init__.py +2 -2
- siliconcompiler/tools/surfer/__init__.py +0 -0
- siliconcompiler/tools/surfer/show.py +53 -0
- siliconcompiler/tools/surfer/surfer.py +30 -0
- siliconcompiler/tools/vpr/route.py +82 -0
- siliconcompiler/tools/vpr/vpr.py +23 -6
- siliconcompiler/tools/yosys/__init__.py +1 -1
- siliconcompiler/tools/yosys/scripts/procs.tcl +143 -0
- siliconcompiler/tools/yosys/{sc_synth_asic.tcl → scripts/sc_synth_asic.tcl} +4 -0
- siliconcompiler/tools/yosys/{sc_synth_fpga.tcl → scripts/sc_synth_fpga.tcl} +24 -77
- siliconcompiler/tools/yosys/syn_fpga.py +14 -0
- siliconcompiler/toolscripts/_tools.json +9 -13
- siliconcompiler/toolscripts/rhel9/install-vpr.sh +0 -2
- siliconcompiler/toolscripts/ubuntu22/install-surfer.sh +33 -0
- siliconcompiler/toolscripts/ubuntu24/install-surfer.sh +33 -0
- siliconcompiler/utils/__init__.py +4 -24
- siliconcompiler/utils/flowgraph.py +29 -28
- siliconcompiler/utils/issue.py +23 -29
- siliconcompiler/utils/logging.py +37 -7
- siliconcompiler/utils/showtools.py +6 -1
- {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.1.dist-info}/METADATA +16 -25
- {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.1.dist-info}/RECORD +98 -91
- siliconcompiler/scheduler/docker_runner.py +0 -254
- siliconcompiler/schema/journalingschema.py +0 -242
- siliconcompiler/tools/yosys/procs.tcl +0 -71
- siliconcompiler/toolscripts/rhel9/install-yosys-parmys.sh +0 -68
- siliconcompiler/toolscripts/ubuntu22/install-yosys-parmys.sh +0 -68
- siliconcompiler/toolscripts/ubuntu24/install-yosys-parmys.sh +0 -68
- /siliconcompiler/tools/yosys/{sc_lec.tcl → scripts/sc_lec.tcl} +0 -0
- /siliconcompiler/tools/yosys/{sc_screenshot.tcl → scripts/sc_screenshot.tcl} +0 -0
- /siliconcompiler/tools/yosys/{syn_strategies.tcl → scripts/syn_strategies.tcl} +0 -0
- {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.1.dist-info}/WHEEL +0 -0
- {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.1.dist-info}/entry_points.txt +0 -0
- {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.1.dist-info}/licenses/LICENSE +0 -0
- {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import docker
|
|
2
|
+
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from siliconcompiler.package import RemoteResolver
|
|
9
|
+
from siliconcompiler.utils import default_email_credentials_file
|
|
10
|
+
from siliconcompiler.scheduler.schedulernode import SchedulerNode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_image(chip, step, index):
|
|
14
|
+
from siliconcompiler import __version__
|
|
15
|
+
|
|
16
|
+
queue = chip.get('option', 'scheduler', 'queue', step=step, index=index)
|
|
17
|
+
if queue:
|
|
18
|
+
return queue
|
|
19
|
+
|
|
20
|
+
return os.getenv(
|
|
21
|
+
'SC_DOCKER_IMAGE',
|
|
22
|
+
f'ghcr.io/siliconcompiler/sc_runner:v{__version__}')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_volumes_directories(chip, cache_dir, workdir, step, index):
|
|
26
|
+
all_dirs = set()
|
|
27
|
+
# Collect files
|
|
28
|
+
for key in chip.allkeys():
|
|
29
|
+
sc_type = chip.get(*key, field='type')
|
|
30
|
+
|
|
31
|
+
if 'file' in sc_type or 'dir' in sc_type:
|
|
32
|
+
cstep = step
|
|
33
|
+
cindex = index
|
|
34
|
+
|
|
35
|
+
if chip.get(*key, field='pernode').is_never():
|
|
36
|
+
cstep = None
|
|
37
|
+
cindex = None
|
|
38
|
+
|
|
39
|
+
files = chip.find_files(*key, step=cstep, index=cindex, missing_ok=True)
|
|
40
|
+
if files:
|
|
41
|
+
if not isinstance(files, list):
|
|
42
|
+
files = [files]
|
|
43
|
+
for path in files:
|
|
44
|
+
if path is None:
|
|
45
|
+
continue
|
|
46
|
+
if 'file' in sc_type:
|
|
47
|
+
all_dirs.add(os.path.dirname(path))
|
|
48
|
+
else:
|
|
49
|
+
all_dirs.add(path)
|
|
50
|
+
|
|
51
|
+
# Collect caches
|
|
52
|
+
for resolver in chip.get('package', field="schema").get_resolvers().values():
|
|
53
|
+
all_dirs.add(resolver())
|
|
54
|
+
|
|
55
|
+
all_dirs = [
|
|
56
|
+
Path(cache_dir),
|
|
57
|
+
Path(workdir),
|
|
58
|
+
Path(chip.scroot),
|
|
59
|
+
*[Path(path) for path in all_dirs]]
|
|
60
|
+
|
|
61
|
+
pruned_dirs = all_dirs.copy()
|
|
62
|
+
for base_path in all_dirs:
|
|
63
|
+
if base_path not in pruned_dirs:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
new_pruned_dirs = [base_path]
|
|
67
|
+
for check_path in pruned_dirs:
|
|
68
|
+
if base_path == check_path:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
if base_path not in check_path.parents:
|
|
72
|
+
new_pruned_dirs.append(check_path)
|
|
73
|
+
pruned_dirs = new_pruned_dirs
|
|
74
|
+
|
|
75
|
+
pruned_dirs = set(pruned_dirs)
|
|
76
|
+
|
|
77
|
+
builddir = chip.find_files('option', 'builddir')
|
|
78
|
+
|
|
79
|
+
rw_volumes = set()
|
|
80
|
+
|
|
81
|
+
for path in pruned_dirs:
|
|
82
|
+
for rw_allow in (Path(builddir), Path(workdir), Path(cache_dir)):
|
|
83
|
+
if path == rw_allow or path in rw_allow.parents:
|
|
84
|
+
rw_volumes.add(path)
|
|
85
|
+
|
|
86
|
+
ro_volumes = pruned_dirs.difference(rw_volumes)
|
|
87
|
+
|
|
88
|
+
return rw_volumes, ro_volumes
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class DockerSchedulerNode(SchedulerNode):
|
|
92
|
+
def __init__(self, chip, step, index, replay=False):
|
|
93
|
+
super().__init__(chip, step, index, replay=replay)
|
|
94
|
+
|
|
95
|
+
self.__queue = get_image(self.chip, self.step, self.index)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def queue(self):
|
|
99
|
+
return self.__queue
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def init(chip):
|
|
103
|
+
if sys.platform == 'win32':
|
|
104
|
+
# this avoids the issue of different file system types
|
|
105
|
+
chip.logger.error('Setting copy field to true for docker run on Windows')
|
|
106
|
+
for key in chip.allkeys():
|
|
107
|
+
if key[0] == 'history':
|
|
108
|
+
continue
|
|
109
|
+
sc_type = chip.get(*key, field='type')
|
|
110
|
+
if 'dir' in sc_type or 'file' in sc_type:
|
|
111
|
+
chip.set(*key, True, field='copy')
|
|
112
|
+
chip.collect()
|
|
113
|
+
|
|
114
|
+
def run(self):
|
|
115
|
+
self._init_run_logger()
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
client = docker.from_env()
|
|
119
|
+
client.version()
|
|
120
|
+
except (docker.errors.DockerException, docker.errors.APIError) as e:
|
|
121
|
+
self.logger.error(f'Unable to connect to docker: {e}')
|
|
122
|
+
self.halt()
|
|
123
|
+
|
|
124
|
+
is_windows = sys.platform == 'win32'
|
|
125
|
+
|
|
126
|
+
workdir = self.chip.getworkdir()
|
|
127
|
+
start_cwd = os.getcwd()
|
|
128
|
+
|
|
129
|
+
# Change working directory since the run may delete this folder
|
|
130
|
+
os.makedirs(workdir, exist_ok=True)
|
|
131
|
+
os.chdir(workdir)
|
|
132
|
+
|
|
133
|
+
image_name = get_image(self.chip, self.step, self.index)
|
|
134
|
+
|
|
135
|
+
# Pull image if needed
|
|
136
|
+
try:
|
|
137
|
+
image = client.images.get(image_name)
|
|
138
|
+
except docker.errors.ImageNotFound:
|
|
139
|
+
# Needs a lock to avoid downloading a bunch in parallel
|
|
140
|
+
image_repo, image_tag = image_name.split(':')
|
|
141
|
+
self.logger.info(f'Pulling docker image {image_name}')
|
|
142
|
+
try:
|
|
143
|
+
image = client.images.pull(image_repo, tag=image_tag)
|
|
144
|
+
except docker.errors.APIError as e:
|
|
145
|
+
self.logger.error(f'Unable to pull image: {e}')
|
|
146
|
+
image_src = image_repo.split('/')[0]
|
|
147
|
+
self.logger.error(f" if you are logged into {image_src} with expired credentials, "
|
|
148
|
+
f"please use 'docker logout {image_src}'")
|
|
149
|
+
self.halt()
|
|
150
|
+
|
|
151
|
+
email_file = default_email_credentials_file()
|
|
152
|
+
if is_windows:
|
|
153
|
+
# Hack to get around manifest merging
|
|
154
|
+
self.chip.set('option', 'cachedir', None)
|
|
155
|
+
cache_dir = '/sc_cache'
|
|
156
|
+
cwd = '/sc_docker'
|
|
157
|
+
builddir = f'{cwd}/build'
|
|
158
|
+
|
|
159
|
+
local_cfg = os.path.join(start_cwd, 'sc_docker.json')
|
|
160
|
+
job = self.chip.get('option', 'jobname')
|
|
161
|
+
cfg = f'{builddir}/{self.chip.design}/{job}/{self.step}/{self.index}/sc_docker.json'
|
|
162
|
+
|
|
163
|
+
user = None
|
|
164
|
+
|
|
165
|
+
volumes = [
|
|
166
|
+
f"{self.chip.cwd}:{cwd}:rw",
|
|
167
|
+
f"{RemoteResolver.determine_cache_dir(self.chip)}:{cache_dir}:rw"
|
|
168
|
+
]
|
|
169
|
+
self.logger.debug(f'Volumes: {volumes}')
|
|
170
|
+
|
|
171
|
+
env = {}
|
|
172
|
+
|
|
173
|
+
if os.path.exists(email_file):
|
|
174
|
+
env["HOME"] = "/sc_home"
|
|
175
|
+
|
|
176
|
+
volumes.append(f'{os.path.dirname(email_file)}:/sc_home/.sc:ro')
|
|
177
|
+
else:
|
|
178
|
+
cache_dir = RemoteResolver.determine_cache_dir(self.chip)
|
|
179
|
+
cwd = self.chip.cwd
|
|
180
|
+
builddir = self.chip.find_files('option', 'builddir')
|
|
181
|
+
|
|
182
|
+
local_cfg = os.path.abspath('sc_docker.json')
|
|
183
|
+
cfg = local_cfg
|
|
184
|
+
|
|
185
|
+
user = os.getuid()
|
|
186
|
+
|
|
187
|
+
rw_volumes, ro_volumes = get_volumes_directories(
|
|
188
|
+
self.chip, cache_dir, workdir, self.step, self.index)
|
|
189
|
+
volumes = [
|
|
190
|
+
*[
|
|
191
|
+
f'{path}:{path}:rw' for path in rw_volumes
|
|
192
|
+
],
|
|
193
|
+
*[
|
|
194
|
+
f'{path}:{path}:ro' for path in ro_volumes
|
|
195
|
+
]
|
|
196
|
+
]
|
|
197
|
+
self.logger.debug(f'Read write volumes: {rw_volumes}')
|
|
198
|
+
self.logger.debug(f'Read only volumes: {ro_volumes}')
|
|
199
|
+
|
|
200
|
+
env = {}
|
|
201
|
+
if os.path.exists(email_file):
|
|
202
|
+
env["HOME"] = "/sc_home"
|
|
203
|
+
|
|
204
|
+
volumes.append(f'{os.path.dirname(email_file)}:/sc_home/.sc:ro')
|
|
205
|
+
|
|
206
|
+
container = None
|
|
207
|
+
try:
|
|
208
|
+
container = client.containers.run(
|
|
209
|
+
image.id,
|
|
210
|
+
volumes=volumes,
|
|
211
|
+
labels=[
|
|
212
|
+
"siliconcompiler",
|
|
213
|
+
f"sc_node:{self.chip.design}:{self.step}:{self.index}"
|
|
214
|
+
],
|
|
215
|
+
user=user,
|
|
216
|
+
detach=True,
|
|
217
|
+
tty=True,
|
|
218
|
+
auto_remove=True,
|
|
219
|
+
environment=env)
|
|
220
|
+
|
|
221
|
+
# Write manifest to make it available to the docker runner
|
|
222
|
+
self.chip.write_manifest(local_cfg)
|
|
223
|
+
|
|
224
|
+
cachemap = []
|
|
225
|
+
for package, resolver in self.chip.get(
|
|
226
|
+
'package', field="schema").get_resolvers().items():
|
|
227
|
+
cachemap.append(f'{package}:{resolver()}')
|
|
228
|
+
|
|
229
|
+
self.logger.info('Running in docker container: '
|
|
230
|
+
f'{container.name} ({container.short_id})')
|
|
231
|
+
args = [
|
|
232
|
+
'-cfg', cfg,
|
|
233
|
+
'-cwd', cwd,
|
|
234
|
+
'-builddir', str(builddir),
|
|
235
|
+
'-cachedir', str(cache_dir),
|
|
236
|
+
'-step', self.step,
|
|
237
|
+
'-index', self.index,
|
|
238
|
+
'-unset_scheduler'
|
|
239
|
+
]
|
|
240
|
+
if not is_windows and cachemap:
|
|
241
|
+
args.append('-cachemap')
|
|
242
|
+
args.extend(cachemap)
|
|
243
|
+
cmd = f'python3 -m siliconcompiler.scheduler.run_node {shlex.join(args)}'
|
|
244
|
+
exec_handle = client.api.exec_create(container.name, cmd)
|
|
245
|
+
stream = client.api.exec_start(exec_handle, stream=True)
|
|
246
|
+
|
|
247
|
+
# Print the log
|
|
248
|
+
for chunk in stream:
|
|
249
|
+
for line in chunk.decode().splitlines():
|
|
250
|
+
print(line)
|
|
251
|
+
|
|
252
|
+
if client.api.exec_inspect(exec_handle['Id']).get('ExitCode') != 0:
|
|
253
|
+
self.halt()
|
|
254
|
+
finally:
|
|
255
|
+
# Ensure we clean up containers
|
|
256
|
+
if container:
|
|
257
|
+
try:
|
|
258
|
+
container.stop()
|
|
259
|
+
except docker.errors.APIError:
|
|
260
|
+
self.logger.error(f'Failed to stop docker container: {container.name}')
|
|
261
|
+
|
|
262
|
+
# Restore working directory
|
|
263
|
+
os.chdir(start_cwd)
|
|
@@ -7,8 +7,7 @@ import tarfile
|
|
|
7
7
|
import os.path
|
|
8
8
|
|
|
9
9
|
from siliconcompiler import Chip, Schema
|
|
10
|
-
from siliconcompiler.
|
|
11
|
-
from siliconcompiler.scheduler import _runtask, _executenode
|
|
10
|
+
from siliconcompiler.scheduler.schedulernode import SchedulerNode
|
|
12
11
|
from siliconcompiler import __version__
|
|
13
12
|
|
|
14
13
|
|
|
@@ -46,9 +45,6 @@ def main():
|
|
|
46
45
|
metavar='<package>:<directory>',
|
|
47
46
|
nargs='+',
|
|
48
47
|
help='Map of caches to prepopulate runner with')
|
|
49
|
-
parser.add_argument('-fetch_cache',
|
|
50
|
-
action='store_true',
|
|
51
|
-
help='Allow for cache downloads')
|
|
52
48
|
parser.add_argument('-step',
|
|
53
49
|
required=True,
|
|
54
50
|
metavar='<step>',
|
|
@@ -99,33 +95,26 @@ def main():
|
|
|
99
95
|
chip.set('record', 'remoteid', args.remoteid)
|
|
100
96
|
|
|
101
97
|
if args.unset_scheduler:
|
|
102
|
-
for
|
|
103
|
-
|
|
98
|
+
for _, step, index in chip.get('option', 'scheduler', 'name',
|
|
99
|
+
field=None).getvalues():
|
|
104
100
|
chip.unset('option', 'scheduler', 'name', step=step, index=index)
|
|
105
101
|
|
|
106
|
-
# Init logger to ensure consistent view
|
|
107
|
-
chip._init_logger(step=chip.get('arg', 'step'),
|
|
108
|
-
index=chip.get('arg', 'index'),
|
|
109
|
-
in_run=True)
|
|
110
|
-
|
|
111
102
|
if args.cachemap:
|
|
112
103
|
for cachepair in args.cachemap:
|
|
113
104
|
package, path = cachepair.split(':')
|
|
114
|
-
chip.
|
|
105
|
+
chip.get("package", field="schema")._set_cache(package, path)
|
|
115
106
|
|
|
116
107
|
# Populate cache
|
|
117
|
-
for
|
|
118
|
-
|
|
108
|
+
for resolver in chip.get('package', field='schema').get_resolvers().values():
|
|
109
|
+
resolver()
|
|
119
110
|
|
|
120
111
|
# Run the task.
|
|
121
112
|
error = True
|
|
122
113
|
try:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
_executenode,
|
|
128
|
-
replay=args.replay)
|
|
114
|
+
SchedulerNode(chip,
|
|
115
|
+
args.step,
|
|
116
|
+
args.index,
|
|
117
|
+
replay=args.replay).run()
|
|
129
118
|
error = False
|
|
130
119
|
|
|
131
120
|
finally:
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import os.path
|
|
7
|
+
|
|
8
|
+
from siliconcompiler import Schema
|
|
9
|
+
from siliconcompiler import NodeStatus
|
|
10
|
+
from siliconcompiler.schema import Journal
|
|
11
|
+
from siliconcompiler.flowgraph import RuntimeFlowgraph
|
|
12
|
+
from siliconcompiler.scheduler.schedulernode import SchedulerNode
|
|
13
|
+
from siliconcompiler.scheduler.slurm import SlurmSchedulerNode
|
|
14
|
+
from siliconcompiler.scheduler.docker import DockerSchedulerNode
|
|
15
|
+
from siliconcompiler.scheduler.taskscheduler import TaskScheduler
|
|
16
|
+
|
|
17
|
+
from siliconcompiler import utils
|
|
18
|
+
from siliconcompiler.scheduler import send_messages
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Scheduler:
|
|
22
|
+
def __init__(self, chip):
|
|
23
|
+
self.__chip = chip
|
|
24
|
+
self.__logger = self.__chip.logger
|
|
25
|
+
|
|
26
|
+
flow = self.__chip.get("option", "flow")
|
|
27
|
+
if not flow:
|
|
28
|
+
raise ValueError("flow must be specified")
|
|
29
|
+
|
|
30
|
+
if flow not in self.__chip.getkeys("flowgraph"):
|
|
31
|
+
raise ValueError("flow is not defined")
|
|
32
|
+
|
|
33
|
+
self.__flow = self.__chip.get("flowgraph", flow, field="schema")
|
|
34
|
+
from_steps = self.__chip.get('option', 'from')
|
|
35
|
+
to_steps = self.__chip.get('option', 'to')
|
|
36
|
+
prune_nodes = self.__chip.get('option', 'prune')
|
|
37
|
+
|
|
38
|
+
if not self.__flow.validate(logger=self.__logger):
|
|
39
|
+
raise ValueError(f"{self.__flow.name()} flowgraph contains errors and cannot be run.")
|
|
40
|
+
if not RuntimeFlowgraph.validate(
|
|
41
|
+
self.__flow,
|
|
42
|
+
from_steps=from_steps,
|
|
43
|
+
to_steps=to_steps,
|
|
44
|
+
prune_nodes=prune_nodes,
|
|
45
|
+
logger=chip.logger):
|
|
46
|
+
raise ValueError(f"{self.__flow.name()} flowgraph contains errors and cannot be run.")
|
|
47
|
+
|
|
48
|
+
self.__flow_runtime = RuntimeFlowgraph(
|
|
49
|
+
self.__flow,
|
|
50
|
+
from_steps=from_steps,
|
|
51
|
+
to_steps=to_steps,
|
|
52
|
+
prune_nodes=self.__chip.get('option', 'prune'))
|
|
53
|
+
|
|
54
|
+
self.__flow_runtime_no_prune = RuntimeFlowgraph(
|
|
55
|
+
self.__flow,
|
|
56
|
+
from_steps=from_steps,
|
|
57
|
+
to_steps=to_steps)
|
|
58
|
+
|
|
59
|
+
self.__flow_load_runtime = RuntimeFlowgraph(
|
|
60
|
+
self.__flow,
|
|
61
|
+
to_steps=from_steps,
|
|
62
|
+
prune_nodes=prune_nodes)
|
|
63
|
+
|
|
64
|
+
self.__flow_something = RuntimeFlowgraph(
|
|
65
|
+
self.__flow,
|
|
66
|
+
from_steps=set([step for step, _ in self.__flow.get_entry_nodes()]),
|
|
67
|
+
prune_nodes=prune_nodes)
|
|
68
|
+
|
|
69
|
+
self.__record = self.__chip.get("record", field="schema")
|
|
70
|
+
self.__metrics = self.__chip.get("metric", field="schema")
|
|
71
|
+
|
|
72
|
+
self.__tasks = {}
|
|
73
|
+
|
|
74
|
+
def __print_status(self, header):
|
|
75
|
+
self.__logger.debug(f"#### {header}")
|
|
76
|
+
for step, index in self.__flow.get_nodes():
|
|
77
|
+
self.__logger.debug(f"({step}, {index}) -> "
|
|
78
|
+
f"{self.__record.get('status', step=step, index=index)}")
|
|
79
|
+
self.__logger.debug("####")
|
|
80
|
+
|
|
81
|
+
def check_manifest(self):
|
|
82
|
+
self.__logger.info("Checking manifest before running.")
|
|
83
|
+
return self.__chip.check_manifest()
|
|
84
|
+
|
|
85
|
+
def run_core(self):
|
|
86
|
+
self.__record.record_python_packages()
|
|
87
|
+
|
|
88
|
+
task_scheduler = TaskScheduler(self.__chip, self.__tasks)
|
|
89
|
+
task_scheduler.run()
|
|
90
|
+
task_scheduler.check()
|
|
91
|
+
|
|
92
|
+
def run(self):
|
|
93
|
+
self.__run_setup()
|
|
94
|
+
self.configure_nodes()
|
|
95
|
+
|
|
96
|
+
# Check validity of setup
|
|
97
|
+
if not self.check_manifest():
|
|
98
|
+
raise RuntimeError("check_manifest() failed")
|
|
99
|
+
|
|
100
|
+
self.run_core()
|
|
101
|
+
|
|
102
|
+
# Store run in history
|
|
103
|
+
self.__chip.schema.record_history()
|
|
104
|
+
|
|
105
|
+
# Record final manifest
|
|
106
|
+
filepath = os.path.join(self.__chip.getworkdir(), f"{self.__chip.design}.pkg.json")
|
|
107
|
+
self.__chip.write_manifest(filepath)
|
|
108
|
+
|
|
109
|
+
send_messages.send(self.__chip, 'summary', None, None)
|
|
110
|
+
|
|
111
|
+
def __mark_pending(self, step, index):
|
|
112
|
+
if (step, index) not in self.__flow_runtime.get_nodes():
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
self.__record.set('status', NodeStatus.PENDING, step=step, index=index)
|
|
116
|
+
for next_step, next_index in self.__flow_runtime.get_nodes_starting_at(step, index):
|
|
117
|
+
if self.__record.get('status', step=next_step, index=next_index) == NodeStatus.SKIPPED:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# Mark following steps as pending
|
|
121
|
+
self.__record.set('status', NodeStatus.PENDING, step=next_step, index=next_index)
|
|
122
|
+
|
|
123
|
+
def __run_setup(self):
|
|
124
|
+
self.__check_display()
|
|
125
|
+
|
|
126
|
+
org_jobname = self.__chip.get('option', 'jobname')
|
|
127
|
+
copy_prev_job = self.__increment_job_name()
|
|
128
|
+
|
|
129
|
+
# Create tasks
|
|
130
|
+
copy_from_nodes = set(self.__flow_load_runtime.get_nodes()).difference(
|
|
131
|
+
self.__flow_runtime.get_entry_nodes())
|
|
132
|
+
for step, index in self.__flow.get_nodes():
|
|
133
|
+
node_cls = SchedulerNode
|
|
134
|
+
|
|
135
|
+
node_scheduler = self.__chip.get('option', 'scheduler', 'name', step=step, index=index)
|
|
136
|
+
if node_scheduler == 'slurm':
|
|
137
|
+
node_cls = SlurmSchedulerNode
|
|
138
|
+
elif node_scheduler == 'docker':
|
|
139
|
+
node_cls = DockerSchedulerNode
|
|
140
|
+
self.__tasks[(step, index)] = node_cls(self.__chip, step, index)
|
|
141
|
+
if self.__flow.get(step, index, "tool") == "builtin":
|
|
142
|
+
self.__tasks[(step, index)].set_builtin()
|
|
143
|
+
|
|
144
|
+
if copy_prev_job and (step, index) in copy_from_nodes:
|
|
145
|
+
self.__tasks[(step, index)].copy_from(org_jobname)
|
|
146
|
+
|
|
147
|
+
if copy_prev_job:
|
|
148
|
+
# Copy collection directory
|
|
149
|
+
copy_from = self.__chip._getcollectdir(jobname=org_jobname)
|
|
150
|
+
copy_to = self.__chip._getcollectdir()
|
|
151
|
+
if os.path.exists(copy_from):
|
|
152
|
+
shutil.copytree(copy_from, copy_to,
|
|
153
|
+
dirs_exist_ok=True,
|
|
154
|
+
copy_function=utils.link_copy)
|
|
155
|
+
|
|
156
|
+
self.__clean_build_dir()
|
|
157
|
+
self.__reset_flow_nodes()
|
|
158
|
+
|
|
159
|
+
def __reset_flow_nodes(self):
|
|
160
|
+
# Reset record
|
|
161
|
+
for step, index in self.__flow.get_nodes():
|
|
162
|
+
self.__record.clear(step, index, keep=['remoteid', 'status', 'pythonpackage'])
|
|
163
|
+
self.__record.set('status', NodeStatus.PENDING, step=step, index=index)
|
|
164
|
+
|
|
165
|
+
# Reset metrics
|
|
166
|
+
for step, index in self.__flow.get_nodes():
|
|
167
|
+
self.__metrics.clear(step, index)
|
|
168
|
+
|
|
169
|
+
def __clean_build_dir(self):
|
|
170
|
+
if self.__record.get('remoteid'):
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
if self.__chip.get('option', 'clean') and not self.__chip.get('option', 'from'):
|
|
174
|
+
# If no step or nodes to start from were specified, the whole flow is being run
|
|
175
|
+
# start-to-finish. Delete the build dir to clear stale results.
|
|
176
|
+
cur_job_dir = self.__chip.getworkdir()
|
|
177
|
+
if os.path.isdir(cur_job_dir):
|
|
178
|
+
shutil.rmtree(cur_job_dir)
|
|
179
|
+
|
|
180
|
+
def configure_nodes(self):
|
|
181
|
+
from_nodes = []
|
|
182
|
+
extra_setup_nodes = {}
|
|
183
|
+
|
|
184
|
+
journal = Journal.access(self.__chip.schema)
|
|
185
|
+
journal.start()
|
|
186
|
+
|
|
187
|
+
self.__print_status("Start")
|
|
188
|
+
|
|
189
|
+
if self.__chip.get('option', 'clean'):
|
|
190
|
+
if self.__chip.get("option", "from"):
|
|
191
|
+
from_nodes = self.__flow_runtime.get_entry_nodes()
|
|
192
|
+
load_nodes = self.__flow.get_nodes()
|
|
193
|
+
else:
|
|
194
|
+
if self.__chip.get("option", "from"):
|
|
195
|
+
from_nodes = self.__flow_runtime.get_entry_nodes()
|
|
196
|
+
load_nodes = self.__flow_load_runtime.get_nodes()
|
|
197
|
+
|
|
198
|
+
# Collect previous run information
|
|
199
|
+
for step, index in self.__flow.get_nodes():
|
|
200
|
+
if (step, index) not in load_nodes:
|
|
201
|
+
# Node not marked for loading
|
|
202
|
+
continue
|
|
203
|
+
if (step, index) in from_nodes:
|
|
204
|
+
# Node will be run so no need to load
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
manifest = os.path.join(self.__chip.getworkdir(step=step, index=index),
|
|
208
|
+
'outputs',
|
|
209
|
+
f'{self.__chip.design}.pkg.json')
|
|
210
|
+
if os.path.exists(manifest):
|
|
211
|
+
# ensure we setup these nodes again
|
|
212
|
+
try:
|
|
213
|
+
extra_setup_nodes[(step, index)] = Schema.from_manifest(filepath=manifest)
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
# Setup tools for all nodes to run
|
|
218
|
+
for layer_nodes in self.__flow.get_execution_order():
|
|
219
|
+
for step, index in layer_nodes:
|
|
220
|
+
with self.__tasks[(step, index)].runtime():
|
|
221
|
+
node_kept = self.__tasks[(step, index)].setup()
|
|
222
|
+
if not node_kept and (step, index) in extra_setup_nodes:
|
|
223
|
+
# remove from previous node data
|
|
224
|
+
del extra_setup_nodes[(step, index)]
|
|
225
|
+
|
|
226
|
+
if (step, index) in extra_setup_nodes:
|
|
227
|
+
schema = extra_setup_nodes[(step, index)]
|
|
228
|
+
node_status = None
|
|
229
|
+
try:
|
|
230
|
+
node_status = schema.get('record', 'status', step=step, index=index)
|
|
231
|
+
except: # noqa E722
|
|
232
|
+
pass
|
|
233
|
+
if node_status:
|
|
234
|
+
# Forward old status
|
|
235
|
+
self.__record.set('status', node_status, step=step, index=index)
|
|
236
|
+
|
|
237
|
+
self.__print_status("After setup")
|
|
238
|
+
|
|
239
|
+
# Check for modified information
|
|
240
|
+
for layer_nodes in self.__flow.get_execution_order():
|
|
241
|
+
for step, index in layer_nodes:
|
|
242
|
+
# Only look at successful nodes
|
|
243
|
+
if self.__record.get("status", step=step, index=index) != NodeStatus.SUCCESS:
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
with self.__tasks[(step, index)].runtime():
|
|
247
|
+
if self.__tasks[(step, index)].requires_run():
|
|
248
|
+
# This node must be run
|
|
249
|
+
self.__mark_pending(step, index)
|
|
250
|
+
elif (step, index) in extra_setup_nodes:
|
|
251
|
+
# import old information
|
|
252
|
+
Journal.access(extra_setup_nodes[(step, index)]).replay(self.__chip.schema)
|
|
253
|
+
|
|
254
|
+
self.__print_status("After requires run")
|
|
255
|
+
|
|
256
|
+
# Ensure all nodes are marked as pending if needed
|
|
257
|
+
for layer_nodes in self.__flow_runtime.get_execution_order():
|
|
258
|
+
for step, index in layer_nodes:
|
|
259
|
+
status = self.__record.get("status", step=step, index=index)
|
|
260
|
+
if NodeStatus.is_waiting(status) or NodeStatus.is_error(status):
|
|
261
|
+
self.__mark_pending(step, index)
|
|
262
|
+
|
|
263
|
+
self.__print_status("After ensure")
|
|
264
|
+
|
|
265
|
+
self.__chip.write_manifest(os.path.join(self.__chip.getworkdir(),
|
|
266
|
+
f"{self.__chip.get('design')}.pkg.json"))
|
|
267
|
+
journal.stop()
|
|
268
|
+
|
|
269
|
+
# Clean nodes marked pending
|
|
270
|
+
for step, index in self.__flow_runtime.get_nodes():
|
|
271
|
+
if NodeStatus.is_waiting(self.__record.get('status', step=step, index=index)):
|
|
272
|
+
with self.__tasks[(step, index)].runtime():
|
|
273
|
+
self.__tasks[(step, index)].clean_directory()
|
|
274
|
+
|
|
275
|
+
def __check_display(self):
|
|
276
|
+
'''
|
|
277
|
+
Automatically disable display for Linux systems without desktop environment
|
|
278
|
+
'''
|
|
279
|
+
|
|
280
|
+
if not self.__chip.get('option', 'nodisplay') and sys.platform == 'linux' \
|
|
281
|
+
and 'DISPLAY' not in os.environ and 'WAYLAND_DISPLAY' not in os.environ:
|
|
282
|
+
self.__logger.warning('Environment variable $DISPLAY or $WAYLAND_DISPLAY not set')
|
|
283
|
+
self.__logger.warning("Setting [option,nodisplay] to True")
|
|
284
|
+
self.__chip.set('option', 'nodisplay', True)
|
|
285
|
+
|
|
286
|
+
def __increment_job_name(self):
|
|
287
|
+
'''
|
|
288
|
+
Auto-update jobname if [option,jobincr] is True
|
|
289
|
+
Do this before initializing logger so that it picks up correct jobname
|
|
290
|
+
'''
|
|
291
|
+
|
|
292
|
+
if not self.__chip.get('option', 'clean'):
|
|
293
|
+
return False
|
|
294
|
+
if not self.__chip.get('option', 'jobincr'):
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
workdir = self.__chip.getworkdir()
|
|
298
|
+
if os.path.isdir(workdir):
|
|
299
|
+
# Strip off digits following jobname, if any
|
|
300
|
+
stem = self.__chip.get('option', 'jobname').rstrip('0123456789')
|
|
301
|
+
|
|
302
|
+
dir_check = re.compile(fr'{stem}(\d+)')
|
|
303
|
+
|
|
304
|
+
jobid = 0
|
|
305
|
+
for job in os.listdir(os.path.dirname(workdir)):
|
|
306
|
+
m = dir_check.match(job)
|
|
307
|
+
if m:
|
|
308
|
+
jobid = max(jobid, int(m.group(1)))
|
|
309
|
+
self.__chip.set('option', 'jobname', f'{stem}{jobid + 1}')
|
|
310
|
+
return True
|
|
311
|
+
return False
|