| /* |
| * SPDX-FileCopyrightText: Copyright OpenBMC Authors |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include "NvidiaGpuClockSpeedControl.hpp" |
| |
| #include "Inventory.hpp" |
| #include "NvidiaGpuControlErrors.hpp" |
| #include "NvidiaUtils.hpp" |
| #include "Utils.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 <utility> |
| #include <vector> |
| |
| static constexpr const char* controlClockSpeedIfaceName = |
| "xyz.openbmc_project.Control.OperatingClockSpeed"; |
| static constexpr const char* controlClockSpeedPrefix = |
| "/xyz/openbmc_project/control/operatingclockspeed/"; |
| |
| static constexpr uint64_t mhzToHzFactor = 1'000'000; |
| |
| // Debounce window to coalesce the RequestedSpeedLimitMin and Max writes of a |
| // single PATCH (which arrive back to back) into one SetClockLimit. |
| static constexpr std::chrono::milliseconds setLimitDebounce{100}; |
| |
| NvidiaGpuClockSpeedControl::NvidiaGpuClockSpeedControl( |
| sdbusplus::asio::object_server& objectServer, const std::string& deviceName, |
| mctp::MctpRequester& mctpRequester, uint8_t eid, |
| boost::asio::io_context& io, std::shared_ptr<Inventory> inventory) : |
| inventory(std::move(inventory)), mctpRequester(mctpRequester), |
| name(escapeName(deviceName)), objectServer(objectServer), setLimitTimer(io), |
| eid(eid) |
| { |
| const int rc = gpu::encodeGetClockLimitRequest( |
| 0, gpu::ClockType::GRAPHICS_CLOCK, requestBuffer); |
| if (rc == 0) |
| { |
| requestEncoded = true; |
| } |
| else |
| { |
| lg2::error("Failed to encode clock limit request for {NAME}: rc={RC}", |
| "NAME", name, "RC", rc); |
| } |
| |
| const sdbusplus::object_path inventoryPath = inventoryPrefix / name; |
| const sdbusplus::object_path objPath(controlClockSpeedPrefix + name); |
| |
| controlClockSpeedInterface = |
| objectServer.add_interface(objPath.str, controlClockSpeedIfaceName); |
| |
| associationInterface = |
| objectServer.add_interface(objPath, 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 Clock Speed Control for {NAME}, eid={EID}", |
| "NAME", name, "EID", eid); |
| } |
| |
| controlClockSpeedInterface->register_property( |
| "PresentSpeedLimitMaxHz", std::numeric_limits<uint64_t>::max(), |
| sdbusplus::asio::PropertyPermission::readOnly); |
| controlClockSpeedInterface->register_property( |
| "PresentSpeedLimitMinHz", uint64_t{0}, |
| sdbusplus::asio::PropertyPermission::readOnly); |
| |
| controlClockSpeedInterface->register_property<uint64_t>( |
| "RequestedSpeedLimitMaxHz", std::numeric_limits<uint64_t>::max(), |
| std::bind_front( |
| &NvidiaGpuClockSpeedControl::handleRequestedSpeedLimitMaxHzSet, |
| this), |
| [this](uint64_t&) { return requestedMaxHz; }); |
| controlClockSpeedInterface->register_property<uint64_t>( |
| "RequestedSpeedLimitMinHz", std::numeric_limits<uint64_t>::max(), |
| std::bind_front( |
| &NvidiaGpuClockSpeedControl::handleRequestedSpeedLimitMinHzSet, |
| this), |
| [this](uint64_t&) { return requestedMinHz; }); |
| |
| if (!controlClockSpeedInterface->initialize()) |
| { |
| lg2::error( |
| "Error initializing OperatingClockSpeed interface for {NAME}, eid={EID}", |
| "NAME", name, "EID", eid); |
| } |
| } |
| |
| NvidiaGpuClockSpeedControl::~NvidiaGpuClockSpeedControl() |
| { |
| objectServer.remove_interface(associationInterface); |
| objectServer.remove_interface(controlClockSpeedInterface); |
| } |
| |
| void NvidiaGpuClockSpeedControl::update() |
| { |
| sendGetClockLimitRequest(); |
| } |
| |
| void NvidiaGpuClockSpeedControl::sendGetClockLimitRequest() |
| { |
| if (!requestEncoded) |
| { |
| return; |
| } |
| |
| mctpRequester.sendRecvMsg( |
| eid, requestBuffer, |
| [weak{weak_from_this()}](const std::error_code& ec, |
| std::span<const uint8_t> buffer) { |
| auto self = weak.lock(); |
| if (!self) |
| { |
| lg2::error("Invalid NvidiaGpuClockSpeedControl reference"); |
| return; |
| } |
| self->handleGetClockLimitResponse(ec, buffer); |
| }); |
| } |
| |
| void NvidiaGpuClockSpeedControl::handleGetClockLimitResponse( |
| const std::error_code& ec, std::span<const uint8_t> buffer) |
| { |
| if (setClockLimitReadbackPending) |
| { |
| // This response is the read-back for a completed SetClockLimit: the |
| // device's new state has now been observed, so open the coalesce gate. |
| // Done before the error returns below so a failed read-back cannot |
| // wedge the gate shut forever. Any writes that arrived while the set |
| // was in flight have re-armed setLimitTimer and dispatch on its next |
| // expiry, so no manual re-dispatch is needed here. |
| setClockLimitReadbackPending = false; |
| setClockLimitInflight = false; |
| } |
| |
| if (ec) |
| { |
| lg2::error("Error reading clock limit for {NAME}: MCTP failed, rc={RC}", |
| "NAME", name, "RC", ec.message()); |
| return; |
| } |
| |
| ocp::accelerator_management::CompletionCode cc{}; |
| uint16_t reasonCode = 0; |
| uint32_t requestedLimitMin = 0; |
| uint32_t requestedLimitMax = 0; |
| uint32_t presentLimitMin = 0; |
| uint32_t presentLimitMax = 0; |
| |
| int rc = gpu::decodeGetClockLimitResponse( |
| buffer, cc, reasonCode, requestedLimitMin, requestedLimitMax, |
| presentLimitMin, presentLimitMax); |
| |
| if (rc != 0 || cc != ocp::accelerator_management::CompletionCode::SUCCESS) |
| { |
| lg2::error( |
| "Error decoding clock limit for {NAME}: rc={RC}, cc={CC}, reasonCode={REASON}", |
| "NAME", name, "RC", rc, "CC", static_cast<uint8_t>(cc), "REASON", |
| reasonCode); |
| return; |
| } |
| |
| const uint64_t newPresentMaxHz = |
| static_cast<uint64_t>(presentLimitMax) * mhzToHzFactor; |
| const uint64_t newPresentMinHz = |
| static_cast<uint64_t>(presentLimitMin) * mhzToHzFactor; |
| const uint64_t newRequestedMaxHz = |
| static_cast<uint64_t>(requestedLimitMax) * mhzToHzFactor; |
| const uint64_t newRequestedMinHz = |
| static_cast<uint64_t>(requestedLimitMin) * mhzToHzFactor; |
| |
| // PresentSpeedLimit*Hz are read-only, so set_property updates the stored |
| // value and emits PropertiesChanged only when it actually changes. |
| // RequestedSpeedLimit*Hz are client-writable; updating them via |
| // set_property would re-enter the client set-handler, so the member is the |
| // source of truth and the change signal is emitted directly. |
| controlClockSpeedInterface->set_property("PresentSpeedLimitMaxHz", |
| newPresentMaxHz); |
| controlClockSpeedInterface->set_property("PresentSpeedLimitMinHz", |
| newPresentMinHz); |
| if (newRequestedMaxHz != requestedMaxHz) |
| { |
| requestedMaxHz = newRequestedMaxHz; |
| controlClockSpeedInterface->signal_property("RequestedSpeedLimitMaxHz"); |
| } |
| if (newRequestedMinHz != requestedMinHz) |
| { |
| requestedMinHz = newRequestedMinHz; |
| controlClockSpeedInterface->signal_property("RequestedSpeedLimitMinHz"); |
| } |
| } |
| |
| int NvidiaGpuClockSpeedControl::handleRequestedSpeedLimitMaxHzSet( |
| const uint64_t& newMaxHz, uint64_t& /*current*/) |
| { |
| if (!inventory) |
| { |
| lg2::error( |
| "RequestedSpeedLimitMaxHz set rejected for eid {EID}: inventory not available", |
| "EID", eid); |
| throw Unavailable(); |
| } |
| |
| std::optional<uint64_t> minHw = inventory->getMinSpeedInHz(); |
| std::optional<uint64_t> maxHw = inventory->getMaxSpeedInHz(); |
| if (!minHw || !maxHw) |
| { |
| lg2::error( |
| "RequestedSpeedLimitMaxHz set rejected for eid {EID}: hardware min/max not yet known", |
| "EID", eid); |
| throw Unavailable(); |
| } |
| |
| if (newMaxHz < *minHw || newMaxHz > *maxHw) |
| { |
| lg2::error( |
| "RequestedSpeedLimitMaxHz set rejected for eid {EID}: value {VAL} out of range [{MIN}, {MAX}]", |
| "EID", eid, "VAL", newMaxHz, "MIN", *minHw, "MAX", *maxHw); |
| throw InvalidArgument(); |
| } |
| |
| // Record the requested max and arm the debounce timer. The actual |
| // SetClockLimit is issued once the burst settles, so a PATCH that sets |
| // both RequestedSpeedLimitMin and Max is coalesced into a single request. |
| // Do not touch requestedMaxHz here: the D-Bus value is only updated once |
| // the device confirms via a GetClockLimit response. |
| pendingMaxHz = newMaxHz; |
| armSetLimitTimer(); |
| |
| return 1; |
| } |
| |
| int NvidiaGpuClockSpeedControl::handleRequestedSpeedLimitMinHzSet( |
| const uint64_t& newMinHz, uint64_t& /*current*/) |
| { |
| if (!inventory) |
| { |
| lg2::error( |
| "RequestedSpeedLimitMinHz set rejected for eid {EID}: inventory not available", |
| "EID", eid); |
| throw Unavailable(); |
| } |
| |
| std::optional<uint64_t> minHw = inventory->getMinSpeedInHz(); |
| std::optional<uint64_t> maxHw = inventory->getMaxSpeedInHz(); |
| if (!minHw || !maxHw) |
| { |
| lg2::error( |
| "RequestedSpeedLimitMinHz set rejected for eid {EID}: hardware min/max not yet known", |
| "EID", eid); |
| throw Unavailable(); |
| } |
| |
| if (newMinHz < *minHw || newMinHz > *maxHw) |
| { |
| lg2::error( |
| "RequestedSpeedLimitMinHz set rejected for eid {EID}: value {VAL} out of range [{MIN}, {MAX}]", |
| "EID", eid, "VAL", newMinHz, "MIN", *minHw, "MAX", *maxHw); |
| throw InvalidArgument(); |
| } |
| |
| pendingMinHz = newMinHz; |
| armSetLimitTimer(); |
| |
| return 1; |
| } |
| |
| void NvidiaGpuClockSpeedControl::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<NvidiaGpuClockSpeedControl> self = weak.lock(); |
| if (!self) |
| { |
| return; |
| } |
| self->applyPendingClockLimit(); |
| }); |
| } |
| |
| void NvidiaGpuClockSpeedControl::applyPendingClockLimit() |
| { |
| if (setClockLimitInflight) |
| { |
| // A previous SetClockLimit is still in flight; retry once it lands. |
| armSetLimitTimer(); |
| return; |
| } |
| |
| const uint64_t minHz = pendingMinHz.value_or(requestedMinHz); |
| const uint64_t maxHz = pendingMaxHz.value_or(requestedMaxHz); |
| pendingMinHz.reset(); |
| pendingMaxHz.reset(); |
| |
| sendSetClockLimitRequest(static_cast<uint32_t>(minHz / mhzToHzFactor), |
| static_cast<uint32_t>(maxHz / mhzToHzFactor)); |
| } |
| |
| void NvidiaGpuClockSpeedControl::sendSetClockLimitRequest(uint32_t limitMinMHz, |
| uint32_t limitMaxMHz) |
| { |
| const int rc = gpu::encodeSetClockLimitRequest( |
| 0, gpu::ClockType::GRAPHICS_CLOCK, gpu::ClockLimitFlag::PERSISTENCE, |
| limitMinMHz, limitMaxMHz, setClockLimitRequestBuffer); |
| |
| if (rc != 0) |
| { |
| lg2::error( |
| "Error encoding SET_CLOCK_LIMIT request for eid {EID}, rc={RC}", |
| "EID", eid, "RC", rc); |
| return; |
| } |
| |
| setClockLimitInflight = true; |
| |
| mctpRequester.sendRecvMsg( |
| eid, setClockLimitRequestBuffer, |
| [weak{weak_from_this()}](const std::error_code& ec, |
| std::span<const uint8_t> buffer) { |
| auto self = weak.lock(); |
| if (!self) |
| { |
| lg2::error("Invalid NvidiaGpuClockSpeedControl reference"); |
| return; |
| } |
| self->handleSetClockLimitResponse(ec, buffer); |
| }); |
| } |
| |
| void NvidiaGpuClockSpeedControl::handleSetClockLimitResponse( |
| const std::error_code& ec, std::span<const uint8_t> buffer) |
| { |
| if (ec) |
| { |
| lg2::error("Error setting clock limit for eid {EID}: {MSG}", "EID", eid, |
| "MSG", ec.message()); |
| setClockLimitInflight = false; |
| return; |
| } |
| |
| ocp::accelerator_management::CompletionCode cc{}; |
| uint16_t reasonCode = 0; |
| const int rc = gpu::decodeSetClockLimitResponse(buffer, cc, reasonCode); |
| |
| if (rc != 0 || cc != ocp::accelerator_management::CompletionCode::SUCCESS) |
| { |
| lg2::error( |
| "Error decoding SetClockLimit response for eid {EID}: rc={RC}, cc={CC}, reasonCode={RESC}", |
| "EID", eid, "RC", rc, "CC", static_cast<uint8_t>(cc), "RESC", |
| reasonCode); |
| setClockLimitInflight = false; |
| return; |
| } |
| |
| // The set succeeded; re-read from the device so requestedMin/MaxHz and |
| // presentMin/MaxHz reflect the new state without waiting for the next |
| // periodic poll. setClockLimitInflight stays set until this read-back's |
| // response lands so the coalesce gate does not reopen on stale state. |
| if (!requestEncoded) |
| { |
| // No read-back can be sent, so no response will ever arrive to reopen |
| // the coalesce gate; release it here to avoid wedging it shut. |
| setClockLimitInflight = false; |
| return; |
| } |
| setClockLimitReadbackPending = true; |
| sendGetClockLimitRequest(); |
| } |