siliconcompiler 0.34.1__py3-none-any.whl → 0.34.3__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 +23 -4
- siliconcompiler/__main__.py +1 -7
- siliconcompiler/_metadata.py +1 -1
- siliconcompiler/apps/_common.py +104 -23
- siliconcompiler/apps/sc.py +4 -8
- siliconcompiler/apps/sc_dashboard.py +6 -4
- siliconcompiler/apps/sc_install.py +10 -6
- siliconcompiler/apps/sc_issue.py +7 -5
- siliconcompiler/apps/sc_remote.py +1 -1
- siliconcompiler/apps/sc_server.py +9 -14
- siliconcompiler/apps/sc_show.py +7 -6
- siliconcompiler/apps/smake.py +130 -94
- siliconcompiler/apps/utils/replay.py +4 -7
- siliconcompiler/apps/utils/summarize.py +3 -5
- siliconcompiler/asic.py +420 -0
- siliconcompiler/checklist.py +25 -2
- siliconcompiler/cmdlineschema.py +534 -0
- siliconcompiler/constraints/__init__.py +17 -0
- siliconcompiler/constraints/asic_component.py +378 -0
- siliconcompiler/constraints/asic_floorplan.py +449 -0
- siliconcompiler/constraints/asic_pins.py +489 -0
- siliconcompiler/constraints/asic_timing.py +517 -0
- siliconcompiler/core.py +10 -35
- siliconcompiler/data/templates/tcl/manifest.tcl.j2 +8 -0
- siliconcompiler/dependencyschema.py +96 -202
- siliconcompiler/design.py +327 -241
- siliconcompiler/filesetschema.py +250 -0
- siliconcompiler/flowgraph.py +298 -106
- siliconcompiler/fpga.py +124 -1
- siliconcompiler/library.py +331 -0
- siliconcompiler/metric.py +327 -92
- siliconcompiler/metrics/__init__.py +7 -0
- siliconcompiler/metrics/asic.py +245 -0
- siliconcompiler/metrics/fpga.py +220 -0
- siliconcompiler/package/__init__.py +391 -67
- siliconcompiler/package/git.py +92 -16
- siliconcompiler/package/github.py +114 -22
- siliconcompiler/package/https.py +79 -16
- siliconcompiler/packageschema.py +341 -16
- siliconcompiler/pathschema.py +255 -0
- siliconcompiler/pdk.py +566 -1
- siliconcompiler/project.py +1460 -0
- siliconcompiler/record.py +38 -1
- siliconcompiler/remote/__init__.py +5 -2
- siliconcompiler/remote/client.py +11 -6
- siliconcompiler/remote/schema.py +5 -23
- siliconcompiler/remote/server.py +41 -54
- siliconcompiler/report/__init__.py +3 -3
- siliconcompiler/report/dashboard/__init__.py +48 -14
- siliconcompiler/report/dashboard/cli/__init__.py +99 -21
- siliconcompiler/report/dashboard/cli/board.py +364 -179
- siliconcompiler/report/dashboard/web/__init__.py +90 -12
- siliconcompiler/report/dashboard/web/components/__init__.py +219 -240
- siliconcompiler/report/dashboard/web/components/flowgraph.py +49 -26
- siliconcompiler/report/dashboard/web/components/graph.py +139 -100
- siliconcompiler/report/dashboard/web/layouts/__init__.py +29 -1
- siliconcompiler/report/dashboard/web/layouts/_common.py +38 -2
- siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph.py +39 -26
- siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_node_tab.py +50 -50
- siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_sac_tabs.py +49 -46
- siliconcompiler/report/dashboard/web/state.py +141 -14
- siliconcompiler/report/dashboard/web/utils/__init__.py +79 -16
- siliconcompiler/report/dashboard/web/utils/file_utils.py +74 -11
- siliconcompiler/report/dashboard/web/viewer.py +25 -1
- siliconcompiler/report/report.py +5 -2
- siliconcompiler/report/summary_image.py +29 -11
- siliconcompiler/scheduler/__init__.py +9 -1
- siliconcompiler/scheduler/docker.py +81 -4
- siliconcompiler/scheduler/run_node.py +37 -20
- siliconcompiler/scheduler/scheduler.py +211 -36
- siliconcompiler/scheduler/schedulernode.py +394 -60
- siliconcompiler/scheduler/send_messages.py +77 -29
- siliconcompiler/scheduler/slurm.py +76 -12
- siliconcompiler/scheduler/taskscheduler.py +142 -21
- siliconcompiler/schema/__init__.py +0 -4
- siliconcompiler/schema/baseschema.py +338 -59
- siliconcompiler/schema/editableschema.py +14 -6
- siliconcompiler/schema/journal.py +28 -17
- siliconcompiler/schema/namedschema.py +22 -14
- siliconcompiler/schema/parameter.py +89 -28
- siliconcompiler/schema/parametertype.py +2 -0
- siliconcompiler/schema/parametervalue.py +258 -15
- siliconcompiler/schema/safeschema.py +25 -2
- siliconcompiler/schema/schema_cfg.py +23 -19
- siliconcompiler/schema/utils.py +2 -2
- siliconcompiler/schema_obj.py +24 -5
- siliconcompiler/tool.py +1131 -265
- siliconcompiler/tools/bambu/__init__.py +41 -0
- siliconcompiler/tools/builtin/concatenate.py +2 -2
- siliconcompiler/tools/builtin/minimum.py +2 -1
- siliconcompiler/tools/builtin/mux.py +2 -1
- siliconcompiler/tools/builtin/nop.py +2 -1
- siliconcompiler/tools/builtin/verify.py +2 -1
- siliconcompiler/tools/klayout/__init__.py +95 -0
- siliconcompiler/tools/openroad/__init__.py +289 -0
- siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +3 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_detailed_route.tcl +7 -2
- siliconcompiler/tools/openroad/scripts/apr/sc_global_route.tcl +8 -4
- siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +9 -5
- siliconcompiler/tools/openroad/scripts/common/write_images.tcl +5 -1
- siliconcompiler/tools/slang/__init__.py +1 -1
- siliconcompiler/tools/slang/elaborate.py +2 -1
- siliconcompiler/tools/vivado/scripts/sc_run.tcl +1 -1
- siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +8 -1
- siliconcompiler/tools/vivado/syn_fpga.py +6 -0
- siliconcompiler/tools/vivado/vivado.py +35 -2
- siliconcompiler/tools/vpr/__init__.py +150 -0
- siliconcompiler/tools/yosys/__init__.py +369 -1
- siliconcompiler/tools/yosys/scripts/procs.tcl +0 -1
- siliconcompiler/toolscripts/_tools.json +5 -10
- siliconcompiler/utils/__init__.py +66 -0
- siliconcompiler/utils/flowgraph.py +2 -2
- siliconcompiler/utils/issue.py +2 -1
- siliconcompiler/utils/logging.py +14 -0
- siliconcompiler/utils/multiprocessing.py +256 -0
- siliconcompiler/utils/showtools.py +10 -0
- {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/METADATA +6 -6
- {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/RECORD +122 -115
- {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/entry_points.txt +3 -0
- siliconcompiler/schema/cmdlineschema.py +0 -250
- siliconcompiler/schema/packageschema.py +0 -101
- siliconcompiler/toolscripts/rhel8/install-slang.sh +0 -40
- siliconcompiler/toolscripts/rhel9/install-slang.sh +0 -40
- siliconcompiler/toolscripts/ubuntu20/install-slang.sh +0 -47
- siliconcompiler/toolscripts/ubuntu22/install-slang.sh +0 -37
- siliconcompiler/toolscripts/ubuntu24/install-slang.sh +0 -37
- {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/WHEEL +0 -0
- {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/licenses/LICENSE +0 -0
- {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A module for sending email notifications about SiliconCompiler job events.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to send detailed email updates at various
|
|
5
|
+
stages of a compilation flow (e.g., on begin, failure, or a final summary).
|
|
6
|
+
It loads SMTP server credentials from a configuration file, constructs
|
|
7
|
+
HTML-formatted emails with relevant job data and attachments (logs, images),
|
|
8
|
+
and sends them to specified recipients.
|
|
9
|
+
"""
|
|
1
10
|
from siliconcompiler.utils import default_email_credentials_file, get_file_template
|
|
2
11
|
import smtplib
|
|
3
12
|
from email.mime.multipart import MIMEMultipart
|
|
@@ -23,6 +32,19 @@ with open(api_dir / 'email_credentials.json') as schema:
|
|
|
23
32
|
|
|
24
33
|
|
|
25
34
|
def __load_config(chip):
|
|
35
|
+
"""
|
|
36
|
+
Loads and validates email credentials from the default configuration file.
|
|
37
|
+
|
|
38
|
+
This function locates the email credentials JSON file, loads its content,
|
|
39
|
+
and validates it against a predefined JSON schema.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
chip (Chip): The Chip object, used for logging.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
dict: A dictionary containing the validated email credentials. Returns
|
|
46
|
+
an empty dictionary if the file is not found or is invalid.
|
|
47
|
+
"""
|
|
26
48
|
path = default_email_credentials_file()
|
|
27
49
|
if not os.path.exists(path):
|
|
28
50
|
chip.logger.warning(f'Email credentials are not available: {path}')
|
|
@@ -39,6 +61,23 @@ def __load_config(chip):
|
|
|
39
61
|
|
|
40
62
|
|
|
41
63
|
def send(chip, msg_type, step, index):
|
|
64
|
+
"""
|
|
65
|
+
Constructs and sends an email notification for a specific job event.
|
|
66
|
+
|
|
67
|
+
This function checks if a notification is required for the given event type
|
|
68
|
+
based on the chip's configuration. If so, it assembles an email with a
|
|
69
|
+
subject, HTML body, and relevant attachments (logs or images) and sends
|
|
70
|
+
it via the configured SMTP server.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
chip (Chip): The Chip object containing all run data and configuration.
|
|
74
|
+
msg_type (str): The type of event triggering the message (e.g., 'begin',
|
|
75
|
+
'fail', 'summary').
|
|
76
|
+
step (str): The step name associated with the event. Can be None for
|
|
77
|
+
global events.
|
|
78
|
+
index (str): The index associated with the event. Can be None for
|
|
79
|
+
global events.
|
|
80
|
+
"""
|
|
42
81
|
chip_step, chip_index = step, index
|
|
43
82
|
if step is None:
|
|
44
83
|
chip_step = Schema.GLOBAL_KEY
|
|
@@ -82,6 +121,7 @@ def send(chip, msg_type, step, index):
|
|
|
82
121
|
|
|
83
122
|
if cred["max_file_size"] > 0:
|
|
84
123
|
if msg_type == "summary":
|
|
124
|
+
# Handle summary message: attach layout image and metrics summary
|
|
85
125
|
layout_img = report_utils._find_summary_image(chip)
|
|
86
126
|
if layout_img and os.path.isfile(layout_img):
|
|
87
127
|
with open(layout_img, 'rb') as img_file:
|
|
@@ -109,6 +149,7 @@ def send(chip, msg_type, step, index):
|
|
|
109
149
|
metrics_unit=metrics_unit,
|
|
110
150
|
metric_keys=metrics_to_show)
|
|
111
151
|
else:
|
|
152
|
+
# Handle general node message: attach log files and node-specific data
|
|
112
153
|
# Attach logs
|
|
113
154
|
for log in (f'sc_{step}_{index}.log', f'{step}.log'):
|
|
114
155
|
log_file = f'{chip.getworkdir(step=step, index=index)}/{log}'
|
|
@@ -125,43 +166,48 @@ def send(chip, msg_type, step, index):
|
|
|
125
166
|
filename=f'{log_name}.txt')
|
|
126
167
|
msg.attach(log_attach)
|
|
127
168
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
# Collect records for the specific node
|
|
170
|
+
records = {}
|
|
171
|
+
for record in chip.getkeys('record'):
|
|
172
|
+
value = None
|
|
173
|
+
if chip.get('record', record, field='pernode').is_never():
|
|
174
|
+
value = chip.get('record', record)
|
|
175
|
+
else:
|
|
176
|
+
value = chip.get('record', record, step=step, index=index)
|
|
177
|
+
|
|
178
|
+
if value is not None:
|
|
179
|
+
records[record] = value
|
|
180
|
+
|
|
181
|
+
# Collect metrics for the specific node
|
|
182
|
+
nodes, errors, metrics, metrics_unit, metrics_to_show, _ = \
|
|
183
|
+
report_utils._collect_data(chip, flow=flow, flowgraph_nodes=[(step, index)])
|
|
184
|
+
|
|
185
|
+
status = chip.get('record', 'status', step=step, index=index)
|
|
186
|
+
|
|
187
|
+
# Render the general email template
|
|
188
|
+
text_msg = get_file_template('email/general.j2').render(
|
|
189
|
+
design=chip.design,
|
|
190
|
+
job=jobname,
|
|
191
|
+
step=step,
|
|
192
|
+
index=index,
|
|
193
|
+
status=status,
|
|
194
|
+
records=records,
|
|
195
|
+
nodes=nodes,
|
|
196
|
+
errors=errors,
|
|
197
|
+
metrics=metrics,
|
|
198
|
+
metrics_unit=metrics_unit,
|
|
199
|
+
metric_keys=metrics_to_show)
|
|
156
200
|
|
|
157
201
|
body = MIMEText(text_msg, 'html')
|
|
158
202
|
msg.attach(body)
|
|
159
203
|
|
|
204
|
+
# Determine whether to use SSL for the SMTP connection
|
|
160
205
|
if cred['ssl']:
|
|
161
206
|
smtp_use = smtplib.SMTP_SSL
|
|
162
207
|
else:
|
|
163
208
|
smtp_use = smtplib.SMTP
|
|
164
209
|
|
|
210
|
+
# Connect to the SMTP server and send the email
|
|
165
211
|
with smtp_use(cred["server"], cred["port"]) as smtp_server:
|
|
166
212
|
do_send = False
|
|
167
213
|
try:
|
|
@@ -180,10 +226,12 @@ def send(chip, msg_type, step, index):
|
|
|
180
226
|
|
|
181
227
|
|
|
182
228
|
if __name__ == "__main__":
|
|
229
|
+
# Example usage for testing the send function
|
|
183
230
|
from siliconcompiler import Chip
|
|
184
231
|
from siliconcompiler.targets import freepdk45_demo
|
|
185
232
|
chip = Chip('test')
|
|
186
233
|
chip.use(freepdk45_demo)
|
|
187
234
|
chip.set('option', 'scheduler', 'msgevent', 'ALL')
|
|
188
|
-
#
|
|
235
|
+
# To test, uncomment the following line and fill in a valid email address
|
|
236
|
+
# chip.set('option', 'scheduler', 'msgcontact', 'your.email@example.com')
|
|
189
237
|
send(chip, "BEGIN", "import", "0")
|
|
@@ -1,21 +1,37 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
3
|
import shlex
|
|
3
|
-
import
|
|
4
|
+
import shutil
|
|
4
5
|
import stat
|
|
6
|
+
import subprocess
|
|
5
7
|
import uuid
|
|
6
|
-
import json
|
|
7
|
-
import shutil
|
|
8
8
|
|
|
9
9
|
import os.path
|
|
10
10
|
|
|
11
11
|
from siliconcompiler import utils
|
|
12
12
|
from siliconcompiler.package import RemoteResolver
|
|
13
13
|
from siliconcompiler.flowgraph import RuntimeFlowgraph
|
|
14
|
-
from siliconcompiler.scheduler
|
|
14
|
+
from siliconcompiler.scheduler import SchedulerNode
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class SlurmSchedulerNode(SchedulerNode):
|
|
18
|
+
"""A SchedulerNode implementation for running tasks on a Slurm cluster.
|
|
19
|
+
|
|
20
|
+
This class extends the base SchedulerNode to handle the specifics of
|
|
21
|
+
submitting a compilation step as a job to a Slurm workload manager.
|
|
22
|
+
It prepares a run script, a manifest, and uses the 'srun' command
|
|
23
|
+
to execute the step on a compute node.
|
|
24
|
+
"""
|
|
25
|
+
|
|
18
26
|
def __init__(self, chip, step, index, replay=False):
|
|
27
|
+
"""Initializes a SlurmSchedulerNode.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
chip (Chip): The parent Chip object.
|
|
31
|
+
step (str): The step name in the flowgraph.
|
|
32
|
+
index (str): The index for the step.
|
|
33
|
+
replay (bool): If True, sets up the node to replay a previous run.
|
|
34
|
+
"""
|
|
19
35
|
super().__init__(chip, step, index, replay=replay)
|
|
20
36
|
|
|
21
37
|
# Get the temporary UID associated with this job run.
|
|
@@ -26,10 +42,22 @@ class SlurmSchedulerNode(SchedulerNode):
|
|
|
26
42
|
|
|
27
43
|
@property
|
|
28
44
|
def jobhash(self):
|
|
45
|
+
"""str: A unique hash identifying the entire job run."""
|
|
29
46
|
return self.__job_hash
|
|
30
47
|
|
|
31
48
|
@staticmethod
|
|
32
49
|
def init(chip):
|
|
50
|
+
"""
|
|
51
|
+
A static pre-processing hook for the Slurm scheduler.
|
|
52
|
+
|
|
53
|
+
This method checks if the compilation flow starts from an entry node.
|
|
54
|
+
If so, it calls `chip.collect()` to gather all necessary source files
|
|
55
|
+
into a central location before any remote jobs are submitted. This
|
|
56
|
+
ensures that compute nodes have access to all required source files.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
chip (Chip): The Chip object to perform pre-processing on.
|
|
60
|
+
"""
|
|
33
61
|
if os.path.exists(chip._getcollectdir()):
|
|
34
62
|
# nothing to do
|
|
35
63
|
return
|
|
@@ -53,26 +81,61 @@ class SlurmSchedulerNode(SchedulerNode):
|
|
|
53
81
|
|
|
54
82
|
@property
|
|
55
83
|
def is_local(self):
|
|
84
|
+
"""bool: Returns False, as this node executes on a remote cluster."""
|
|
56
85
|
return False
|
|
57
86
|
|
|
58
87
|
@staticmethod
|
|
59
88
|
def get_configuration_directory(chip):
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
"""Gets the directory for storing Slurm-related configuration files.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
chip (Chip): The Chip object.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
str: The path to the configuration directory.
|
|
96
|
+
"""
|
|
63
97
|
|
|
64
98
|
return os.path.join(chip.getworkdir(), 'sc_configs')
|
|
65
99
|
|
|
66
100
|
@staticmethod
|
|
67
101
|
def get_job_name(jobhash, step, index):
|
|
102
|
+
"""Generates a unique job name for a Slurm job.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
jobhash (str): The unique hash for the entire run.
|
|
106
|
+
step (str): The step name of the node.
|
|
107
|
+
index (str): The index of the node.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
str: A unique job name string.
|
|
111
|
+
"""
|
|
68
112
|
return f'{jobhash}_{step}_{index}'
|
|
69
113
|
|
|
70
114
|
@staticmethod
|
|
71
115
|
def get_runtime_file_name(jobhash, step, index, ext):
|
|
116
|
+
"""Generates a standardized filename for runtime files.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
jobhash (str): The unique hash for the entire run.
|
|
120
|
+
step (str): The step name of the node.
|
|
121
|
+
index (str): The index of the node.
|
|
122
|
+
ext (str): The file extension.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
str: A standardized filename.
|
|
126
|
+
"""
|
|
72
127
|
return f"{SlurmSchedulerNode.get_job_name(jobhash, step, index)}.{ext}"
|
|
73
128
|
|
|
74
129
|
@staticmethod
|
|
75
130
|
def get_slurm_partition():
|
|
131
|
+
"""Determines a default Slurm partition by querying the cluster.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
str: The name of the first available Slurm partition.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
RuntimeError: If the 'sinfo' command fails.
|
|
138
|
+
"""
|
|
76
139
|
partitions = subprocess.run(['sinfo', '--json'],
|
|
77
140
|
stdout=subprocess.PIPE,
|
|
78
141
|
stderr=subprocess.STDOUT)
|
|
@@ -86,12 +149,13 @@ class SlurmSchedulerNode(SchedulerNode):
|
|
|
86
149
|
return sinfo['nodes'][0]['partitions'][0]
|
|
87
150
|
|
|
88
151
|
def run(self):
|
|
89
|
-
|
|
90
|
-
|
|
152
|
+
"""
|
|
153
|
+
Runs the node's task as a job on a Slurm cluster.
|
|
91
154
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
155
|
+
This method prepares all necessary files (manifest, run script),
|
|
156
|
+
constructs an 'srun' command, and submits the job. It then blocks
|
|
157
|
+
until the job completes on the compute node.
|
|
158
|
+
"""
|
|
95
159
|
|
|
96
160
|
self._init_run_logger()
|
|
97
161
|
|
|
@@ -11,12 +11,21 @@ from siliconcompiler import SiliconCompilerError
|
|
|
11
11
|
from siliconcompiler import utils
|
|
12
12
|
from siliconcompiler.flowgraph import RuntimeFlowgraph
|
|
13
13
|
|
|
14
|
+
from siliconcompiler.package import Resolver
|
|
14
15
|
from siliconcompiler.schema import Journal
|
|
15
16
|
|
|
16
|
-
from siliconcompiler.utils.logging import SCBlankLoggerFormatter
|
|
17
|
+
from siliconcompiler.utils.logging import SCBlankLoggerFormatter, SCBlankColorlessLoggerFormatter
|
|
18
|
+
from siliconcompiler.utils.multiprocessing import MPManager
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class TaskScheduler:
|
|
22
|
+
"""A class for managing the execution of individual tasks in a flowgraph.
|
|
23
|
+
|
|
24
|
+
This class is responsible for the fine-grained scheduling of tasks,
|
|
25
|
+
handling multiprocessing, resource allocation (cores/threads), and
|
|
26
|
+
dependency checking. It operates on a set of pending tasks defined by the
|
|
27
|
+
main Scheduler and executes them in a loop until the flow is complete.
|
|
28
|
+
"""
|
|
20
29
|
__callbacks = {
|
|
21
30
|
"pre_run": lambda chip: None,
|
|
22
31
|
"pre_node": lambda chip, step, index: None,
|
|
@@ -26,11 +35,30 @@ class TaskScheduler:
|
|
|
26
35
|
|
|
27
36
|
@staticmethod
|
|
28
37
|
def register_callback(hook, func):
|
|
38
|
+
"""Registers a callback function to be executed at a specific hook point.
|
|
39
|
+
|
|
40
|
+
Valid hooks are 'pre_run', 'pre_node', 'post_node', and 'post_run'.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
hook (str): The name of the hook to register the callback for.
|
|
44
|
+
func (function): The function to be called. It should accept the
|
|
45
|
+
chip object and, for node hooks, the step and index as arguments.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If the specified hook is not valid.
|
|
49
|
+
"""
|
|
29
50
|
if hook not in TaskScheduler.__callbacks:
|
|
30
51
|
raise ValueError(f"{hook} is not a valid callback")
|
|
31
52
|
TaskScheduler.__callbacks[hook] = func
|
|
32
53
|
|
|
33
54
|
def __init__(self, chip, tasks):
|
|
55
|
+
"""Initializes the TaskScheduler.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
chip (Chip): The Chip object containing the configuration.
|
|
59
|
+
tasks (dict): A dictionary of SchedulerNode objects keyed by
|
|
60
|
+
(step, index) tuples.
|
|
61
|
+
"""
|
|
34
62
|
self.__chip = chip
|
|
35
63
|
self.__logger = self.__chip.logger
|
|
36
64
|
self.__logger_console_handler = self.__chip._logger_console
|
|
@@ -54,7 +82,7 @@ class TaskScheduler:
|
|
|
54
82
|
to_steps=self.__chip.get('option', 'to'),
|
|
55
83
|
prune_nodes=self.__chip.get('option', 'prune'))
|
|
56
84
|
|
|
57
|
-
self.__log_queue =
|
|
85
|
+
self.__log_queue = MPManager.get_manager().Queue()
|
|
58
86
|
|
|
59
87
|
self.__nodes = {}
|
|
60
88
|
self.__startTimes = {}
|
|
@@ -63,6 +91,16 @@ class TaskScheduler:
|
|
|
63
91
|
self.__create_nodes(tasks)
|
|
64
92
|
|
|
65
93
|
def __create_nodes(self, tasks):
|
|
94
|
+
"""
|
|
95
|
+
Private helper to prepare all pending tasks for execution.
|
|
96
|
+
|
|
97
|
+
This method iterates through the tasks identified by the main Scheduler,
|
|
98
|
+
creates a multiprocessing.Process for each one, and sets up pipes for
|
|
99
|
+
inter-process communication (primarily for logging and package resolution).
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
tasks (dict): A dictionary of SchedulerNode objects.
|
|
103
|
+
"""
|
|
66
104
|
runtime = RuntimeFlowgraph(
|
|
67
105
|
self.__flow,
|
|
68
106
|
from_steps=set([step for step, _ in self.__flow.get_entry_nodes()]),
|
|
@@ -74,25 +112,24 @@ class TaskScheduler:
|
|
|
74
112
|
if self.__record.get('status', step=step, index=index) != NodeStatus.PENDING:
|
|
75
113
|
continue
|
|
76
114
|
|
|
77
|
-
with tasks[(step, index)].runtime():
|
|
78
|
-
threads = tasks[(step, index)].threads
|
|
79
|
-
if not threads:
|
|
80
|
-
threads = self.__max_threads
|
|
81
|
-
threads = max(1, min(threads, self.__max_threads))
|
|
82
|
-
|
|
83
115
|
task = {
|
|
84
116
|
"name": f"{step}/{index}",
|
|
85
117
|
"inputs": runtime.get_node_inputs(step, index, record=self.__record),
|
|
86
118
|
"proc": None,
|
|
87
119
|
"parent_pipe": None,
|
|
88
|
-
"threads":
|
|
120
|
+
"threads": None,
|
|
89
121
|
"running": False,
|
|
90
|
-
"manifest":
|
|
91
|
-
'outputs',
|
|
92
|
-
f'{self.__chip.design}.pkg.json'),
|
|
122
|
+
"manifest": None,
|
|
93
123
|
"node": tasks[(step, index)]
|
|
94
124
|
}
|
|
95
125
|
|
|
126
|
+
with tasks[(step, index)].runtime():
|
|
127
|
+
threads = tasks[(step, index)].threads
|
|
128
|
+
task["manifest"] = tasks[(step, index)].get_manifest()
|
|
129
|
+
if not threads:
|
|
130
|
+
threads = self.__max_threads
|
|
131
|
+
task["threads"] = max(1, min(threads, self.__max_threads))
|
|
132
|
+
|
|
96
133
|
task["parent_pipe"], pipe = multiprocessing.Pipe()
|
|
97
134
|
task["node"].set_queue(pipe, self.__log_queue)
|
|
98
135
|
|
|
@@ -104,14 +141,29 @@ class TaskScheduler:
|
|
|
104
141
|
for init_func in init_funcs:
|
|
105
142
|
init_func(self.__chip)
|
|
106
143
|
|
|
107
|
-
def run(self):
|
|
144
|
+
def run(self, job_log_handler):
|
|
145
|
+
"""
|
|
146
|
+
The main entry point for the task scheduling loop.
|
|
147
|
+
|
|
148
|
+
This method sets up a listener to handle logs from child processes,
|
|
149
|
+
calls the 'pre_run' callback, enters the main execution loop, and
|
|
150
|
+
handles cleanup and the 'post_run' callback.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
job_log_handler (logging.FileHandler): The handler for the main job log file.
|
|
154
|
+
"""
|
|
108
155
|
# Call this in case this was invoked without __main__
|
|
109
156
|
multiprocessing.freeze_support()
|
|
110
157
|
|
|
111
158
|
# Handle logs across threads
|
|
112
|
-
log_listener = QueueListener(self.__log_queue, self.__logger_console_handler
|
|
159
|
+
log_listener = QueueListener(self.__log_queue, self.__logger_console_handler,
|
|
160
|
+
job_log_handler)
|
|
113
161
|
console_format = self.__logger_console_handler.formatter
|
|
162
|
+
file_formatter = job_log_handler.formatter
|
|
114
163
|
self.__logger_console_handler.setFormatter(SCBlankLoggerFormatter())
|
|
164
|
+
job_log_handler.setFormatter(SCBlankColorlessLoggerFormatter())
|
|
165
|
+
self.__logger.removeHandler(job_log_handler)
|
|
166
|
+
|
|
115
167
|
log_listener.start()
|
|
116
168
|
|
|
117
169
|
# Update dashboard before run begins
|
|
@@ -122,18 +174,30 @@ class TaskScheduler:
|
|
|
122
174
|
|
|
123
175
|
try:
|
|
124
176
|
self.__run_loop()
|
|
177
|
+
TaskScheduler.__callbacks["post_run"](self.__chip)
|
|
125
178
|
except KeyboardInterrupt:
|
|
126
179
|
# exit immediately
|
|
127
180
|
log_listener.stop()
|
|
128
181
|
sys.exit(0)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
182
|
+
finally:
|
|
183
|
+
# Cleanup logger
|
|
184
|
+
try:
|
|
185
|
+
log_listener.stop()
|
|
186
|
+
except AttributeError:
|
|
187
|
+
# Logger already stopped
|
|
188
|
+
pass
|
|
189
|
+
self.__logger_console_handler.setFormatter(console_format)
|
|
190
|
+
job_log_handler.setFormatter(file_formatter)
|
|
191
|
+
self.__logger.addHandler(job_log_handler)
|
|
135
192
|
|
|
136
193
|
def __run_loop(self):
|
|
194
|
+
"""
|
|
195
|
+
The core execution loop of the scheduler.
|
|
196
|
+
|
|
197
|
+
This loop continues as long as there are nodes running or waiting to
|
|
198
|
+
run. In each iteration, it processes completed nodes and launches new
|
|
199
|
+
ones whose dependencies have been met.
|
|
200
|
+
"""
|
|
137
201
|
self.__startTimes = {None: time.time()}
|
|
138
202
|
|
|
139
203
|
while len(self.get_nodes_waiting_to_run()) > 0 or len(self.get_running_nodes()) > 0:
|
|
@@ -163,9 +227,19 @@ class TaskScheduler:
|
|
|
163
227
|
self.__nodes[running_nodes[0]]["proc"].join(timeout=self.__dwellTime)
|
|
164
228
|
|
|
165
229
|
def get_nodes(self):
|
|
230
|
+
"""Gets a sorted list of all nodes managed by this scheduler.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
list: A list of (step, index) tuples for all nodes.
|
|
234
|
+
"""
|
|
166
235
|
return sorted(self.__nodes.keys())
|
|
167
236
|
|
|
168
237
|
def get_running_nodes(self):
|
|
238
|
+
"""Gets a sorted list of all nodes that are currently running.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
list: A list of (step, index) tuples for running nodes.
|
|
242
|
+
"""
|
|
169
243
|
nodes = []
|
|
170
244
|
for node, info in self.__nodes.items():
|
|
171
245
|
if info["running"]:
|
|
@@ -173,6 +247,11 @@ class TaskScheduler:
|
|
|
173
247
|
return sorted(nodes)
|
|
174
248
|
|
|
175
249
|
def get_nodes_waiting_to_run(self):
|
|
250
|
+
"""Gets a sorted list of all nodes that are pending execution.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
list: A list of (step, index) tuples for pending nodes.
|
|
254
|
+
"""
|
|
176
255
|
nodes = []
|
|
177
256
|
for node, info in self.__nodes.items():
|
|
178
257
|
if not info["running"] and info["proc"]:
|
|
@@ -180,6 +259,17 @@ class TaskScheduler:
|
|
|
180
259
|
return sorted(nodes)
|
|
181
260
|
|
|
182
261
|
def __process_completed_nodes(self):
|
|
262
|
+
"""
|
|
263
|
+
Private helper to check for and process completed nodes.
|
|
264
|
+
|
|
265
|
+
This method iterates through running nodes, checks if their process has
|
|
266
|
+
terminated, and if so, merges their results (manifest and package cache)
|
|
267
|
+
back into the main chip object. It updates the node's status based on
|
|
268
|
+
the process exit code.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
bool: True if any node's status changed, False otherwise.
|
|
272
|
+
"""
|
|
183
273
|
changed = False
|
|
184
274
|
for node in self.get_running_nodes():
|
|
185
275
|
info = self.__nodes[node]
|
|
@@ -200,7 +290,7 @@ class TaskScheduler:
|
|
|
200
290
|
packages = info["parent_pipe"].recv()
|
|
201
291
|
if isinstance(packages, dict):
|
|
202
292
|
for package, path in packages.items():
|
|
203
|
-
self.__chip
|
|
293
|
+
Resolver.set_cache(self.__chip, package, path)
|
|
204
294
|
except: # noqa E722
|
|
205
295
|
pass
|
|
206
296
|
|
|
@@ -224,6 +314,18 @@ class TaskScheduler:
|
|
|
224
314
|
return changed
|
|
225
315
|
|
|
226
316
|
def __allow_start(self, node):
|
|
317
|
+
"""
|
|
318
|
+
Private helper to check if a node is allowed to start based on resources.
|
|
319
|
+
|
|
320
|
+
This method checks if launching a new node would exceed the configured
|
|
321
|
+
maximum number of parallel jobs or the total available CPU cores.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
node (tuple): The (step, index) of the node to check.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
bool: True if the node can be launched, False otherwise.
|
|
328
|
+
"""
|
|
227
329
|
info = self.__nodes[node]
|
|
228
330
|
|
|
229
331
|
if not info["node"].is_local:
|
|
@@ -246,6 +348,16 @@ class TaskScheduler:
|
|
|
246
348
|
return True
|
|
247
349
|
|
|
248
350
|
def __lanuch_nodes(self):
|
|
351
|
+
"""
|
|
352
|
+
Private helper to launch new nodes whose dependencies are met.
|
|
353
|
+
|
|
354
|
+
This method iterates through pending nodes, checks if all their input
|
|
355
|
+
nodes have completed successfully, and if system resources are available.
|
|
356
|
+
If all conditions are met, it starts the node's process.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
bool: True if any new node was launched, False otherwise.
|
|
360
|
+
"""
|
|
249
361
|
changed = False
|
|
250
362
|
for node in self.get_nodes_waiting_to_run():
|
|
251
363
|
# TODO: breakpoint logic:
|
|
@@ -297,6 +409,15 @@ class TaskScheduler:
|
|
|
297
409
|
return changed
|
|
298
410
|
|
|
299
411
|
def check(self):
|
|
412
|
+
"""
|
|
413
|
+
Checks if the flow completed successfully.
|
|
414
|
+
|
|
415
|
+
This method verifies that all nodes designated as exit points in the
|
|
416
|
+
flowgraph have been successfully completed.
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
RuntimeError: If any final steps in the flow were not reached.
|
|
420
|
+
"""
|
|
300
421
|
exit_steps = set([step for step, _ in self.__runtime_flow.get_exit_nodes()])
|
|
301
422
|
completed_steps = set([step for step, _ in
|
|
302
423
|
self.__runtime_flow.get_completed_nodes(record=self.__record)])
|
|
@@ -3,9 +3,7 @@ from .journal import Journal
|
|
|
3
3
|
from .safeschema import SafeSchema
|
|
4
4
|
from .editableschema import EditableSchema
|
|
5
5
|
from .baseschema import BaseSchema
|
|
6
|
-
from .cmdlineschema import CommandLineSchema
|
|
7
6
|
from .namedschema import NamedSchema
|
|
8
|
-
from .packageschema import PackageSchema
|
|
9
7
|
|
|
10
8
|
from .schema_cfg import SCHEMA_VERSION
|
|
11
9
|
|
|
@@ -14,9 +12,7 @@ __all__ = [
|
|
|
14
12
|
"BaseSchema",
|
|
15
13
|
"SafeSchema",
|
|
16
14
|
"EditableSchema",
|
|
17
|
-
"CommandLineSchema",
|
|
18
15
|
"NamedSchema",
|
|
19
|
-
"PackageSchema",
|
|
20
16
|
"Parameter",
|
|
21
17
|
"Scope",
|
|
22
18
|
"PerNode",
|