| /* |
| * SPDX-FileCopyrightText: Copyright OpenBMC Authors |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include "NvidiaGpuPowerControl.hpp" |
| |
| #include "NvidiaGpuControlErrors.hpp" |
| #include "NvidiaUtils.hpp" |
| #include "Utils.hpp" |
| |
| #include <Inventory.hpp> |
| #include <MctpRequester.hpp> |
| #include <NvidiaGpuMctpVdm.hpp> |
| #include <OcpMctpVdm.hpp> |
| #include <boost/asio/error.hpp> |
| #include <boost/asio/io_context.hpp> |
| #include <phosphor-logging/lg2.hpp> |
| #include <sdbusplus/asio/object_server.hpp> |
| #include <sdbusplus/message/native_types.hpp> |
| |
| #include <chrono> |
| #include <cstdint> |
| #include <functional> |
| #include <limits> |
| #include <memory> |
| #include <optional> |
| #include <span> |
| #include <string> |
| #include <system_error> |
| #include <vector> |
| |
| constexpr const char* controlPowerPrefix = |
| "/xyz/openbmc_project/control/power/"; |
| |
| constexpr uint32_t milliwattsPerWatt = 1000; |
| constexpr uint32_t unsetPowerLimit = std::numeric_limits<uint32_t>::max(); |
| |
| // Debounce window to coalesce the PowerCap and PowerCapEnable writes of a |
| // single PATCH (which arrive back to back) into one SetPowerLimits. |
| constexpr std::chrono::milliseconds setLimitDebounce{100}; |
| |
| NvidiaGpuPowerControl::NvidiaGpuPowerControl( |
| sdbusplus::asio::object_server& objectServer, const std::string& deviceName, |
| mctp::MctpRequester& mctpRequester, uint8_t eid, |
| boost::asio::io_context& io, |
| const std::shared_ptr<sdbusplus::asio::dbus_interface>& powerCapIface, |
| const std::shared_ptr<Inventory>& inventory) : |
| powerCapInterface(powerCapIface), inventory(inventory), |
| name(escapeName(deviceName)), objectServer(objectServer), |
| mctpRequester(mctpRequester), setLimitTimer(io), eid(eid) |
| { |
| constexpr uint32_t devicePowerLimitId = 0; |
| const int rc = gpu::encodeGetPowerLimitsRequest( |
| 0, devicePowerLimitId, getPowerLimitsRequestBuffer); |
| if (rc == 0) |
| { |
| requestEncoded = true; |
| } |
| else |
| { |
| lg2::error( |
| "Failed to encode GET_POWER_LIMITS request for eid {EID}, rc={RC}", |
| "EID", eid, "RC", rc); |
| } |
| |
| const std::string powerControlPath = controlPowerPrefix + name; |
| const sdbusplus::object_path inventoryPath = inventoryPrefix / name; |
| |
| associationInterface = |
| objectServer.add_interface(powerControlPath, association::interface); |
| |
| std::vector<Association> associations; |
| associations.emplace_back("controlling", "controlled_by", inventoryPath); |
| |
| associationInterface->register_property("Associations", associations); |
| if (!associationInterface->initialize()) |
| { |
| lg2::error( |
| "Error initializing Association interface for GPU Control for {NAME}, eid={EID}", |
| "NAME", name, "EID", eid); |
| } |
| |
| powerCapInterface->register_property<uint32_t>( |
| "PowerCap", std::numeric_limits<uint32_t>::max(), |
| std::bind_front(&NvidiaGpuPowerControl::handlePowerCapSet, this), |
| [this](uint32_t&) { return powerCapValue; }); |
| powerCapInterface->register_property<bool>( |
| "PowerCapEnable", false, |
| std::bind_front(&NvidiaGpuPowerControl::handlePowerCapEnableSet, this), |
| [this](bool&) { return powerCapEnabled; }); |
| if (!powerCapInterface->initialize()) |
| { |
| lg2::error( |
| "Error initializing Power Cap interface for {NAME}, eid={EID}", |
| "NAME", name, "EID", eid); |
| } |
| } |
| |
| NvidiaGpuPowerControl::~NvidiaGpuPowerControl() |
| { |
| objectServer.remove_interface(associationInterface); |
| } |
| |
| void NvidiaGpuPowerControl::update() |
| { |
| sendGetPowerLimitsRequest(); |
| } |
| |
| void NvidiaGpuPowerControl::sendGetPowerLimitsRequest() |
| { |
| if (!requestEncoded) |
| { |
| return; |
| } |
| |
| mctpRequester.sendRecvMsg( |
| eid, getPowerLimitsRequestBuffer, |
| [weak{weak_from_this()}](const std::error_code& ec, |
| std::span<const uint8_t> buffer) { |
| std::shared_ptr<NvidiaGpuPowerControl> self = weak.lock(); |
| if (!self) |
| { |
| lg2::error("Invalid NvidiaGpuPowerControl reference"); |
| return; |
| } |
| self->handleGetPowerLimitsResponse(ec, buffer); |
| }); |
| } |
| |
| void NvidiaGpuPowerControl::handleGetPowerLimitsResponse( |
| const std::error_code& ec, std::span<const uint8_t> buffer) |
| { |
| if (ec) |
| { |
| lg2::error("Error getting power limits for eid {EID}: {MSG}", "EID", |
| eid, "MSG", ec.message()); |
| return; |
| } |
| |
| ocp::accelerator_management::CompletionCode cc{}; |
| uint16_t reasonCode = 0; |
| uint32_t persistentPowerLimit = 0; |
| uint32_t oneshotPowerLimit = 0; |
| uint32_t enforcedLimit = 0; |
| |
| const int rc = gpu::decodeGetPowerLimitsResponse( |
| buffer, cc, reasonCode, persistentPowerLimit, oneshotPowerLimit, |
| enforcedLimit); |
| |
| if (rc != 0 || cc != ocp::accelerator_management::CompletionCode::SUCCESS) |
| { |
| lg2::error( |
| "Error decoding power limits for eid {EID}: rc={RC}, cc={CC}, reasonCode={RESC}", |
| "EID", eid, "RC", rc, "CC", static_cast<uint8_t>(cc), "RESC", |
| reasonCode); |
| return; |
| } |
| |
| // PDI defines PowerCap as the user-specified value (maxint if the |
| // user has not set one). The Set path only ever writes the one-shot |
| // limit, so the read path mirrors that and reports the one-shot |
| // requested limit (maxint if unset), ignoring the persistent limit. |
| const uint32_t newPowerCapValue = |
| (oneshotPowerLimit != unsetPowerLimit) |
| ? oneshotPowerLimit / milliwattsPerWatt |
| : unsetPowerLimit; |
| |
| // PowerCapEnable is true only when the one-shot cap is the limit |
| // currently being enforced. Guard against the case where enforcedLimit |
| // is the unset sentinel: a matching unset one-shot would otherwise |
| // compare equal and incorrectly report the cap as enabled. |
| const bool newPowerCapEnabled = (enforcedLimit != unsetPowerLimit) && |
| (oneshotPowerLimit == enforcedLimit); |
| |
| // Only emit PropertiesChanged when the value actually changes; this |
| // handler runs on every poll. |
| if (newPowerCapValue != powerCapValue) |
| { |
| powerCapValue = newPowerCapValue; |
| powerCapInterface->signal_property("PowerCap"); |
| } |
| |
| if (newPowerCapEnabled != powerCapEnabled) |
| { |
| powerCapEnabled = newPowerCapEnabled; |
| powerCapInterface->signal_property("PowerCapEnable"); |
| } |
| } |
| |
| int NvidiaGpuPowerControl::handlePowerCapSet(const uint32_t& newCap, |
| uint32_t& /*current*/) |
| { |
| if (!inventory) |
| { |
| lg2::error( |
| "PowerCap set rejected for eid {EID}: inventory not available", |
| "EID", eid); |
| throw Unavailable(); |
| } |
| |
| std::optional<uint32_t> min = inventory->getMinPowerCap(); |
| std::optional<uint32_t> max = inventory->getMaxPowerCap(); |
| if (!min || !max) |
| { |
| lg2::error("PowerCap set rejected for eid {EID}: min/max not yet known", |
| "EID", eid); |
| throw Unavailable(); |
| } |
| |
| if (newCap < *min || newCap > *max) |
| { |
| lg2::error( |
| "PowerCap set rejected for eid {EID}: value {VAL} out of range [{MIN}, {MAX}]", |
| "EID", eid, "VAL", newCap, "MIN", *min, "MAX", *max); |
| throw InvalidArgument(); |
| } |
| |
| // Record the requested cap and arm the debounce timer. The actual |
| // SetPowerLimits is issued once the burst settles, so a PATCH that sets |
| // both PowerCap and PowerCapEnable is coalesced into a single request. |
| // Do not touch powerCapValue here: the D-Bus value is only updated once |
| // the device confirms via a GetPowerLimits response. |
| requestedPowerCapWatts = newCap; |
| armSetLimitTimer(); |
| |
| return 1; |
| } |
| |
| int NvidiaGpuPowerControl::handlePowerCapEnableSet(const bool& newEnable, |
| bool& /*current*/) |
| { |
| // Record the requested enable state and arm the debounce timer. Do not |
| // touch powerCapEnabled here: the D-Bus value is only updated from a |
| // GetPowerLimits response. |
| pendingEnable = newEnable; |
| armSetLimitTimer(); |
| |
| return 1; |
| } |
| |
| void NvidiaGpuPowerControl::armSetLimitTimer() |
| { |
| setLimitTimer.expires_after(setLimitDebounce); |
| setLimitTimer.async_wait( |
| [weak{weak_from_this()}](const boost::system::error_code& ec) { |
| if (ec == boost::asio::error::operation_aborted) |
| { |
| // Superseded by a later set within the debounce window. |
| return; |
| } |
| std::shared_ptr<NvidiaGpuPowerControl> self = weak.lock(); |
| if (!self) |
| { |
| return; |
| } |
| self->applyPendingPowerLimit(); |
| }); |
| } |
| |
| void NvidiaGpuPowerControl::applyPendingPowerLimit() |
| { |
| if (setPowerLimitInflight) |
| { |
| // A previous SetPowerLimits is still in flight; retry once it lands. |
| armSetLimitTimer(); |
| return; |
| } |
| |
| const bool enable = pendingEnable.value_or(powerCapEnabled); |
| const bool enableChanged = pendingEnable.has_value(); |
| |
| // Snapshot the cap to enforce before clearing: prefer the explicitly |
| // requested cap, otherwise fall back to the device's current value. |
| // powerCapValue is the D-Bus sentinel (unset) when the device reports |
| // no cap. |
| std::optional<uint32_t> capToEnforce = requestedPowerCapWatts; |
| if (!capToEnforce && powerCapValue != unsetPowerLimit) |
| { |
| capToEnforce = powerCapValue; |
| } |
| |
| // The burst is consumed; clear both pending requests so a cap set while |
| // disabled is not silently re-applied on a later, unrelated enable. |
| pendingEnable.reset(); |
| requestedPowerCapWatts.reset(); |
| |
| if (enable) |
| { |
| if (!capToEnforce) |
| { |
| // Nothing to enforce. Skip rather than send the unset sentinel |
| // (which would overflow when scaled to milliwatts). |
| lg2::error( |
| "PowerCapEnable set ignored for eid {EID}: no power cap value available", |
| "EID", eid); |
| return; |
| } |
| sendSetPowerLimitsRequest(*capToEnforce * milliwattsPerWatt, |
| gpu::SetPowerLimitsAction::NEW_LIMIT); |
| } |
| else if (enableChanged) |
| { |
| // Explicit disable: revert to the firmware default limit. |
| sendSetPowerLimitsRequest(0, gpu::SetPowerLimitsAction::DEFAULT_LIMIT); |
| } |
| // else: disabled and only the cap changed; nothing to enforce. |
| } |
| |
| void NvidiaGpuPowerControl::sendSetPowerLimitsRequest( |
| uint32_t milliwatts, gpu::SetPowerLimitsAction action) |
| { |
| constexpr uint32_t devicePowerLimitId = 0; |
| |
| const int rc = gpu::encodeSetPowerLimitsRequest( |
| 0, devicePowerLimitId, action, gpu::SetPowerLimitsPersistence::ONE_SHOT, |
| milliwatts, setPowerLimitsRequestBuffer); |
| |
| if (rc != 0) |
| { |
| lg2::error( |
| "Error encoding SET_POWER_LIMITS request for eid {EID}, rc={RC}", |
| "EID", eid, "RC", rc); |
| return; |
| } |
| |
| setPowerLimitInflight = true; |
| |
| mctpRequester.sendRecvMsg( |
| eid, setPowerLimitsRequestBuffer, |
| [weak{weak_from_this()}](const std::error_code& ec, |
| std::span<const uint8_t> buffer) { |
| std::shared_ptr<NvidiaGpuPowerControl> self = weak.lock(); |
| if (!self) |
| { |
| lg2::error("Invalid NvidiaGpuPowerControl reference"); |
| return; |
| } |
| self->handleSetPowerLimitsResponse(ec, buffer); |
| }); |
| } |
| |
| void NvidiaGpuPowerControl::handleSetPowerLimitsResponse( |
| const std::error_code& ec, std::span<const uint8_t> buffer) |
| { |
| setPowerLimitInflight = false; |
| |
| if (ec) |
| { |
| lg2::error("Error setting power limits for eid {EID}: {MSG}", "EID", |
| eid, "MSG", ec.message()); |
| return; |
| } |
| |
| ocp::accelerator_management::CompletionCode cc{}; |
| uint16_t reasonCode = 0; |
| const int rc = gpu::decodeSetPowerLimitsResponse(buffer, cc, reasonCode); |
| |
| if (rc != 0 || cc != ocp::accelerator_management::CompletionCode::SUCCESS) |
| { |
| lg2::error( |
| "Error decoding SetPowerLimits response for eid {EID}: rc={RC}, cc={CC}, reasonCode={RESC}", |
| "EID", eid, "RC", rc, "CC", static_cast<uint8_t>(cc), "RESC", |
| reasonCode); |
| return; |
| } |
| |
| // The set succeeded; re-read from the device so powerCapValue / |
| // powerCapEnabled reflect the new state without waiting for the next |
| // periodic poll. |
| sendGetPowerLimitsRequest(); |
| } |