A portable test leaves the machine in the config

Questions

  • Why is valid_systems = ['snellius'] a trap?

  • Where does a portable test read the core count from?

  • What does the EESSI mixin replace in the class body?

What you will be able to do

  1. Point at the three lines that make a ReFrame test specific to one cluster, and say where each line belongs instead.

  2. Read an EESSI mixin test and say what device_type and compute_unit cause the hooks to set.

  3. Skip a test when the node cannot run it, rather than hardcoding a size that node will never have.

Before this chapter

The mpi4py test in Parameters multiply the suite runs on Snellius and on no other system, due to three separate reasons:

valid_systems = ['snellius']
self.num_tasks_per_node = min(self.num_tasks, 128)
module_name = parameter([
    'mpi4py/3.1.4-gompi-2023a',
    'mpi4py/3.1.5-gompi-2023b',
])

The first binds the test to a single system name. The second provides the system’s core count as a constant. The third supplies two module strings present in that site’s tree. A 64-core node running EESSI’s mpi4py/3.1.5-gompi-2023b still fails the first filter and oversubscribes when the name is patched manually. EESSI’s writing-tests page presents a three-step tutorial: write the Slurm scripts for one machine, write the ReFrame test that encodes those scripts, then remove the machine. The third step is a mixin. The test class retains the workload. The configuration retains the node.

What the mixin changes

@rfm.simple_test
class EESSI_MPI4PY(rfm.RunOnlyRegressionTest, EESSI_Mixin):
    device_type = DEVICE_TYPES[CPU]
    compute_unit = COMPUTE_UNIT[CPU]
    module_name = parameter(find_modules('mpi4py'))
    n_iterations = variable(int, value=1000)
    executable = 'python3'
    time_limit = '5m00s'
    readonly_files = ['mpi4py_reduce.py']

    def required_mem_per_node(self):
        return self.num_tasks_per_node * 100 + 250

EESSI_Mixin provides hooks that read the partition. The expression “One full node” corresponds to compute_unit = COMPUTE_UNIT[CPU], and the hook assigns num_tasks from processor.num_cpus in the configuration. A node with 64 cores receives 64 tasks. A node with 128 cores receives 128 tasks. The test file lacks both numbers. find_modules('mpi4py') queries the modules tool for what is loaded on this machine. The parameter list is no longer two strings from last year’s Snellius tree. A site with a single mpi4py module yields one case. A site with four mpi4py modules yields four cases. valid_systems is no longer a hostname. The mixin requests the configuration features declared: CPU, GPU, and a minimum memory. A partition that does not advertise those features skips the test. That skip constitutes a result, and it is the correct result. required_mem_per_node follows the same concept as a residual in Inspect writes a work queue, not a recipe. The test defines a requirement. The framework compares the requirement to the partition. A mismatch results in a skip rather than a job that OOM‑kills the node.

The configuration is now the only machine file

The configuration file is the machine’s processor.num_cpus and features are the values read by these hooks. A site that omits them produces tests that cannot compute a task count, causing them to skip or fall back to a conservative option. A site that misrepresents the configuration causes tests to follow the same misrepresentation. The portable suite reflects the accuracy of that file. CSCS’s webinar demonstrates the same expansion: STREAM as a local binary, followed by the same class on any partition described by the configuration. ExCALIBUR-tests applies this in item 3 of its outline, “Configuring ReFrame to run tests on HPC systems”, before creating a second test. The order conveys the lesson. The machine is entered into the configuration first.

EB-ReFrame-45-1 — What to move

A GROMACS test contains these four assignments. For each, say whether it stays in the test, moves to the configuration, or becomes a hook that reads the configuration.

  1. executable = 'gmx'

  2. valid_systems = ['snellius:gpu']

  3. num_tasks = 128

  4. modules = ['GROMACS/2024.4-foss-2023b']

Solution
  1. Stays. The workload names the binary.

  2. Moves. The configuration marks the GPU partition with a gpu feature. The test requests +gpu. Binding the cluster name makes every other site skip the test.

  3. Becomes a hook. compute_unit of one node, or of one GPU, reads num_cpus or the device count from the partition. 128 is Snellius.

  4. Becomes parameter(find_modules('GROMACS')), or a single find_modules result if the suite is checking one installation. The version string is a fact about one tree.

What to remember

  • A cluster name, a core count and a module string in the test are three facts about one machine.

  • The mixin reads the partition. “One full node” is a request; the configuration supplies the number.

  • find_modules builds the parameter list from the tree that is actually loaded.

  • A missing feature or too little memory is a skip, not a failed job.