Skip to content

[Mellanox] Add thermalctld-rs, the Rust thermal daemon and its native platform API - #29197

Open
ganglyu wants to merge 2 commits into
sonic-net:masterfrom
ganglyu:rust-thermalctld-mlnx
Open

ganglyu wants to merge 2 commits into
sonic-net:masterfrom
ganglyu:rust-thermalctld-mlnx

Conversation

@ganglyu

@ganglyu ganglyu commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Why I did it

thermalctld polls thermal and fan sensors in the pmon container and writes the results to STATE_DB. In Python it imports sonic_platform, which loads the vendor plugin at startup and keeps a full interpreter resident for the life of the container. The Mellanox plugin reads hw-management sysfs — pure file I/O, no native-library requirement — so a Rust implementation does the same work with a fraction of the memory: 3.3 MB of anonymous memory against 23.3 MB, measured on the same device.

Nothing is switched on for a platform that does not opt in. The Python thermalctld stays installed and stays the default; which one runs is a key in pmon_daemon_control.json and a pmon restart, with no rebuild.

Work item tracking
  • Microsoft ADO (number only): N/A

PR:

PR title state context
[sonic-platform-common] Add crates/platform-traits, the Rust half of the platform API GitHub issue/pull request detail GitHub pull request check contexts
[sonic-platform-daemons] Add crates/thermalctld, a Rust thermalctld beside the Python daemon GitHub issue/pull request detail GitHub pull request check contexts

Both must merge and the submodules advance before this builds. No submodule pointer is moved here.

How I did it

The daemon and the vendor-neutral traits it compiles against live in the two submodules above. This PR adds the build glue and the two vendor crates:

  • platform/mellanox/mlnx-platform-api-rs — the Mellanox platform API, sitting next to the Python mlnx-platform-api rather than under src/, because it is vendor code. Ports device_data.py, fan.py, fan_drawer.py, thermal.py, PSU and PDB, the fan-drawer LEDs, the SPC1 pre-feed step, liquid-cooling and leak sensors, the SmartSwitch DPU feed and ThermalUpdater.
  • platform/mellanox/hw-management-rs — the writer half of hw-management's independent-mode interface, a port of hw_management_independent_mode_update.py and the DPU thermal helpers. hw-management publishes its file layout to Python only, so this is a copy of that layout rather than a call into it.
  • rules/sonic-thermalctld-rs.{mk,dep} — the package. Names no vendor and no platform; on a platform that does not opt in it is never built and never enters the pmon image.

A vendor opts in from its own rules.mk, and these two lines are the whole coupling:

$(SONIC_THERMALCTLD_RS)_BUILD_ENV = VENDOR_PLATFORM_RS_PATH=$(PROJECT_ROOT)/$(PLATFORM_PATH)/mlnx-platform-api-rs
$(DOCKER_PLATFORM_MONITOR)_DEPENDS += $(SONIC_THERMALCTLD_RS)

The vendor crate is compiled from source into the daemon — no prebuilt library, no ABI boundary. mlnx-platform-api-rs.mk also adds a SONIC_MAKE_FILES target so the crate can be built and unit-tested on its own, and appends it to $(DOCKER_PLATFORM_MONITOR)_FILES, so an ordinary pmon build runs the vendor tests too.

Behaviour matches the Python daemon throughout: per-component polling intervals from platform.json; ASIC, module and DPU feed cadences from tc_config.json's dev_parameters with the same regex semantics; fan drawer LEDs including the capability file and the _blink suffix; TEMPERATURE_INFO_{slot} on CHASSIS_STATE_DB rather than STATE_DB; leak detection on its own interval; Switch-BMC mirroring on the host and event-log teeing on the BMC; liquid-cooled platforms running no ASIC/module feed and never writing suspend.

One deliberate difference: the release profile sets panic = "abort", so nothing unwinds and Drop never runs. A panic hook clears the thermal data and suspends hw-management-tc instead, chaining to the previous hook so the message is still logged. Python's main loop has no try/finally and skips the suspend write on an unhandled exception, leaving tc running against cleared data.

How to verify it

# Build and unit-test the vendor crate on its own
make target/files/$(BLDENV)/mlnx-platform-api-rs

# Build the package and the pmon image
make target/debs/$(BLDENV)/sonic-thermalctld-rs_1.0.0_amd64.deb
make target/docker-platform-monitor.gz

On an opted-in platform, set thermalctld-rs in pmon_daemon_control.json, restart pmon, and compare TEMPERATURE_INFO, FAN_INFO, FAN_DRAWER_INFO and PHYSICAL_ENTITY_INFO against the Python daemon — key counts and field values are identical. Platforms that do not opt in are unaffected: the package is not built and pmon is unchanged.

Which release branch to backport (provide reason below if selected)

  • 202305
  • 202311
  • 202405
  • 202411
  • 202505
  • 202511
  • 202512
  • 202605
  • 202608

N/A — new feature, no backport requested.

Tested branch

  • master

Description for the changelog

Add thermalctld-rs, a Rust thermal daemon with a native Mellanox platform API; the Python thermalctld stays installed and stays the default.

Link to config_db schema for YANG module changes

N/A — no CONFIG_DB schema change.

… platform API

`thermalctld` polls thermal and fan sensors in the pmon container and writes the
results to STATE_DB. In Python it imports `sonic_platform`, which loads the
vendor plugin at startup and keeps a full interpreter resident for the life of
the container. The Mellanox plugin reads hw-management sysfs -- pure file I/O
with no native-library requirement -- so a Rust implementation does the same
work with a fraction of the memory.

This adds the build glue and the two vendor crates. The daemon itself and the
vendor-neutral traits it compiles against live in the sonic-platform-daemons
and sonic-platform-common submodules.

  * platform/mellanox/mlnx-platform-api-rs -- the Mellanox platform API, sitting
    next to the Python mlnx-platform-api rather than under src/, because it is
    vendor code. Ports device_data.py, fan.py, fan_drawer.py, thermal.py, psu,
    pdb, the fan-drawer LEDs, the SPC1 pre-feed step, liquid-cooling and leak
    sensors, the SmartSwitch DPU feed and ThermalUpdater.
  * platform/mellanox/hw-management-rs -- the writer half of hw-management's
    independent-mode interface, a port of
    hw_management_independent_mode_update.py and the DPU thermal helpers.
    hw-management publishes its file layout to Python only, so this is a copy of
    that layout rather than a call into it.
  * rules/sonic-thermalctld-rs.{mk,dep} -- the package. Names no vendor and no
    platform, and on a platform that does not opt in it is never built and never
    enters the pmon image.

A vendor opts in from its own rules.mk, and those two lines are the whole
coupling:

    $(SONIC_THERMALCTLD_RS)_BUILD_ENV = VENDOR_PLATFORM_RS_PATH=$(PROJECT_ROOT)/$(PLATFORM_PATH)/mlnx-platform-api-rs
    $(DOCKER_PLATFORM_MONITOR)_DEPENDS += $(SONIC_THERMALCTLD_RS)

The vendor crate is compiled from source into the daemon: no prebuilt library
and no ABI boundary. `mlnx-platform-api-rs.mk` adds a SONIC_MAKE_FILES target so
it can be built and unit-tested on its own -- the counterpart of building the
Python platform API's wheel -- and appends it to
$(DOCKER_PLATFORM_MONITOR)_FILES, so an ordinary pmon build runs the vendor
tests too.

Behaviour beyond the base thermal/fan feed, all of it matching the Python
daemon: per-component polling intervals from platform.json; ASIC, module and DPU
feed cadences from tc_config.json's dev_parameters, matched with the same regex
semantics; fan drawer LEDs including the capability file and the _blink suffix;
module vendor data and the module counter, cleared once when a module goes
absent; TEMPERATURE_INFO_{slot} on CHASSIS_STATE_DB -- not STATE_DB -- for a
modular chassis or a SmartSwitch DPU; leak detection on its own interval;
Switch-BMC mirroring on the host and event-log teeing on the BMC; the
SmartSwitch DPU thermal feed, where an offline DPU is cleared once on the
transition and an unparsable field is written as a fault code rather than a
plausible zero; and liquid-cooled platforms running no ASIC/module feed and
never writing suspend, matching thermal_manager.py's three-way branch.

Two details worth calling out. Python clears every ASIC's and module's thermal
data from an atexit handler (thermal_updater.py:64-81, :92) and suspends
hw-management-tc in deinit(); both are reproduced in that order. The release
profile sets panic = "abort", so nothing unwinds and Drop never runs -- a panic
hook does both instead and chains to the previous hook so the message is still
logged. It is installed only where the feed actually starts, so a liquid-cooled
platform is unaffected. This is the one place the port deliberately does more
than Python, whose main loop has no try/finally and so skips the suspend write
on an unhandled exception, leaving tc running against cleared data.

The other is the absent-device rule: Python withholds a device's derived fields
when the device could not be answered. An absent fan writes N/A for speed,
speed_target, is_under_speed, is_over_speed, status and direction; an unreadable
sensor writes N/A for its recorded extremes and all four thresholds. Both row
builders do the same.

pmon_daemon_control.json on the opted-in platforms selects which daemon runs, so
switching back is a key and a pmon restart with no rebuild. The Python
thermalctld stays installed and stays the default.

Signed-off-by: ganglyu <glv@nvidia.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run Azure.sonic-buildimage

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

platform/mellanox/rustfmt.toml and ci-* targets in mlnx-platform-api-rs's
Makefile, covering both Rust crates here -- rustfmt finds the config by walking
up, so one file serves both. The policy and the target names are
sonic-dash-ha's, which is the only other Rust in SONiC, so a contributor moving
between the two repositories runs the same commands and reads the same style.

`make ci-all` is `cargo fmt --check`, clippy with clippy::all denied, build and
doc with warnings denied, and the tests -- the last three in debug and release,
because a lint or a cfg can differ between the two and what ships is release.
The targets sit beside the DEST rule rather than replacing it: nothing in the
image depends on them, so they run on a workstation without a DEST.

Two deviations from sonic-dash-ha's Makefile, both deliberate. It runs
`cargo clean` between the debug and release passes; that is a disk-space
measure for its agent pool, and here it would throw away a contributor's build
cache on every run, so it is omitted -- the two use separate target
directories. And ci-doc passes --no-deps, so the verdict is about this
platform's documentation rather than its dependencies'.

The rest of the diff is rustfmt output over hand-formatted code: re-wrapping,
the trailing commas rustfmt adds when it breaks a literal, and the mod/use
reordering reorder_imports asks for. No behaviour changes; clippy is clean and
the 224 tests here and the 17 in hw-management-rs pass in debug and release.

Signed-off-by: ganglyu <glv@nvidia.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run Azure.sonic-buildimage

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

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.

2 participants