Skip to content

Runtime CPU+GPU monitoring, multi-config benchmark matrix, GPU selection, presets, and MPS control#61

Open
Parsifal-2045 wants to merge 5 commits into
cms-patatrack:masterfrom
Parsifal-2045:devMonitorGPU
Open

Runtime CPU+GPU monitoring, multi-config benchmark matrix, GPU selection, presets, and MPS control#61
Parsifal-2045 wants to merge 5 commits into
cms-patatrack:masterfrom
Parsifal-2045:devMonitorGPU

Conversation

@Parsifal-2045

@Parsifal-2045 Parsifal-2045 commented Jul 1, 2026

Copy link
Copy Markdown

Overview

This PR reworks the benchmark/scan workflow and adds device-level runtime monitoring so a single benchmark invocation produces the throughput, resource-usage, and log artifacts previously only available via external scripts.
The developments build on the input-only benchmark (#57) and configurable host-memory monitoring (#58) and, although the scope of this PR is quite large, I've tried to keep the changes organized as 4 self-contained commits.

Edited 07/07 addressing the first batch of comments #61 (comment)
Few more features and fixes introduced in a follow-up commit, see #61 (comment)

Main developments (more or less per commit)

  1. Benchmark workflow: multi-config matrix, --gpus, input auto-report, templated logs, several config files

    • The new --setup J,T,S --setup j=J,t=T,s=S (in any order and with omitted fields automatically deduced using the existing logic) in conjunction with the possibility to list multiple configs to benchmark, runs the full configs x setup matrix saving the results of each run under a unique log dir / CSV (deduped, never overwriting)
    • --gpus restricts to a GPU subset (plain list or nvidia=..:amd=..) by filtering the visible devices so affinity still assigns one GPU per job across the selection (warns if the selection matches no device)
    • --logdir without any additional arguments now automatically produces a directory for the output logs created following the default template logs.%config.%jobs.%threads.%streams.%gpus logs_%config_%jobs_%threads_%streams(_%gpus) with the %gpus tag omitted in case the job does not use any GPU. The user can also specify custom names for the output directory taking advantage of the same expansions (documented in the --help message)
    • --output-log tees the full console output to <logdir>/output.log
    • --input-xml auto (+ --input-xml-events) generates the job report and builds the trimmed input-only process once per config
    • Each run's config file is now automatically copied into its log directory together with the fully customised dump that is actually run during the benchmark
    • Auto-merged resources (resources.json / Phase2Timing_resources.json) are named after the run's log directory (e.g. gpu_16j_16t_16s_allGPUs.json)
  2. Unified multi-vendor CPU+GPU runtime monitoring

    • Aggregate host RSS + per-GPU util/mem (+power/temp) on a single cadence (one measurement per second) via auto-selected nvidia-smi/amd-smi, for the GPUs the jobs can use (detected devices restricted to CUDA_VISIBLE_DEVICES/HIP_VISIBLE_DEVICES, honoring --gpus)
    • --gpu-monitoring and --host-memory-monitoring --monitor-host and --monitor-gpu (none/basic/full, both basic by default). A single monitor spans a whole benchmark run streaming one continuous cpu_monitor.csv + gpu_monitor.csv to <logdir> line by line (so they survive an interrupt and a hard crash/SIGKILL), and folding each step's numpy arrays into <logdir>/step*/monit.py (extending the pre-existing per-process host-memory arrays). Finally a new "hardware usage summary" is printed to standard output once the benchmark finishes processing (CPU/GPU max/mean for all reported metrics). The new GPU monitor reuses the CPU-monitoring pattern. Indefinite runs (-r 0) fall back to each run's own monitor, drained per step into monit.py so memory stays bounded. A cmsRun job failure is handled gracefully (measurement skipped) and each run's own monitor is flushed, so no termination loses the collected data. The CPU-only path remains identical to the previous implementation when (--gpu-monitoring none)
  3. --preset NAME

    • Named default-option presets in an extensible registry
    • hltRun3 (10300 ev, skip 300, res 100, 8j/32t/24s, wait 0, --input-collections rawDataCollector) and hltPhase2 (1000 ev, skip 100, res 25, 16/16/16, wait 30, --input-xml auto). Applied before parsing so any value can be overridden explicitly.
  4. Automatic MPS setup/usage

    • The new --nvidia-mps starts/attaches the NVIDIA MPS daemon and set each job's active-thread % from how many jobs share its GPU (or an explicit value)
    • By default the assigned active thread percentage (per GPU) is obtained by flooring ceiling 100 / number_of_jobs_per_gpu to the nearest integer (e.g. 100/16 = 6.25 -> 6 7)
    • Restore/stop on exit
    • Works with --slot and the automatic affinity
    • Extend the automatic naming (<logdir>, csv and merged resources) to include the MPS status or active thread percentage at the end (noMPS, MPS<VALUE> or MPS<RANGE>). MPS<RANGE> is used in cases where jobs are not equally distributed across GPUs and provides the min-max range of active thread percentages set (e.g. 7 jobs split across 2 GPUs -> 4 jobs on one GPU (MPS 25%), 3 jobs on the other (34%), MPS<RANGE> will be MPS25-34). The %mps tag is added to the expandable parameters that can be passed to --logdir and is automatically dropped when no NVIDIA GPU is available
    • Add the corresponding --no-nvidia-mps flag that does not start the MPS daemon and errors out of the job in case a daemon is running already
    • Also adds a shared build_options helper (single source for the multiCmsRun option dict across benchmark/scan/multirun), actionable errors when a variadic option swallows the config, per-run output-file closing, and a couple of typo fixes

Examples

1. Phase-2 timing job (with reduced setup and full GPU monitoring)

$ ./benchmark --preset hltPhase2 --setup j=2,t=8,s=8 -r2  --monitor-gpu full -- gpu_config.py

Console output:

1 CPUs:
  0: AMD EPYC 9654 96-Core Processor (47 cores, 94 threads)

2 visible NVIDIA CUDA GPUs:
  0: NVIDIA H100 NVL
  1: NVIDIA H100 NVL

No visible AMD ROCm GPUs

Generating the input job report by running "cmsRun -j /tmp/.../jobReport.xml ..."
Benchmarking only the input
Warming up
Running 2 times over 100 events with 2 jobs, each with 8 threads, 8 streams, and 1 GPUs
    16.5 ±   0.2 ev/s (80 events, 99.6% overlap),     16.2 ±   0.2 ev/s (⩾ 60 events, overlap-only)
    17.2 ±   0.2 ev/s (80 events, 95.7% overlap),     17.1 ±   0.2 ev/s (⩾ 60 events, overlap-only)
 --------------------
    16.9 ±   0.5 ev/s,     16.7 ±   0.6 ev/s (⩾ 60 events, overlap-only)

Benchmarking gpu_config.py
Warming up
Running 2 times over 100 events with 2 jobs, each with 8 threads, 8 streams, and 1 GPUs
     7.7 ±   0.1 ev/s (80 events, 98.9% overlap),      7.7 ±   0.2 ev/s (⩾ 40 events, overlap-only)
     7.8 ±   0.1 ev/s (80 events, 99.2% overlap),      7.9 ±   0.2 ev/s (⩾ 40 events, overlap-only)
 --------------------
     7.7 ±   0.1 ev/s,      7.8 ±   0.1 ev/s (⩾ 40 events, overlap-only)

-------------------------------------
       HARDWARE USAGE SUMMARY
-------------------------------------
Monitoring Interval: 1 second(s)

--- CPU Memory ---
Peak Total CPU Memory Usage: 14802 MiB
Mean Total CPU Memory Usage: 5796 MiB

--- GPU Memory & Usage ---
Peak Total GPU Memory Usage: 4740 MiB
Mean Total GPU Memory Usage: 1668 MiB

Per-GPU GPU Utilization:
  GPU-0: 1.12% (mean), 28% (max)
  GPU-1: 1.21% (mean), 24% (max)
Per-GPU GPU Memory:
  GPU-0: 828 MiB (mean), 2579 MiB (max)
  GPU-1: 840 MiB (mean), 2161 MiB (max)
Per-GPU Power / Temperature:
  GPU-0: 88.5 W (mean), 98.2 W (max); 41 C (mean), 42 C (max)
  GPU-1: 88.1 W (mean), 96.4 W (max); 40 C (mean), 41 C (max)
-------------------------------------

Resulting log directory:

logs_gpu_2j_8t_8s_allGPUs_noMPS/
├── output.log                    # full console log: overview -> job report -> both runs -> summary
├── gpu_config.py                 # copy of the configuration passed as input to benchmark
├── gpu_config_dump.py      # copy of the configuration that was actually run (full customised dump)
├── cpu_monitor.csv               # continuous host-memory series (I/O warm-up -> last repetition)
├── gpu_monitor.csv               # continuous per-GPU series (util, memory[, power, temp], totals)
├── Phase2Timing_resources.json     # merged Phase2Timing resources
├── step0000/
│   ├── monit.py                    # per-job host-memory arrays + this step's GPU/aggregate slice
│   ├── Phase2Timing_resources.json # per-step merged resources
│   ├── pid769428/                  # per-job cmsRun logs (+ Phase2Timing_resources.json)
│   └── pid769429/
├── step0001/ ...
└── warmup/ ...

gpu_monitor.csv format

$ head -2 logs_gpu_2j_8t_8s_allGPUs_noMPS/gpu_monitor.csv
elapsed_seconds,gpu0_usage,gpu0_memory,gpu0_power,gpu0_temp,gpu1_usage,gpu1_memory,gpu1_power,gpu1_temp,total_usage,total_memory
0,0,0,66.48,40,0,0,77.43,39,0.00,0

2. A configurations × setups matrix, restricted to a subset of GPUs

$ ./benchmark --setup j=8,t=32,s=24 j=16,t=16,s=16 --gpus 0,1 --logdir -- gpu_config.py cpu_config.py

runs the 2 configs × 2 setups matrix on GPUs 0 and 1, producing four independent log directories (each with its own CSVs, output.log, config copy and per-step data, no %gpus and %mps tags in the logs for cpu_config.py); the monitor reports only GPUs 0 and 1:

logs_gpu_8j_32t_24s_gpu01_noMPS/     logs_cpu_8j_32t_24s/
logs_gpu_16j_16t_16s_gpu01_noMPS/    logs_cpu_16j_16t_16s/

3. Sharing one GPU between two slots via MPS

$ ./benchmark -j2 --slot c=0-15:nv=0 --slot c=16-31:nv=0 --nvidia-mps --debug-affinity -- gpu_config.py
...
Using NVIDIA MPS active thread percentage: 2 job(s) @ 50%
  - job 0 will run on the CPUs 0-15, with the NVIDIA GPU 0, ..., with MPS active thread percentage 50%
  - job 1 will run on the CPUs 16-31, with the NVIDIA GPU 0, ..., with MPS active thread percentage 50%

Both jobs share GPU 0, so each is capped at 50%; with one job per GPU it would be 100%.

4. MPS with uneven job division across GPUs

$ ./benchmark --setup j=7,t=8,s=8 --gpus 0,1 --logdir -- gpu_config.py
...
Using NVIDIA MPS active thread percentage: 4 job(s) @ 25%, 3 job(s) @ 34%
  - job 0 will run on the CPUs 1-12, with the NVIDIA GPU 0, with any available AMD GPUs, with MPS active thread percentage 25%
  - job 1 will run on the CPUs 13-23,47, with the NVIDIA GPU 1, with any available AMD GPUs, with MPS active thread percentage 34%
  - job 2 will run on the CPUs 48-59, with the NVIDIA GPU 0, with any available AMD GPUs, with MPS active thread percentage 25%
  - job 3 will run on the CPUs 60-69,93,193, with the NVIDIA GPU 1, with any available AMD GPUs, with MPS active thread percentage 34%
  - job 4 will run on the CPUs 194-205, with the NVIDIA GPU 0, with any available AMD GPUs, with MPS active thread percentage 25%
  - job 5 will run on the CPUs 206-215,239-240, with the NVIDIA GPU 1, with any available AMD GPUs, with MPS active thread percentage 33%
  - job 6 will run on the CPUs 241-252, with the NVIDIA GPU 0, with any available AMD GPUs, with MPS active thread percentage 25%

Behaviour changes and backward compatibility

  • Passing no --logdir or --logdir with no argument now creates a <logdir> named using the default pattern logs_%config_%jobs_%threads_%streams(_%gpus)(_%mps) (was logs, still disabled by passing either --logdir ''or--no-logdir). Users can also pass a custom name --logdir DIRusing the same expandable parameters (documented in a new section at the end of the--help` message
  • --monitor-gpu defaults to basic: every GPU-node run starts the monitor and prints the summary (--monitor-gpu none opts out)
  • Every other option/functionality should not be affected by these changes.

Validation

I have run extensive tests on both NVIDIA and AMD hardware, trying to stress-test the new configuration options and setup. I was able to reproduce results obtained with an external monitoring script. The logging behaviour seems to be consistent regardless of the available hardware/setup and the automatic MPS setup sets the active thread percentage to the expected value regardless of the job division across GPUs.

@bfonta

bfonta commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

I tested the script on my side and it seems to work as expected.

  • What exactly is intended by MPS<RANGE>? It might be missing from the PR description.

  • Very minor nitpick: when using --mps, the reported value is rounded, and the print reads Using NVIDIA MPS active thread percentage: 16 job(s) @ 6%, when IIUC it should be 6.25% (I'm using a single GPU). This is also propagated to the output name logs.*.MPS6/.

@bfonta

bfonta commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

There seems to be an issue when running with multiple configurations. In my case:

./patatrack-scripts/benchmark --preset hltPhase2 --setup 16,16,16 --gpu-monitoring full --gpus 0 --mps -- gpu-nvidia_OneFile_config.py gpu-nvidia_ngtScouting_OneFile_config.py gpu-nvidia_ProcMods_null_Menu_NGTScouting_OneFile_config.py

correctly processes the first configuration, but fails before starting the second:

...
Per-GPU GPU Utilization:
  GPU-0: 54.11% (mean), 100% (max)
Per-GPU GPU Memory:
  GPU-0: 19051 MiB (mean), 38499 MiB (max)
Per-GPU Power / Temperature:
  GPU-0: 152.7 W (mean), 248.9 W (max); 60 C (mean), 68 C (max)
-------------------------------------
Failed to parse gpu-nvidia_ngtScouting_OneFile_config.py: The Process HLTX tried to redefine which Modifiers to use after another Process was already started
Stopped the NVIDIA MPS control daemon

Running the second configuration separately works as expected.

EDIT:

Running the configuration a second time I get a different kind of error once the second configuration starts:

Using NVIDIA MPS active thread percentage: 16 job(s) @ 6%
Warming up
The underlying cmsRun job failed with return code 65

The last lines of the error log are:
%MSG
02-Jul-2026 09:28:55 CEST  Initiating request to open file file:/cms-hlt-nfs/data/relValTTbar_200PU/merged_1k_CMSSW_17_0_0_pre1.root
02-Jul-2026 09:29:01 CEST  Successfully opened file file:/cms-hlt-nfs/data/relValTTbar_200PU/merged_1k_CMSSW_17_0_0_pre1.root
02-Jul-2026 09:29:03 CEST  Closed file file:/cms-hlt-nfs/data/relValTTbar_200PU/merged_1k_CMSSW_17_0_0_pre1.root
----- Begin Fatal Exception 02-Jul-2026 09:29:03 CEST-----------------------
An exception of category 'InsertError' occurred while
   [0] Constructing the EventProcessor
Exception Message:
Module with label 'Phase2L1GTProducer' already exists.
----- End Fatal Exception -------------------------------------------------

@Parsifal-2045

Parsifal-2045 commented Jul 2, 2026

Copy link
Copy Markdown
Author

What exactly is intended by MPS<RANGE>? It might be missing from the PR description.

MPS is used for cases where the jobs are not evenly distributed on GPUs. For example, if 7 jobs are sent to 2 GPUs, one of them will end up with 4 jobs (MPS 25%) and the other with 3 (MPS 33%). In this case the automatic label will be MPS25-33 (also updated the PR description with the same example)

Very minor nitpick: when using --mps, the reported value is rounded, and the print reads Using NVIDIA MPS active thread percentage: 16 job(s) @ 6%, when IIUC it should be 6.25% (I'm using a single GPU). This is also propagated to the output name logs.*.MPS6/.

The reported value is correct in the sense that the same rounding is applied to the value set internally. The result of the division is floored to the nearest integer (e.g. 6.25 -> 6) since I've not found references to MPS supporting floating point values and the actual SM attribution is quantized in any case (updated the PR description to include this info)

There seems to be an issue when running with multiple configurations

Looking into it

@Parsifal-2045

Copy link
Copy Markdown
Author

There seems to be an issue when running with multiple configurations. In my case:

./patatrack-scripts/benchmark --preset hltPhase2 --setup 16,16,16 --gpu-monitoring full --gpus 0 --mps -- gpu-nvidia_OneFile_config.py gpu-nvidia_ngtScouting_OneFile_config.py gpu-nvidia_ProcMods_null_Menu_NGTScouting_OneFile_config.py

correctly processes the first configuration, but fails before starting the second:

...
Per-GPU GPU Utilization:
  GPU-0: 54.11% (mean), 100% (max)
Per-GPU GPU Memory:
  GPU-0: 19051 MiB (mean), 38499 MiB (max)
Per-GPU Power / Temperature:
  GPU-0: 152.7 W (mean), 248.9 W (max); 60 C (mean), 68 C (max)
-------------------------------------
Failed to parse gpu-nvidia_ngtScouting_OneFile_config.py: The Process HLTX tried to redefine which Modifiers to use after another Process was already started
Stopped the NVIDIA MPS control daemon

Running the second configuration separately works as expected.

EDIT:

Running the configuration a second time I get a different kind of error once the second configuration starts:

Using NVIDIA MPS active thread percentage: 16 job(s) @ 6%
Warming up
The underlying cmsRun job failed with return code 65

The last lines of the error log are:
%MSG
02-Jul-2026 09:28:55 CEST  Initiating request to open file file:/cms-hlt-nfs/data/relValTTbar_200PU/merged_1k_CMSSW_17_0_0_pre1.root
02-Jul-2026 09:29:01 CEST  Successfully opened file file:/cms-hlt-nfs/data/relValTTbar_200PU/merged_1k_CMSSW_17_0_0_pre1.root
02-Jul-2026 09:29:03 CEST  Closed file file:/cms-hlt-nfs/data/relValTTbar_200PU/merged_1k_CMSSW_17_0_0_pre1.root
----- Begin Fatal Exception 02-Jul-2026 09:29:03 CEST-----------------------
An exception of category 'InsertError' occurred while
   [0] Constructing the EventProcessor
Exception Message:
Module with label 'Phase2L1GTProducer' already exists.
----- End Fatal Exception -------------------------------------------------

The latest force-push should have fixed all these errors. The root cause was in the way the parsing of each config was done. benchmark parsed every config in the same Python interpreter (parseProcess → loadModuleFromFile), but CMSSW is fundamentally "one config per interpreter":

  • cms.Process.__init__ has a class-global _firstProcess; the first Process locks in its modifiers, and any later Process naming a modifier the first didn't choose raises the original error.
  • Simply resetting that guard is not a valid fix: _cfi/_cff modules run modifier.toModify(...) at import time, and Python caches modules meaning that whichever config imports them first freezes their contents, and a later config silently gets the wrong modules.

I have folded the fix in the first commit, where multi-config parsing was introduced.

  • parseProcess now parses each config in its own subprocess and loads back only its fully-expanded dumpPython(). The flat dump has no modifiers and no _cfi/_cff imports, so it's self-contained and safe to load alongside other configs
  • loadModuleFromFile moved to common.py as the single loader shared by the subprocess and the parent

@bfonta

bfonta commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@Parsifal-2045 I confirm that the new changes solve the issue.

@fwyzard

fwyzard commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@Parsifal-2045 thanks for all the developments and improvements !

For now I have a few comments about the interface, later I'll try to go through the changes.


  • The new --setup J,T,S in conjunction with possibility to list multiple configs to benchmark, runs the full configs x setup matrix saving the results of each run under a unique log dir / CSV (deduped, never overwriting)

I have a mild preference for a syntax that is a bit more explicit, for example --setup j=J,t=T,s=S


  • Each run's config file is now automatically copied into its log directory

Is this the original file given on the command line, or the preprocessed file passed to cmsRun ?

I guess both could have a use: what one is asking to run, and what is effectively running.


  • Auto-merged resources (resources.json / Phase2Timing_resources.json) are named after the run's log directory (e.g. gpu_16j_16t_16s_allGPUs.json)

I would prefer to keep the name resources.json (or whatever the file created by cmsRun is called). It's anyway inside its own log directory.


  • --gpu-monitoring and --host-memory-monitoring (none/basic/full, both basic by default).

How about --monitor-gpu and --monitor-host ?
(not --monitor-host-memory because I think we can monitor also the CPU usage via psutil's oneshot().cpu_percent() - but maybe that can be added in a separate PR)


  • The CPU-only path remains identical to the previous implementation when (--gpu-monitoring none)

Does anything rely on the old behaviour?
Otherwise I think we can just use the new approach consistently.


--mps

I have a mild preference for --nvidia-mps, both to be more explicit about what it is, and that it only affects NVIDIA GPUs, not AMD.

What happens if the job does not use any NVIDIA GPUs ?

Also for completeness can you add a --no-nvidia-mps option that

  • does not start MPS
  • exits with an error if MPS is already running

?

* By default the assigned active thread percentage (per GPU) is obtained by flooring `100 / number_of_jobs_per_gpu` to the nearest integer (e.g. 100/16 = 6.25 -> 6)

I think we should round to the ceiling, e.g. 100/16 = 6.25 -> 7.
Otherwise we allocate only 16 × 6 = 96% of the total GPU processing capacity.
I expect some oversubscription (16 × 7 = 112%) not to be a big deal, since until now we have been doing 16×100 = 1600% :-D


  • --logdir without any additional arguments now automatically produces a directory for the output logs created following the default template logs.%config.%jobs.%threads.%streams.%gpus. The user can also specify custom names for the output directory taking advantage of the same expansions

I like the idea, with some suggestions:

  • can you add a section about the fields that are supported to the --help message ?

  • from a quick look at the code, the default actually seems to be logs.%config.%jj.%tt.%ss.%gpus.%mps.
    Does that end with a single . when MPS is not used ?

  • being old and coming from printf I am more accustomed to single letter % patterns; however I understand that there are only so many letters, and some longer fields may be more descriptive. However, please avoid conflicting fields like %g vs %gpus.

  • possibly for a follow up PR, can you think of a way to make some fields completely optional ?
    For example, if a job does not use GPUs (or only AMD GPUs) the .%mps field is kind of redundant.

  • I would prefer _ instead of . as a separator by default.

  • --logdir now defaults to OFF (was logs): no logs/auto-merge unless --logdir is given. This is to enable the option of having no logdir, having the default created one following the auto-naming scheme (--logdir), or having a custom named logdir (--logdir DIR)

Please keep the old default, one can use --no-logdir (or --logdir '') to switch it off.

@Parsifal-2045

Copy link
Copy Markdown
Author

Thanks for the comments. I'll address most of the interface-related ones in follow-up commits. Let me also answer to the more direct questions.

  • Each run's config file is now automatically copied into its log directory

Is this the original file given on the command line, or the preprocessed file passed to cmsRun ?

I guess both could have a use: what one is asking to run, and what is effectively running.

Right now the original file is copied, to preserve exactly the configuration that was passed as argument to benchmark. I don't think there would be any strong limitation in copying the full customized dump if that's preferable.

  • The CPU-only path remains identical to the previous implementation when (--gpu-monitoring none)

Does anything rely on the old behaviour? Otherwise I think we can just use the new approach consistently.

Not that I know of, but I stayed on the side of caution here. I have no issue in using basic monitoring for both CPU and GPU as default everywhere

--mps

I have a mild preference for --nvidia-mps, both to be more explicit about what it is, and that it only affects NVIDIA GPUs, not AMD.

What happens if the job does not use any NVIDIA GPUs ?

Also for completeness can you add a --no-nvidia-mps option that

* does not start MPS

* exits with an error if MPS is already running

?

Will change the name of the parameter and include the corresponding --no-nvidia-mps option. Currently, on a node equipped only with AMD GPUs, passing the --nvidia-mps option produces a warning

Warning: could not start the MPS control daemon:

then the job goes on as normal using the available GPUs

* By default the assigned active thread percentage (per GPU) is obtained by flooring `100 / number_of_jobs_per_gpu` to the nearest integer (e.g. 100/16 = 6.25 -> 6)

I think we should round to the ceiling, e.g. 100/16 = 6.25 -> 7. Otherwise we allocate only 16 × 6 = 96% of the total GPU processing capacity. I expect some oversubscription (16 × 7 = 112%) not to be a big deal, since until now we have been doing 16×100 = 1600% :-D

Fair point, I was undecided on whether to floor or ceil the division, will implement ceiling :)

  • --logdir without any additional arguments now automatically produces a directory for the output logs created following the default template logs.%config.%jobs.%threads.%streams.%gpus. The user can also specify custom names for the output directory taking advantage of the same expansions

I like the idea, with some suggestions:

* can you add a section about the fields that are supported to the `--help` message ?

* from a quick look at the code, the default actually seems to be `logs.%config.%jj.%tt.%ss.%gpus.%mps`.
  Does that end with a single `.` when MPS is not used ?

* being old and coming from `printf` I am more accustomed to single letter `%` patterns; however I understand that there are only so many letters, and some longer fields may be more descriptive. However, please avoid conflicting fields like `%g` vs `%gpus`.

* possibly for a follow up PR, can you think of a way to make some fields completely optional ?
  For example, if a job does not use GPUs (or only AMD GPUs) the `.%mps` field is kind of redundant.

* I would prefer `_` instead of `.` as a separator by default.

I will take another look at naming and expansion of parameters to try and incorporate all the suggestions. Currently any job that does no use MPS still gets a noMPS tag at the end (so the name of the directory either ends with noMPS or MPS<VALUE> also on AMD nodes)

  • --logdir now defaults to OFF (was logs): no logs/auto-merge unless --logdir is given. This is to enable the option of having no logdir, having the default created one following the auto-naming scheme (--logdir), or having a custom named logdir (--logdir DIR)

Please keep the old default, one can use --no-logdir (or --logdir '') to switch it off.

Just to be clear, you'd prefer the following behaviour:

  • Passing --no-logdir produces no logs
  • Passing nothing produces the default logs directory
  • Passing --logdir produced the logs directory named using the default pattern logs.%config.%jj.%tt.%ss.%gpus.%mps (or some newer variation)
  • Passing --logdir <DIR> saves the logs under <DIR>

Do I understand correctly?

@fwyzard

fwyzard commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Right now the original file is copied, to preserve exactly the configuration that was passed as argument to benchmark. I don't think there would be any strong limitation in copying the full customized dump if that's preferable.

The downside of this approach is that it may not capture the whole configuration.
For example, in my Run 3 HLT workflows running on data the .py file that I pass as the argument is only a small wrapper that loads the HLT configuration, input data, etc. from some other python files in the local directory.

@fwyzard

fwyzard commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

I have no issue in using basic monitoring for both CPU and GPU as default everywhere

Unless there is any evidence that it adversely affect performance, I would say let's do that.

@Parsifal-2045

Copy link
Copy Markdown
Author

Right now the original file is copied, to preserve exactly the configuration that was passed as argument to benchmark. I don't think there would be any strong limitation in copying the full customized dump if that's preferable.

The downside of this approach is that it may not capture the whole configuration. For example, in my Run 3 HLT workflows running on data the .py file that I pass as the argument is only a small wrapper that loads the HLT configuration, input data, etc. from some other python files in the local directory.

I see, that's something I had not considered. We can maybe copy both the original input and the fully customized dump to cover all cases?

@fwyzard

fwyzard commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

We can maybe copy both the original input and the fully customized dump to cover all cases?

That would make sense.

Maybe keep the original name for the file passed as an argument, and use something like config.py or expanded.py for the expanded configuration.

@fwyzard

fwyzard commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Just to be clear, you'd prefer the following behaviour:

* Passing `--no-logdir` produces no logs

* Passing nothing produces the default `logs` directory

* Passing` --logdir` produced the logs directory named using the default pattern `logs.%config.%jj.%tt.%ss.%gpus.%mps` (or some newer variation)

* Passing `--logdir <DIR>` saves the logs under `<DIR>`

Do I understand correctly?

I think that's what I wrote, but not what I meant :-D

Let me amend my proposal to

  • passing --no-logdir or --logs '' produces no logs
  • passing nothing or just --logs produces the logs directory named using the default pattern
  • passing --logdir <DIR> produces the logs directory <DIR>

@fwyzard

fwyzard commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Currently any job that does no use MPS still gets a noMPS tag at the end (so the name of the directory either ends with noMPS or MPS<VALUE> also on AMD nodes)

OK... if you manage, I would suggest that a job that does not use NVIDIA GPUs (either CPU only or using only AMD GPUs) should not get any MPS field at all.

@Parsifal-2045

Copy link
Copy Markdown
Author

Currently any job that does no use MPS still gets a noMPS tag at the end (so the name of the directory either ends with noMPS or MPS<VALUE> also on AMD nodes)

OK... if you manage, I would suggest that a job that does not use NVIDIA GPUs (either CPU only or using only AMD GPUs) should not get any MPS field at all.

I wanted to do something similar for CPU-only jobs not getting the allGPUs (or equivalent) field. I will try to implement both solutions

…port, templated logs

- accept several config files and a --setup list (explicit "j=J,t=T,s=S"
fields, any order, omitted fields auto-derived), running the configs x
setups matrix with unique log directories and CSVs
- add --gpus to restrict the run to a subset of GPUs (plain list or
per-vendor form): it filters the visible devices so the automatic
affinity still assigns one GPU per job, only across the selection
- make --logdir templated and on by default: no --logdir (or a bare
--logdir) uses the automatic template, --logdir DIR uses DIR, and
--no-logdir (or --logdir '') disables logs. The placeholders
%config/%j/%t/%s/%gpj/%gpus are documented in a dedicated "LOGDIR
TEMPLATE" help section; %gpus is optional and drops out (with its
separator) when no GPU is used (also parsing the configuration's
accelerators)
- add --output-log to tee the full console output (from the system
overview and input job report onward) to <logdir>/output.log
- generate the input job report automatically with --input-xml auto and
build the trimmed input-only process once per configuration
- copy each run's configuration file into its log directory, together
with the fully-expanded dump that is actually run (<config>_dump.py)
- auto-merge Phase2Timing_resources.json
- parse each configuration in its own subprocess and load back only its
fully-expanded dumpPython(): HLT menus apply their era / ProcessModifier
customisations at import time, and CMSSW forbids a later cms.Process
from choosing modifiers the first one did not, so parsing several
configs in a single interpreter would abort ("tried to redefine which
Modifiers to use after another Process was already started") or leak the
first config's import-time modifier state into the next;
loadModuleFromFile moves to common.py as the single loader used by both
the subprocess and the parent
- sample aggregate host-memory RSS and per-GPU utilization and memory
(optionally power and temperature) at one sample per second, through
auto-selected nvidia-smi / amd-smi backends
- monitor the GPUs the jobs can use: detect the devices and restrict
them to the CUDA_VISIBLE_DEVICES / HIP_VISIBLE_DEVICES selection,
honoring --gpus
- control the detail with --monitor-gpu and --monitor-host
(none/basic/full, both default basic)
- write the full-run cpu_monitor.csv and gpu_monitor.csv at <logdir> top
level and fold each step's numpy arrays into its monit.py (drained per
step, so even an indefinite run stays memory-bounded), extending the
per-process host-memory monitoring already there
- provide a line-by-line CSV streamer and a shared RunMonitor
(start/finish plus per-step snapshot) so a caller can span several runs
into one continuous series that survives an interruption or crash
- apply a named set of default options before parsing, so any preset
value can still be overridden explicitly; the presets live in an
extensible registry
- include hltRun3 and hltPhase2 for the "standard" HLT timing setups
- with --nvidia-mps, start the MPS control daemon (or attach to a
running one) and set each job's active-thread percentage from how many
jobs share its GPU. By default it rounds up to the nearest integer:
ceil(100 / jobs-per-GPU). Otherwise the acrive thread percentage can be
set to a specific value by passing --nvidia-mps VALUE. Restore or stop
the daemon on exit and on Ctrl+C
- derive the per-slot percentages from the actual job-to-GPU assignment,
so it works with both --slot and the automatic affinity
- start MPS only when an NVIDIA GPU is actually used; otherwise warn and
skip it. Add --no-nvidia-mps to require MPS off: never start it, and
exit with an error if a control daemon is already running
- record the applied MPS setting in each run's automatic log directory
and CSV name through a %mps placeholder (e.g. ..._MPS50, ..._MPS25-34
when different GPUs run at different percentages, or ..._noMPS when
off); the MPS tag drops out entirely when no NVIDIA GPU is used
- share the multiCmsRun options through a single build_options helper
used by benchmark, scan and multirun
- also give actionable errors when a variadic option swallows the
config, close per-run output files on every exit path, and fix a
makedirs typo on the EvFDaqDirector path
@Parsifal-2045

Parsifal-2045 commented Jul 6, 2026

Copy link
Copy Markdown
Author

The latest force push implements all the changes discussed. In particular (again, ~ per commit):

  1. Benchmark workflow: multi-config matrix, --gpus, input auto-report, templated logs, several config files
  • --setup uses explicit j=J,t=T,s=S fields (any order, omitted fields auto-derived)
  • --logdir is on by default and templated with a _ separator; placeholders %config %j %t %s %gpj %gpus are documented in a dedicated LOGDIR TEMPLATE --help section. (Can be disabled by passing --no-logdir or --logdir '')
  • %gpus drops out (with its separator) on CPU-only runs detected from process.options.accelerators or when no jobs run on GPU (e.g. passing -g 0)
  • each run saves both the original config and the fully customised and expanded <config>_dump.py it actually runs in its <logdir>
  • auto-merged resources keep their plain recipe name (resources.json, Phase2Timing_resources.json)
  1. Unified CPU/GPU monitoring
  • controlled by --monitor-gpu and --monitor-host (none/basic/full, both default basic)
  1. Automatic MPS setup
  • flag renamed --mps--nvidia-mps
  • the automatic split rounds up (ceil(100 / jobs-per-GPU), e.g. 16 jobs → 7%)
  • --no-nvidia-mps never starts MPS and exits with an error if a daemon is already running
  • MPS is started only when an NVIDIA GPU is actually used; otherwise the process prints a warning and keeps running without starting the daemon
  • the applied active thread percentage is recorded in the logdir/CSV name via %mps (_MPS50, _MPS25-34, or _noMPS), dropped entirely without an NVIDIA GPU

Extend the host monitoring added in "Add unified multi-vendor CPU+GPU
runtime monitoring" to report CPU utilization alongside memory, and make
it
independent of GPU monitoring.

- rename HostMemoryInfo to HostMonitorInfo: BASIC now samples CPU
utilization (psutil cpu_percent), VSS, and RSS; FULL adds USS and PSS.
Both the per-process (singleCmsRun) and the aggregate (monitorResources)
samplers carry the new column as well as the cpu_monitor.csv, per-step
monit.py and hardware summary
- fixed a bug where cpu_monitor.csv and the hardware summary were
coupled to GPU monitoring. Now cpu_monitor.csv and the CPU hardware
summary are produced whenever --monitor-host is on, even with
--monitor-gpu none or on a GPU-less node. --monitor-gpu now governs only
the GPU metrics, and gpu_monitor.csv is written only when --monitor-gpu
is set to basic or full
@Parsifal-2045

Copy link
Copy Markdown
Author

The latest commit adds CPU frequency monitoring via psutil.cpu_percent() and USS memory measurements (the latter only when --monitor-host is set to full). It also fixes a bug where some of the monitoring outputs were incorrectly gated behind the --monitor-gpu level.
This concludes the list of features planned for this batch of developments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants