From 7908233dbb34eed0d9f076f0c4bdb13695ada948 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Thu, 1 Oct 2026 00:05:11 -0400 Subject: gsp-phys: keep the dGPU power domain in D0 across the claim/release handoff --- kernel/gsp_phys_gpu.c | 262 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 250 insertions(+), 12 deletions(-) (limited to 'kernel') diff --git a/kernel/gsp_phys_gpu.c b/kernel/gsp_phys_gpu.c index 24ed887..f9fffc4 100644 --- a/kernel/gsp_phys_gpu.c +++ b/kernel/gsp_phys_gpu.c @@ -19,6 +19,17 @@ * of the cycles that survived). Nothing in the module's own code * crashed; the trigger is the loss of the dGPU power domain. * + * Mitigation (implemented here): before the unbind, the whole board - the + * GPU, its same-vendor sibling functions on the bus (audio, xHCI, UCSI) + * and the upstream root port - is locked into D0 with + * pm_runtime_forbid() and pci_d3cold_disable(). The locks are held across + * the whole unbind -> claim -> release window. The GPU config ID and the + * root-port link status are re-read after the claim and again before + * rebinding the distribution driver; an unresponsive board aborts the + * handoff and the locks are deliberately left in place. Residual risk: a + * platform power cut that is not driven by PCI D3 (EC/SMI or thermal) has + * the same signature; if it happens, do not retry in that boot. + * * Therefore claim now requires BOTH gsp_phys_gpu=1 AND * gsp_phys_hazard_ack=1, and must not be enabled without a reviewed * recovery plan. The normal emulation path (module load, mdev, VM) does @@ -32,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -89,6 +101,21 @@ MODULE_PARM_DESC(gsp_phys_hazard_ack, #define GSP_PHYS_CPUCTL_HALTED BIT(4) #define GSP_PHYS_RISCV_ACTIVE BIT(0) +/* Settle time after the distribution driver probes before releasing D0. */ +#define GSP_PHYS_POWER_SETTLE_MS 1500 +/* GPU + up to 7 sibling functions + upstream root port. */ +#define GSP_PHYS_MAX_POWER_LOCKS 16 + +/* + * One device held in D0 across the claim window. The pre-lock runtime-PM + * and D3cold policy is recorded so the unlock restores exactly that policy. + */ +struct gsp_phys_power_lock { + struct pci_dev *pdev; + bool was_runtime_auto; + bool was_d3cold_enabled; +}; + struct gsp_phys_gpu { struct pci_dev *pdev; void __iomem *bar0; @@ -106,6 +133,8 @@ struct gsp_phys_gpu { bool drv_registered; bool claimed; struct gsp_phys_boot boot; + struct gsp_phys_power_lock locks[GSP_PHYS_MAX_POWER_LOCKS]; + unsigned int nr_locks; }; static struct gsp_phys_gpu phys; @@ -373,6 +402,170 @@ static struct pci_driver gsp_phys_driver = { .remove = gsp_phys_remove, }; +/* + * Power-domain lock: the board's ACPI power resource hangs off the root + * port, so a D3 transition of the root port (or of the bridge below it) + * cuts the whole board rail. A driverless GPU would let the root port + * autosuspend. Forbid runtime PM and D3cold on the GPU, its sibling + * functions and the root port before unbinding anything; both knobs live + * in struct device and survive driver unbind. Only the exact devices that + * were locked are unlocked, restoring their pre-lock policy. + */ +static bool gsp_phys_power_lock_one(struct pci_dev *pdev) +{ + struct gsp_phys_power_lock *lock; + + if (phys.nr_locks >= ARRAY_SIZE(phys.locks)) { + pr_warn("gsp-phys: power-lock table full, %s left unlocked\n", + pci_name(pdev)); + return false; + } + + lock = &phys.locks[phys.nr_locks++]; + lock->pdev = pci_dev_get(pdev); + lock->was_runtime_auto = pdev->dev.power.runtime_auto; + lock->was_d3cold_enabled = !pdev->no_d3cold; + + pm_runtime_forbid(&pdev->dev); + pci_d3cold_disable(pdev); + + pr_info("gsp-phys: power domain locked: %s (runtime_auto was %d, d3cold was %d)\n", + pci_name(pdev), lock->was_runtime_auto, + lock->was_d3cold_enabled); + return true; +} + +static int gsp_phys_power_lock(struct pci_dev *pdev) +{ + struct pci_dev *dev; + int fn; + + /* The GPU must go first: if it cannot be locked, do not unbind. */ + if (!gsp_phys_power_lock_one(pdev)) + return -ENOSPC; + + /* Sibling functions of the same board (audio, xHCI, UCSI, ...). */ + for (fn = 0; fn < 8; fn++) { + dev = pci_get_slot(pdev->bus, + PCI_DEVFN(PCI_SLOT(pdev->devfn), fn)); + if (!dev) + continue; + if (dev != pdev && dev->vendor == GSP_PHYS_VENDOR) + gsp_phys_power_lock_one(dev); + pci_dev_put(dev); + } + + /* The root port whose D3 triggers the platform board power switch. */ + dev = pci_upstream_bridge(pdev); + if (dev) + gsp_phys_power_lock_one(dev); + + return 0; +} + +static void gsp_phys_power_unlock(void) +{ + while (phys.nr_locks) { + struct gsp_phys_power_lock *lock = + &phys.locks[--phys.nr_locks]; + + if (lock->was_runtime_auto) + pm_runtime_allow(&lock->pdev->dev); + if (lock->was_d3cold_enabled) + pci_d3cold_enable(lock->pdev); + + pr_info("gsp-phys: power domain released: %s\n", + pci_name(lock->pdev)); + pci_dev_put(lock->pdev); + lock->pdev = NULL; + } +} + +static const char *gsp_phys_link_speed(u16 lnksta) +{ + switch (lnksta & PCI_EXP_LNKSTA_CLS) { + case PCI_EXP_LNKSTA_CLS_2_5GB: + return "2.5GT/s"; + case PCI_EXP_LNKSTA_CLS_5_0GB: + return "5.0GT/s"; + case PCI_EXP_LNKSTA_CLS_8_0GB: + return "8.0GT/s"; + case PCI_EXP_LNKSTA_CLS_16_0GB: + return "16.0GT/s"; + default: + return "unknown"; + } +} + +/* + * Abort signal for the handoff. The board must answer config reads while + * it is claimed and again just before it is handed back; an invalid ID + * means the power domain was cut (or never came back) and neither the + * deeper boot work nor the distribution driver's power-up path may run. + */ +static int gsp_phys_board_check(struct pci_dev *pdev, const char *phase) +{ + struct pci_dev *root = pci_upstream_bridge(pdev); + u32 id = 0; + u16 lnksta = 0; + int ret; + + ret = pci_read_config_dword(pdev, PCI_VENDOR_ID, &id); + if (ret != PCIBIOS_SUCCESSFUL || id == 0xffffffff || + (id & 0xffff) == 0x0000) { + WARN(1, "gsp-phys: board unresponsive %s: %s config id=0x%08x rc=%d; aborting - do not retry in this boot\n", + phase, pci_name(pdev), id, ret); + return -EIO; + } + + if (root && pci_is_pcie(root)) { + ret = pcie_capability_read_word(root, PCI_EXP_LNKSTA, &lnksta); + if (ret != PCIBIOS_SUCCESSFUL) + pr_warn("gsp-phys: %s: root port %s link status read failed (rc=%d)\n", + phase, pci_name(root), ret); + else + pr_info("gsp-phys: board check %s: %s id=%04x:%04x, root %s link speed %s width x%u\n", + phase, pci_name(pdev), id & 0xffff, id >> 16, + pci_name(root), gsp_phys_link_speed(lnksta), + (lnksta & PCI_EXP_LNKSTA_NLW) >> + PCI_EXP_LNKSTA_NLW_SHIFT); + } else { + pr_info("gsp-phys: board check %s: %s id=%04x:%04x (no PCIe root port)\n", + phase, pci_name(pdev), id & 0xffff, id >> 16); + } + + return 0; +} + +/* + * Abort of a claim that did not get as far as owning the GPU: give the + * device back to the distribution driver, let its probe settle, then + * release the D0 locks. + */ +static void gsp_phys_claim_failed(struct pci_dev *pdev) +{ + struct device_driver *drv; + int ret; + + if (phys.drv_registered) { + pci_unregister_driver(&gsp_phys_driver); + phys.drv_registered = false; + } + phys.claimed = false; + + ret = device_attach(&pdev->dev); + drv = pdev->dev.driver; + if (drv) + pr_info("gsp-phys: claim aborted; %s rebound to %s (rc=%d)\n", + pci_name(pdev), drv->name, ret); + else + pr_warn("gsp-phys: claim aborted; %s has no driver (rc=%d)\n", + pci_name(pdev), ret); + + msleep(GSP_PHYS_POWER_SETTLE_MS); + gsp_phys_power_unlock(); +} + int gsp_phys_gpu_claim(void) { struct pci_dev *pdev; @@ -393,6 +586,16 @@ int gsp_phys_gpu_claim(void) return -ENODEV; } + /* + * Lock the whole board and its root port into D0 before the unbind; + * see the hazard note at the top of this file. + */ + ret = gsp_phys_power_lock(pdev); + if (ret) { + pci_dev_put(pdev); + return ret; + } + if (pdev->dev.driver) { pr_info("gsp-phys: unbinding %s from %s\n", pdev->dev.driver->name, pci_name(pdev)); @@ -401,26 +604,27 @@ int gsp_phys_gpu_claim(void) if (pdev->dev.driver) { pr_err("gsp-phys: %s still bound after unbind (usage count?)\n", pdev->dev.driver->name); - pci_dev_put(pdev); - return -EBUSY; + ret = -EBUSY; + goto err_recover; } ret = pci_register_driver(&gsp_phys_driver); if (ret) { pr_err("gsp-phys: register driver failed: %d\n", ret); - pci_dev_put(pdev); - return ret; + goto err_recover; } phys.drv_registered = true; if (!phys.claimed) { pr_err("gsp-phys: our driver did not bind %s\n", pci_name(pdev)); - pci_unregister_driver(&gsp_phys_driver); - phys.drv_registered = false; - pci_dev_put(pdev); - return -ENODEV; + ret = -ENODEV; + goto err_recover; } + ret = gsp_phys_board_check(pdev, "after claim"); + if (ret) + goto err_board; + pci_dev_put(pdev); pr_info("gsp-phys: physical GPU claimed (backend rung 1)\n"); @@ -436,14 +640,32 @@ int gsp_phys_gpu_claim(void) "backend stays at rung 2\n"); } return 0; + +err_board: + if (phys.drv_registered) { + pci_unregister_driver(&gsp_phys_driver); + phys.drv_registered = false; + } + phys.claimed = false; + pr_err("gsp-phys: claim aborted with the power domain LOCKED and no driver on %s; not rebinding the distribution driver (do not retry in this boot)\n", + pci_name(pdev)); + pci_dev_put(pdev); + return ret; + +err_recover: + gsp_phys_claim_failed(pdev); + pci_dev_put(pdev); + return ret; } EXPORT_SYMBOL_GPL(gsp_phys_gpu_claim); void gsp_phys_gpu_release(void) { struct pci_dev *pdev; + struct device_driver *drv; + int ret; - if (!phys.drv_registered && !phys.claimed) + if (!phys.drv_registered && !phys.claimed && !phys.nr_locks) return; if (phys.pdev) @@ -457,11 +679,24 @@ void gsp_phys_gpu_release(void) pci_unregister_driver(&gsp_phys_driver); phys.drv_registered = false; } + phys.claimed = false; if (pdev) { - int ret = device_attach(&pdev->dev); - struct device_driver *drv = pdev->dev.driver; + /* + * Abort signal before handing the GPU back: if the board no + * longer answers config reads, do not run the distribution + * driver's power-up path. Keep the D0 locks so that a + * driverless board cannot be power-gated again. + */ + if (gsp_phys_board_check(pdev, "before rebind")) { + pr_err("gsp-phys: NOT rebinding %s: board unresponsive; power domain stays LOCKED (do not retry in this boot)\n", + pci_name(pdev)); + pci_dev_put(pdev); + return; + } + ret = device_attach(&pdev->dev); + drv = pdev->dev.driver; if (drv) pr_info("gsp-phys: released %s; rebound to %s (rc=%d)\n", pci_name(pdev), drv->name, ret); @@ -470,6 +705,9 @@ void gsp_phys_gpu_release(void) "(rc=%d), run `modprobe nvidia`\n", pci_name(pdev), ret); pci_dev_put(pdev); + + msleep(GSP_PHYS_POWER_SETTLE_MS); } - phys.claimed = false; + + gsp_phys_power_unlock(); } -- cgit v1.2.3