blob: 79a6fd0eb5fb4e866b55bd4eb1dff1ff7e90df68 [file] [edit]
/*
* 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();
}