cuopt-server-cu12 26.8.0__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.
- cuopt_server/GIT_COMMIT +1 -0
- cuopt_server/VERSION +1 -0
- cuopt_server/__init__.py +9 -0
- cuopt_server/_version.py +14 -0
- cuopt_server/cuopt_service.py +446 -0
- cuopt_server/tests/__init__.py +2 -0
- cuopt_server/tests/test_bill_logging.py +128 -0
- cuopt_server/tests/test_cuopt_amr_service.py +26 -0
- cuopt_server/tests/test_file_path_validation.py +97 -0
- cuopt_server/tests/test_grpc_server_entry_point.py +52 -0
- cuopt_server/tests/test_incumbents.py +88 -0
- cuopt_server/tests/test_initial_solutions.py +40 -0
- cuopt_server/tests/test_job_abort.py +230 -0
- cuopt_server/tests/test_job_cache.py +103 -0
- cuopt_server/tests/test_lp.py +241 -0
- cuopt_server/tests/test_multi_cost.py +97 -0
- cuopt_server/tests/test_pdlp_warmstart.py +90 -0
- cuopt_server/tests/test_server.py +1364 -0
- cuopt_server/tests/test_server_health.py +63 -0
- cuopt_server/tests/test_set_cost_matrix.py +115 -0
- cuopt_server/tests/test_set_cost_waypoint_graph.py +128 -0
- cuopt_server/tests/test_set_fleet_data.py +582 -0
- cuopt_server/tests/test_set_solver_config.py +113 -0
- cuopt_server/tests/test_set_task_data.py +293 -0
- cuopt_server/tests/test_set_travel_time_waypoint_graph.py +111 -0
- cuopt_server/tests/test_solver_logging.py +59 -0
- cuopt_server/tests/utils/__init__.py +2 -0
- cuopt_server/tests/utils/sdk_tester.py +18 -0
- cuopt_server/tests/utils/utils.py +452 -0
- cuopt_server/utils/__init__.py +4 -0
- cuopt_server/utils/billing_data.py +94 -0
- cuopt_server/utils/data_definition.py +374 -0
- cuopt_server/utils/exceptions.py +79 -0
- cuopt_server/utils/health_check.py +48 -0
- cuopt_server/utils/job_queue.py +1760 -0
- cuopt_server/utils/linear_programming/__init__.py +2 -0
- cuopt_server/utils/linear_programming/data_definition.py +997 -0
- cuopt_server/utils/linear_programming/data_validation.py +129 -0
- cuopt_server/utils/linear_programming/generate_metadata.py +2 -0
- cuopt_server/utils/linear_programming/solver.py +412 -0
- cuopt_server/utils/logutil.py +65 -0
- cuopt_server/utils/mock_store.py +76 -0
- cuopt_server/utils/process_handler.py +142 -0
- cuopt_server/utils/request_filter.py +163 -0
- cuopt_server/utils/response_util.py +39 -0
- cuopt_server/utils/result_store.py +60 -0
- cuopt_server/utils/routing/__init__.py +2 -0
- cuopt_server/utils/routing/data_definition.py +1055 -0
- cuopt_server/utils/routing/generate_metadata.py +113 -0
- cuopt_server/utils/routing/initial_solution.py +42 -0
- cuopt_server/utils/routing/optimization_data_model.py +1048 -0
- cuopt_server/utils/routing/solver.py +571 -0
- cuopt_server/utils/routing/validation_cost_matrix.py +53 -0
- cuopt_server/utils/routing/validation_fleet_data.py +235 -0
- cuopt_server/utils/routing/validation_solver_config.py +29 -0
- cuopt_server/utils/routing/validation_task_data.py +158 -0
- cuopt_server/utils/routing/validation_waypoint_graph.py +96 -0
- cuopt_server/utils/sema.py +38 -0
- cuopt_server/utils/settings.py +64 -0
- cuopt_server/utils/solver.py +506 -0
- cuopt_server/utils/utils.py +87 -0
- cuopt_server/webserver.py +1651 -0
- cuopt_server_cu12-26.8.0.dist-info/METADATA +40 -0
- cuopt_server_cu12-26.8.0.dist-info/RECORD +67 -0
- cuopt_server_cu12-26.8.0.dist-info/WHEEL +5 -0
- cuopt_server_cu12-26.8.0.dist-info/licenses/LICENSE +201 -0
- cuopt_server_cu12-26.8.0.dist-info/top_level.txt +1 -0
cuopt_server/GIT_COMMIT
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
400863c1a92914bd47f5d68245f9a00e2d0d2df4
|
cuopt_server/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
26.08.00
|
cuopt_server/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from cuopt_server import cuopt_service
|
|
5
|
+
from cuopt_server._version import (
|
|
6
|
+
__git_commit__,
|
|
7
|
+
__version__,
|
|
8
|
+
__version_major_minor__,
|
|
9
|
+
)
|
cuopt_server/_version.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import importlib.resources
|
|
5
|
+
|
|
6
|
+
__version__ = (
|
|
7
|
+
importlib.resources.files("cuopt_server")
|
|
8
|
+
.joinpath("VERSION")
|
|
9
|
+
.read_text()
|
|
10
|
+
.strip()
|
|
11
|
+
)
|
|
12
|
+
__git_commit__ = ""
|
|
13
|
+
|
|
14
|
+
__version_major_minor__ = ".".join(__version__.split(".")[:2])
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
from multiprocessing import get_context
|
|
10
|
+
|
|
11
|
+
import psutil
|
|
12
|
+
|
|
13
|
+
import cuopt_server.utils.process_handler as process_handler
|
|
14
|
+
import cuopt_server.utils.request_filter as request_filter
|
|
15
|
+
import cuopt_server.utils.settings as settings
|
|
16
|
+
from cuopt_server._version import __version__
|
|
17
|
+
from cuopt_server.utils.job_queue import create_abort_list
|
|
18
|
+
from cuopt_server.utils.logutil import (
|
|
19
|
+
get_ncaid,
|
|
20
|
+
get_requestid,
|
|
21
|
+
get_solverid,
|
|
22
|
+
message_init,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
log_fmt = "%(ncaid)s%(requestid)s%(asctime)s.%(msecs)03d %(levelname)s %(message)s%(solverid)s" # noqa
|
|
26
|
+
date_fmt = "%Y-%m-%d %H:%M:%S"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def watcher(app_exit, results_queue, job_queue, abort_queue, abort_list):
|
|
30
|
+
try:
|
|
31
|
+
process_handler.watch_solvers(
|
|
32
|
+
app_exit, job_queue, results_queue, abort_queue, abort_list
|
|
33
|
+
)
|
|
34
|
+
logging.info("app.exit set, watcher exiting")
|
|
35
|
+
except Exception:
|
|
36
|
+
logging.info("exception, watcher exiting")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
import subprocess
|
|
41
|
+
|
|
42
|
+
# Do this before setting the signal handler,
|
|
43
|
+
# because sigchld from the subprocess will terminate us :)
|
|
44
|
+
try:
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["nvidia-smi", "-L"], capture_output=True, text=True
|
|
47
|
+
)
|
|
48
|
+
gpu_string = [r for r in result.stdout.split("\n") if r]
|
|
49
|
+
|
|
50
|
+
# extract GPU id number from each line (looks like "GPU 0: ..."
|
|
51
|
+
gpu_ids = [s.split(" ")[1][0:-1] for s in gpu_string]
|
|
52
|
+
|
|
53
|
+
# if for some reason the parse failed (nvidia-smi output changed)
|
|
54
|
+
gpu_ids = [s for s in gpu_ids if s.isnumeric()]
|
|
55
|
+
except Exception:
|
|
56
|
+
gpu_string = None
|
|
57
|
+
gpu_ids = []
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
cuda_string = None
|
|
61
|
+
result = subprocess.run(["nvidia-smi"], capture_output=True, text=True)
|
|
62
|
+
for r in result.stdout.split("\n"):
|
|
63
|
+
if "CUDA Version" in r:
|
|
64
|
+
cuda_string = r
|
|
65
|
+
break
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
ctx = get_context("fork")
|
|
70
|
+
|
|
71
|
+
# Flag for this process that says we have already run the
|
|
72
|
+
# exit handler
|
|
73
|
+
terminated = ctx.Event()
|
|
74
|
+
|
|
75
|
+
# Flag for all processes that the app is shutting down
|
|
76
|
+
app_exit = ctx.Event()
|
|
77
|
+
|
|
78
|
+
# Flag set by results thread when all jobs have been
|
|
79
|
+
# marked done, to give a chance for anyone actively
|
|
80
|
+
# waiting to get a graceful response
|
|
81
|
+
jobs_marked_done = ctx.Event()
|
|
82
|
+
|
|
83
|
+
job_queue = ctx.Queue()
|
|
84
|
+
abort_queue = ctx.Queue()
|
|
85
|
+
results_queue = ctx.Queue()
|
|
86
|
+
|
|
87
|
+
w = None
|
|
88
|
+
|
|
89
|
+
def handle_exit(signame, frame):
|
|
90
|
+
if terminated.is_set():
|
|
91
|
+
return
|
|
92
|
+
if signame:
|
|
93
|
+
logging.info(f"cuopt received signal {signame}")
|
|
94
|
+
if signame == 17:
|
|
95
|
+
return
|
|
96
|
+
terminated.set()
|
|
97
|
+
app_exit.set()
|
|
98
|
+
process_handler.terminate(
|
|
99
|
+
job_queue, results_queue, abort_queue, signame
|
|
100
|
+
)
|
|
101
|
+
logging.info("handle_exit complete")
|
|
102
|
+
|
|
103
|
+
signal.signal(signal.SIGINT, handle_exit)
|
|
104
|
+
signal.signal(signal.SIGTERM, handle_exit)
|
|
105
|
+
signal.signal(signal.SIGCHLD, handle_exit)
|
|
106
|
+
|
|
107
|
+
# Allow defaults to be overridden from the environment.
|
|
108
|
+
# Commandline arguments have the highest precedence.
|
|
109
|
+
# These values all have commandline analogs below.
|
|
110
|
+
ip = os.environ.get("CUOPT_SERVER_IP", "0.0.0.0")
|
|
111
|
+
port = os.environ.get("CUOPT_SERVER_PORT", 5000)
|
|
112
|
+
log_level = os.environ.get("CUOPT_SERVER_LOG_LEVEL", "debug")
|
|
113
|
+
log_file = os.environ.get("CUOPT_SERVER_LOG_FILE", "")
|
|
114
|
+
log_max = os.environ.get("CUOPT_SERVER_LOG_MAX", 10485760)
|
|
115
|
+
log_backup = os.environ.get("CUOPT_SERVER_LOG_BACKUP", 10)
|
|
116
|
+
tier = os.environ.get("CUOPT_SERVER_TIER", "managed_default")
|
|
117
|
+
datadir = os.environ.get("CUOPT_DATA_DIR", "")
|
|
118
|
+
resultdir = os.environ.get("CUOPT_RESULT_DIR", "")
|
|
119
|
+
maxresult = os.environ.get("CUOPT_MAX_RESULT", 250)
|
|
120
|
+
resultmode = os.environ.get("CUOPT_RESULT_MODE", "644")
|
|
121
|
+
ssl_certfile = os.environ.get("CUOPT_SSL_CERTFILE", "")
|
|
122
|
+
ssl_keyfile = os.environ.get("CUOPT_SSL_KEYFILE", "")
|
|
123
|
+
gpu_count = os.environ.get("CUOPT_GPU_COUNT", 1)
|
|
124
|
+
|
|
125
|
+
# This setting is experimental, and so is only settable via an
|
|
126
|
+
# env var. This setting allows multiple solver processes per GPU
|
|
127
|
+
# to be allocated, so that problems may be solved in parallel on
|
|
128
|
+
# the same GPU. Additional work may be needed on the best way to
|
|
129
|
+
# set up rmm in this case.
|
|
130
|
+
procs_per_gpu = os.environ.get("CUOPT_PROCS_PER_GPU", 1)
|
|
131
|
+
|
|
132
|
+
if not isinstance(procs_per_gpu, int):
|
|
133
|
+
raise ValueError(
|
|
134
|
+
"Process per GPU should be an integer value, "
|
|
135
|
+
f"current value is of type : {type(procs_per_gpu)}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
levels = {
|
|
139
|
+
"critical": logging.CRITICAL,
|
|
140
|
+
"error": logging.ERROR,
|
|
141
|
+
"warning": logging.WARNING,
|
|
142
|
+
"info": logging.INFO,
|
|
143
|
+
"debug": logging.DEBUG,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
parser = argparse.ArgumentParser()
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"-i",
|
|
149
|
+
"--ip",
|
|
150
|
+
type=str,
|
|
151
|
+
help=(
|
|
152
|
+
"IP to the server, also can be set by environment "
|
|
153
|
+
"variable CUOPT_SERVER_IP"
|
|
154
|
+
),
|
|
155
|
+
default=ip,
|
|
156
|
+
)
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"-p",
|
|
159
|
+
"--port",
|
|
160
|
+
type=int,
|
|
161
|
+
help=(
|
|
162
|
+
"Port of the server, also can be set by environment "
|
|
163
|
+
"variable CUOPT_SERVER_PORT"
|
|
164
|
+
),
|
|
165
|
+
default=port,
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"-d",
|
|
169
|
+
"--datadir",
|
|
170
|
+
type=str,
|
|
171
|
+
help=(
|
|
172
|
+
"Data directory used to optionally pass cuopt problem data files "
|
|
173
|
+
"to /cuopt/request endpoint via the filesystem instead of over "
|
|
174
|
+
"http, also can be set by environment variable CUOPT_DATA_DIR"
|
|
175
|
+
),
|
|
176
|
+
default=datadir,
|
|
177
|
+
)
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"-r",
|
|
180
|
+
"--resultdir",
|
|
181
|
+
type=str,
|
|
182
|
+
help=(
|
|
183
|
+
"Output directory used to optionally pass cuopt result files from "
|
|
184
|
+
"/cuopt/request endpoint via the filesystem instead of over http, "
|
|
185
|
+
"also can be set by environment variable CUOPT_RESULT_DIR"
|
|
186
|
+
),
|
|
187
|
+
default=resultdir,
|
|
188
|
+
)
|
|
189
|
+
parser.add_argument(
|
|
190
|
+
"-mr",
|
|
191
|
+
"--maxresult",
|
|
192
|
+
type=int,
|
|
193
|
+
help=(
|
|
194
|
+
"Maximum size (kilobytes) of a result returned over http from "
|
|
195
|
+
"/cuopt/request endpoint when RESULTDIR is set. Set to 0 to have "
|
|
196
|
+
"all results written to RESULTDIR (default 250), also can be "
|
|
197
|
+
"set by environment variable CUOPT_MAX_RESULT"
|
|
198
|
+
),
|
|
199
|
+
default=maxresult,
|
|
200
|
+
)
|
|
201
|
+
parser.add_argument(
|
|
202
|
+
"-mo",
|
|
203
|
+
"--mode",
|
|
204
|
+
type=str,
|
|
205
|
+
help=(
|
|
206
|
+
"Mode (octal) of result files from /cuopt/request endpoint "
|
|
207
|
+
"(default is 644), also can be set by environment variable "
|
|
208
|
+
"CUOPT_RESULT_MODE"
|
|
209
|
+
),
|
|
210
|
+
default=resultmode,
|
|
211
|
+
)
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
"-l",
|
|
214
|
+
"--log-level",
|
|
215
|
+
type=str,
|
|
216
|
+
choices=list(levels.keys()),
|
|
217
|
+
help=(
|
|
218
|
+
"Log level, also can be set by environment variable "
|
|
219
|
+
"CUOPT_SERVER_LOG_LEVEL"
|
|
220
|
+
),
|
|
221
|
+
default=log_level,
|
|
222
|
+
)
|
|
223
|
+
parser.add_argument(
|
|
224
|
+
"-f",
|
|
225
|
+
"--log-file",
|
|
226
|
+
type=str,
|
|
227
|
+
help=(
|
|
228
|
+
"Log filename (by default logs to stdout), also can be set by "
|
|
229
|
+
"environment variable CUOPT_SERVER_LOG_FILE"
|
|
230
|
+
),
|
|
231
|
+
default=log_file,
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"-lm",
|
|
235
|
+
"--log-max",
|
|
236
|
+
type=int,
|
|
237
|
+
help=(
|
|
238
|
+
"Max bytes for a log file, also can be set by environment "
|
|
239
|
+
"variable CUOPT_SERVER_LOG_MAX"
|
|
240
|
+
),
|
|
241
|
+
default=log_max,
|
|
242
|
+
)
|
|
243
|
+
parser.add_argument(
|
|
244
|
+
"-b",
|
|
245
|
+
"--log-backup",
|
|
246
|
+
type=int,
|
|
247
|
+
help=(
|
|
248
|
+
"Number of backup log files, also can be set by environment "
|
|
249
|
+
"variable CUOPT_SERVER_LOG_BACKUP"
|
|
250
|
+
),
|
|
251
|
+
default=log_backup,
|
|
252
|
+
)
|
|
253
|
+
parser.add_argument(
|
|
254
|
+
"--ssl-certfile",
|
|
255
|
+
type=str,
|
|
256
|
+
help=(
|
|
257
|
+
"SSL certificate file with complete path, also can be set by "
|
|
258
|
+
"environment variable CUOPT_SSL_CERTFILE"
|
|
259
|
+
),
|
|
260
|
+
default=ssl_certfile,
|
|
261
|
+
)
|
|
262
|
+
parser.add_argument(
|
|
263
|
+
"--ssl-keyfile",
|
|
264
|
+
type=str,
|
|
265
|
+
help=(
|
|
266
|
+
"SSL key file with complete path, also can be set by environment "
|
|
267
|
+
"variable CUOPT_SSL_KEYFILE"
|
|
268
|
+
),
|
|
269
|
+
default=ssl_keyfile,
|
|
270
|
+
)
|
|
271
|
+
parser.add_argument(
|
|
272
|
+
"-g",
|
|
273
|
+
"--gpu-count",
|
|
274
|
+
type=int,
|
|
275
|
+
help=(
|
|
276
|
+
"Number of available GPUs to use for solver processes "
|
|
277
|
+
"(the available GPUs can be affected by arguments to 'docker' "
|
|
278
|
+
"when using a container and by the CUDA_VISIBLE_DEVICES "
|
|
279
|
+
"environment variable). If more GPUs are requested than are "
|
|
280
|
+
"available, cuopt will use all available. Default is 1, zero "
|
|
281
|
+
"will be ignored, also can be set by environment variable "
|
|
282
|
+
"CUOPT_GPU_COUNT"
|
|
283
|
+
),
|
|
284
|
+
default=gpu_count,
|
|
285
|
+
)
|
|
286
|
+
parser.add_argument(
|
|
287
|
+
"-t",
|
|
288
|
+
"--tier",
|
|
289
|
+
type=str,
|
|
290
|
+
choices=request_filter.tier_list(),
|
|
291
|
+
help=(
|
|
292
|
+
"Feature tier for /cuopt/cuopt endpoint. This names a feature "
|
|
293
|
+
"tier defined in request_filter.py which restricts allowed "
|
|
294
|
+
"features or values for datasets in a request. Not currently "
|
|
295
|
+
"implemented for the /cuopt/request endpoint. "
|
|
296
|
+
"See request_filter.py for details. "
|
|
297
|
+
"Also can be set by environment variable CUOPT_TIER"
|
|
298
|
+
),
|
|
299
|
+
default=tier,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
args = parser.parse_args()
|
|
303
|
+
if args.log_file:
|
|
304
|
+
if args.log_backup < 1:
|
|
305
|
+
parser.error(
|
|
306
|
+
"Minimum number of backup log files is 1, also can be set by "
|
|
307
|
+
"environment variable CUOPT_SERVER_LOG_BACKUP"
|
|
308
|
+
)
|
|
309
|
+
handlers = [
|
|
310
|
+
logging.handlers.RotatingFileHandler(
|
|
311
|
+
args.log_file, args.log_max, args.log_backup
|
|
312
|
+
)
|
|
313
|
+
]
|
|
314
|
+
else:
|
|
315
|
+
handlers = [logging.StreamHandler(sys.stdout)]
|
|
316
|
+
|
|
317
|
+
logging.basicConfig(
|
|
318
|
+
handlers=handlers,
|
|
319
|
+
level=levels[args.log_level],
|
|
320
|
+
format=log_fmt,
|
|
321
|
+
datefmt=date_fmt,
|
|
322
|
+
)
|
|
323
|
+
log_factory = logging.getLogRecordFactory()
|
|
324
|
+
|
|
325
|
+
def record_factory(*args, **kwargs):
|
|
326
|
+
record = log_factory(*args, **kwargs)
|
|
327
|
+
record.ncaid = get_ncaid()
|
|
328
|
+
record.requestid = get_requestid()
|
|
329
|
+
record.solverid = get_solverid()
|
|
330
|
+
if record.ncaid:
|
|
331
|
+
record.ncaid = f"NCA_ID={record.ncaid} "
|
|
332
|
+
if record.requestid:
|
|
333
|
+
record.requestid = f"NVCF_REQID={record.requestid} "
|
|
334
|
+
if record.solverid:
|
|
335
|
+
record.solverid = f" (GPU {record.solverid})"
|
|
336
|
+
return record
|
|
337
|
+
|
|
338
|
+
logging.setLogRecordFactory(record_factory)
|
|
339
|
+
|
|
340
|
+
message_init()
|
|
341
|
+
logging.info(f"cuopt server version {__version__}")
|
|
342
|
+
|
|
343
|
+
request_filter.set_tier(args.tier)
|
|
344
|
+
if args.datadir:
|
|
345
|
+
settings.set_data_dir(args.datadir)
|
|
346
|
+
if args.resultdir:
|
|
347
|
+
settings.set_result_dir(args.resultdir, args.maxresult, args.mode)
|
|
348
|
+
|
|
349
|
+
if not terminated.is_set():
|
|
350
|
+
if cuda_string:
|
|
351
|
+
logging.info(cuda_string)
|
|
352
|
+
if gpu_string is None:
|
|
353
|
+
logging.info("failed to read GPU list with nvidia-smi")
|
|
354
|
+
else:
|
|
355
|
+
logging.info(f"{', '.join(gpu_string)}")
|
|
356
|
+
if not gpu_ids:
|
|
357
|
+
logging.debug("failed to determine gpu ids")
|
|
358
|
+
else:
|
|
359
|
+
# validate visible setting if we can
|
|
360
|
+
visible = os.environ.get("CUDA_VISIBLE_DEVICES", "")
|
|
361
|
+
if visible:
|
|
362
|
+
filtered = list(set(visible.split(",")).intersection(gpu_ids))
|
|
363
|
+
if not filtered:
|
|
364
|
+
logging.error(
|
|
365
|
+
"CUDA_VISIBLE_DEVICES does not allow "
|
|
366
|
+
f"any available GPUs {gpu_ids}"
|
|
367
|
+
)
|
|
368
|
+
sys.exit(1)
|
|
369
|
+
gpu_ids = filtered
|
|
370
|
+
logging.debug(
|
|
371
|
+
f"gpu_ids after CUDA_VISIBLE_DEVICES filter {gpu_ids}"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
gpu_count = args.gpu_count
|
|
375
|
+
if not isinstance(gpu_count, int):
|
|
376
|
+
gpu_count = 1
|
|
377
|
+
if not gpu_ids:
|
|
378
|
+
# We failed to read the list, skip the id
|
|
379
|
+
gpu_ids = [None]
|
|
380
|
+
else:
|
|
381
|
+
if gpu_count < 1:
|
|
382
|
+
gpu_count = 1
|
|
383
|
+
logging.warning("GPU count cannot be less than 1")
|
|
384
|
+
if gpu_count < len(gpu_ids):
|
|
385
|
+
gpu_ids = gpu_ids[0:gpu_count]
|
|
386
|
+
|
|
387
|
+
abort_list = create_abort_list()
|
|
388
|
+
|
|
389
|
+
for id in gpu_ids:
|
|
390
|
+
for p in range(int(procs_per_gpu)):
|
|
391
|
+
process_handler.create_process(
|
|
392
|
+
app_exit, job_queue, results_queue, abort_list, id
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# For whatever reason, if we put this before the solver process
|
|
396
|
+
# then the cuda health checks fail. Check into this.
|
|
397
|
+
import socket
|
|
398
|
+
|
|
399
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as skt:
|
|
400
|
+
if skt.connect_ex(("localhost", args.port)) == 0:
|
|
401
|
+
logging.info(f"Port {args.port} already in use")
|
|
402
|
+
handle_exit("", "")
|
|
403
|
+
|
|
404
|
+
from cuopt_server.webserver import run_server
|
|
405
|
+
|
|
406
|
+
w = ctx.Process(
|
|
407
|
+
target=run_server,
|
|
408
|
+
args=(
|
|
409
|
+
app_exit,
|
|
410
|
+
results_queue,
|
|
411
|
+
job_queue,
|
|
412
|
+
abort_queue,
|
|
413
|
+
abort_list,
|
|
414
|
+
jobs_marked_done,
|
|
415
|
+
args.ip,
|
|
416
|
+
args.port,
|
|
417
|
+
args.log_level,
|
|
418
|
+
args.log_file,
|
|
419
|
+
args.log_max,
|
|
420
|
+
args.log_backup,
|
|
421
|
+
args.ssl_certfile,
|
|
422
|
+
args.ssl_keyfile,
|
|
423
|
+
),
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
w.start()
|
|
427
|
+
watcher(app_exit, results_queue, job_queue, abort_queue, abort_list)
|
|
428
|
+
jobs_marked_done.wait(timeout=1)
|
|
429
|
+
for _, s in process_handler.get_solver_processes().items():
|
|
430
|
+
s.process.join()
|
|
431
|
+
|
|
432
|
+
else:
|
|
433
|
+
logging.info("calling handle_exit")
|
|
434
|
+
handle_exit("", "")
|
|
435
|
+
|
|
436
|
+
w.join()
|
|
437
|
+
|
|
438
|
+
# Check for leftover threads. Sometimes queue feeder threads do not exit
|
|
439
|
+
import threading
|
|
440
|
+
|
|
441
|
+
thread_names = {
|
|
442
|
+
t.name for t in threading.enumerate() if t.name != "MainThread"
|
|
443
|
+
}
|
|
444
|
+
if len(thread_names) > 0:
|
|
445
|
+
logging.info("not all threads exited in main process, killing")
|
|
446
|
+
psutil.Process(os.getpid()).kill()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import signal
|
|
9
|
+
|
|
10
|
+
import pexpect
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from cuopt_server import cuopt_service
|
|
14
|
+
from cuopt_server.tests.utils import utils
|
|
15
|
+
from cuopt_server.tests.utils.utils import RequestClient
|
|
16
|
+
|
|
17
|
+
# Find where is server script
|
|
18
|
+
server_script = cuopt_service.__file__
|
|
19
|
+
python_path = shutil.which("python")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.skipif(
|
|
23
|
+
"x86" not in platform.uname().machine,
|
|
24
|
+
reason="Billing logs are not currently supported on non-x86_64 architectures (they take too long to run)", # noqa: E501
|
|
25
|
+
)
|
|
26
|
+
def test_time_limit_logs():
|
|
27
|
+
dataset_path = (
|
|
28
|
+
utils.RAPIDS_DATASET_ROOT_DIR
|
|
29
|
+
+ "/cuopt_service_data/cuopt_problem_data.json"
|
|
30
|
+
)
|
|
31
|
+
dataset = json.load(open(dataset_path))
|
|
32
|
+
client = RequestClient(port=5010)
|
|
33
|
+
env = os.environ.copy()
|
|
34
|
+
env["CUOPT_MAX_SOLVE_TIME_LIMIT"] = "5"
|
|
35
|
+
|
|
36
|
+
url = "/cuopt/"
|
|
37
|
+
|
|
38
|
+
cmd = python_path + " " + server_script + " -i 127.0.0.1 -p 5010 -l debug"
|
|
39
|
+
process = pexpect.spawn(cmd, env=env)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
process.expect("Uvicorn running on", timeout=60, searchwindowsize=500)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
finally:
|
|
46
|
+
assert client.get(url + "health").status_code == 200
|
|
47
|
+
|
|
48
|
+
# /cuopt/cuopt endpoint, time limit omitted
|
|
49
|
+
dataset["solver_config"]["time_limit"] = None
|
|
50
|
+
client.post(
|
|
51
|
+
url + "cuopt",
|
|
52
|
+
json={"action": "cuOpt_OptimizedRouting", "data": dataset},
|
|
53
|
+
)
|
|
54
|
+
process.expect(
|
|
55
|
+
"Solver time limit not specified, setting to", timeout=60
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# /cuopt/cuopt endpoint, time limit modified (too high)
|
|
59
|
+
dataset["solver_config"]["time_limit"] = 100
|
|
60
|
+
client.post(
|
|
61
|
+
url + "cuopt",
|
|
62
|
+
json={"action": "cuOpt_OptimizedRouting", "data": dataset},
|
|
63
|
+
)
|
|
64
|
+
process.expect("Solver time modified to", timeout=60)
|
|
65
|
+
|
|
66
|
+
# /cuopt/cuopt endpoint, use specified (low)
|
|
67
|
+
dataset["solver_config"]["time_limit"] = 0.1
|
|
68
|
+
client.post(
|
|
69
|
+
url + "cuopt",
|
|
70
|
+
json={"action": "cuOpt_OptimizedRouting", "data": dataset},
|
|
71
|
+
)
|
|
72
|
+
process.expect("Using specified solver time 0.1", timeout=60)
|
|
73
|
+
|
|
74
|
+
# /cuopt/cuopt endpoint, solver_config omitted
|
|
75
|
+
save_config = dataset["solver_config"]
|
|
76
|
+
del dataset["solver_config"]
|
|
77
|
+
client.post(
|
|
78
|
+
url + "cuopt",
|
|
79
|
+
json={"action": "cuOpt_OptimizedRouting", "data": dataset},
|
|
80
|
+
)
|
|
81
|
+
process.expect("Creating default solver config")
|
|
82
|
+
process.expect(
|
|
83
|
+
"Solver time limit not specified, setting to", timeout=60
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# /cuopt/cuopt solver_config none
|
|
87
|
+
dataset["solver_config"] = None
|
|
88
|
+
client.post(
|
|
89
|
+
url + "cuopt",
|
|
90
|
+
json={"action": "cuOpt_OptimizedRouting", "data": dataset},
|
|
91
|
+
)
|
|
92
|
+
process.expect("Creating default solver config")
|
|
93
|
+
process.expect(
|
|
94
|
+
"Solver time limit not specified, setting to", timeout=60
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# /cuopt/request, time limit omitted
|
|
98
|
+
dataset["solver_config"] = save_config
|
|
99
|
+
dataset["solver_config"]["time_limit"] = None
|
|
100
|
+
client.post(url + "request", json=dataset)
|
|
101
|
+
process.expect(
|
|
102
|
+
"Solver time limit not specified, setting to", timeout=60
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# /cuopt/request, use specified time
|
|
106
|
+
dataset["solver_config"]["time_limit"] = 0.1
|
|
107
|
+
client.post(url + "request", json=dataset)
|
|
108
|
+
process.expect("Using specified solver time 0.1", timeout=60)
|
|
109
|
+
|
|
110
|
+
# /cuopt/request, solver config omitted
|
|
111
|
+
del dataset["solver_config"]
|
|
112
|
+
client.post(url + "request", json=dataset)
|
|
113
|
+
process.expect("Creating default solver config")
|
|
114
|
+
process.expect(
|
|
115
|
+
"Solver time limit not specified, setting to", timeout=60
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# /cuopt/request, solver config omitted
|
|
119
|
+
dataset["solver_config"] = None
|
|
120
|
+
client.post(url + "request", json=dataset)
|
|
121
|
+
process.expect("Creating default solver config")
|
|
122
|
+
process.expect(
|
|
123
|
+
"Solver time limit not specified, setting to", timeout=60
|
|
124
|
+
)
|
|
125
|
+
process.kill(signal.SIGINT)
|
|
126
|
+
process.wait()
|
|
127
|
+
if process.isalive():
|
|
128
|
+
process.kill(signal.SIGTERM)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from cuopt_server.tests.utils.utils import RequestClient, cuoptproc # noqa
|
|
5
|
+
|
|
6
|
+
client = RequestClient()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_health(cuoptproc): # noqa
|
|
10
|
+
# Normal health check
|
|
11
|
+
response = client.get("/cuopt/health")
|
|
12
|
+
assert response.status_code == 200
|
|
13
|
+
|
|
14
|
+
# health check with root path
|
|
15
|
+
response = client.get("/")
|
|
16
|
+
assert response.status_code == 200
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_readiness(cuoptproc): # noqa
|
|
20
|
+
response = client.get("/v2/health/ready")
|
|
21
|
+
assert response.status_code == 200
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_liveness(cuoptproc): # noqa
|
|
25
|
+
response = client.get("/v2/health/live")
|
|
26
|
+
assert response.status_code == 200
|