Fix the CPU-only ROCm routing errors and two font-scale UI flakes (#7469)
* Fix the CPU-only ROCm routing errors and two font-scale UI flakes Two unrelated causes of red CI on every PR, both reproduced before fixing. ROCm routing: 12 errors on Repo tests (CPU). The spoof reports an AMD GPU, and unsloth_zoo pulls in bitsandbytes, which picks a compute backend at import. Once torch looks like a GPU is present, bnb loads its ROCm/CUDA ops, which a CPU-only torch cannot satisfy (no libhipblas.so.2, no torch._C._cuda_getCurrentRawStream), so the child died before printing RESULT. Nothing here tests bitsandbytes, so import it first, under the honest hardware. Reproduced in a CPU-only torch venv: 11 passed with 12 errors before, 23 passed after. Still 23 passed on a CUDA build. Font-scale UI: the select-viewport step pressed ArrowDown six times behind fixed sleeps, but Radix moves focus into the listbox after the content opens, so on a loaded runner the keys landed on the trigger and nothing scrolled. Wait on the overflow and press until it moves, bounded at 40. The same fixed-sleep pattern made open_appearance miss the dialog when the shortcut fired before the app wired its handler; alternate both chords on a bounded retry and wait for the control the caller is about to drive. Both were reproduced locally by running the suite against a real Studio under full CPU load. Original: 2 of 10 passed, with the exact CI signature 'keyboard did not scroll the select viewport: 0' five times. Fixed: 10 of 10. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep the ROCm routing assertion live on Apple Silicon for PR #7469 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: danielhanchen <unslothai@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
c3d3680e7c
commit
170b412c1d
2 changed files with 79 additions and 38 deletions
|
|
@ -12,6 +12,7 @@ at import) resolves from a clean process.
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
|
@ -43,6 +44,15 @@ _ARCHES = {
|
|||
_CHILD = """
|
||||
import json, sys
|
||||
sys.path.insert(0, {tests!r})
|
||||
# Import bitsandbytes under the real torch first. unsloth_zoo pulls it in, and it
|
||||
# picks a compute backend at import: once the spoof reports an AMD GPU, it loads
|
||||
# its ROCm/CUDA ops, which a CPU-only torch cannot satisfy (no libhipblas, no
|
||||
# torch._C._cuda_getCurrentRawStream) and the child dies before printing RESULT.
|
||||
# Nothing here tests bitsandbytes, so let it see the honest hardware.
|
||||
try:
|
||||
import bitsandbytes # noqa: F401
|
||||
except Exception:
|
||||
pass
|
||||
import _zoo_rocm_spoof as spoof
|
||||
arches = {arches!r}
|
||||
spoof.apply(arches[0])
|
||||
|
|
@ -60,7 +70,11 @@ print("RESULT " + json.dumps({{"device_type": device_type, "targets": targets}})
|
|||
@pytest.fixture(scope = "module")
|
||||
def routed():
|
||||
code = _CHILD.format(tests = str(_TESTS_DIR), arches = list(_ARCHES))
|
||||
proc = subprocess.run([sys.executable, "-c", code], capture_output = True, text = True)
|
||||
# get_device_type() returns "mlx" before it ever looks at torch on Darwin arm64
|
||||
# with mlx installed, so the spoof would be ignored. Force the GPU path to keep
|
||||
# the assertion live there instead of skipping it.
|
||||
env = {**os.environ, "UNSLOTH_FORCE_GPU_PATH": "1"}
|
||||
proc = subprocess.run([sys.executable, "-c", code], capture_output = True, text = True, env = env)
|
||||
line = next((l for l in proc.stdout.splitlines() if l.startswith("RESULT ")), None)
|
||||
assert line, f"child produced no result.\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
|
||||
return json.loads(line[len("RESULT ") :])
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import os
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.sync_api import TimeoutError as PWTimeout
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
|
@ -46,6 +47,18 @@ def near(
|
|||
return a is not None and b is not None and abs(a - b) <= tol
|
||||
|
||||
|
||||
_VP = 'document.querySelector("[data-radix-select-viewport]")'
|
||||
SCROLL_TOP_JS = f"() => {_VP}.scrollTop"
|
||||
SCROLLABLE_JS = f"() => {{ const vp = {_VP}; return !!vp && vp.scrollHeight > vp.clientHeight; }}"
|
||||
VIEWPORT_STATE_JS = f"""
|
||||
() => {{
|
||||
const vp = {_VP};
|
||||
return vp
|
||||
? {{ scrollHeight: vp.scrollHeight, clientHeight: vp.clientHeight, top: vp.scrollTop }}
|
||||
: null;
|
||||
}}
|
||||
"""
|
||||
|
||||
MEASURE_JS = """
|
||||
() => {
|
||||
const fs = (el) => (el ? parseFloat(getComputedStyle(el).fontSize) : null);
|
||||
|
|
@ -83,15 +96,22 @@ def set_input(page, label, value):
|
|||
|
||||
|
||||
def open_appearance(page):
|
||||
page.keyboard.press("Control+,")
|
||||
page.wait_for_timeout(700)
|
||||
if page.get_by_role("dialog").count() == 0:
|
||||
page.keyboard.press("Meta+,")
|
||||
page.wait_for_timeout(700)
|
||||
if page.get_by_role("dialog").count() == 0:
|
||||
fail("settings dialog did not open")
|
||||
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click()
|
||||
page.wait_for_timeout(600)
|
||||
# The shortcut can fire before the app has wired its key handler, so press
|
||||
# each chord once behind a fixed sleep and a slow boot loses the dialog.
|
||||
# Alternate them on a bounded retry, waiting on the dialog itself.
|
||||
dialog = page.get_by_role("dialog")
|
||||
for attempt in range(10):
|
||||
page.keyboard.press("Meta+," if attempt % 2 else "Control+,")
|
||||
try:
|
||||
dialog.first.wait_for(state = "visible", timeout = 2_000)
|
||||
break
|
||||
except PWTimeout:
|
||||
continue
|
||||
if dialog.count() == 0:
|
||||
fail("settings dialog did not open after 10 attempts")
|
||||
dialog.get_by_role("button").filter(has_text = "Appearance").first.click()
|
||||
# Wait for the control the caller is about to drive, not a fixed interval.
|
||||
page.locator("input[aria-label='UI font size']").wait_for(state = "visible", timeout = 15_000)
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -155,39 +175,46 @@ def main():
|
|||
page.wait_for_timeout(400)
|
||||
|
||||
step("overflowing select scrolls its Radix viewport")
|
||||
page.get_by_role("dialog").get_by_role("button").filter(has_text = "Voice").first.click()
|
||||
page.wait_for_timeout(600)
|
||||
voice = page.get_by_role("dialog").get_by_role("button").filter(has_text = "Voice").first
|
||||
voice.click()
|
||||
page.set_viewport_size({"width": 1440, "height": 480})
|
||||
page.locator("[aria-label='Dictation language']").click()
|
||||
page.wait_for_timeout(700)
|
||||
state = page.evaluate(
|
||||
"""
|
||||
() => {
|
||||
const vp = document.querySelector("[data-radix-select-viewport]");
|
||||
return vp
|
||||
? { scrollable: vp.scrollHeight > vp.clientHeight, top: vp.scrollTop }
|
||||
: null;
|
||||
}
|
||||
"""
|
||||
)
|
||||
if not state or not state["scrollable"]:
|
||||
fail(f"select viewport not scrollable: {state}")
|
||||
for _ in range(6):
|
||||
trigger = page.locator("[aria-label='Dictation language']")
|
||||
trigger.wait_for(state = "visible")
|
||||
trigger.click()
|
||||
|
||||
viewport = page.locator("[data-radix-select-viewport]")
|
||||
viewport.wait_for(state = "visible")
|
||||
# Wait for the overflow itself rather than a fixed sleep: the list is
|
||||
# populated asynchronously, so measuring too early reads it as short.
|
||||
try:
|
||||
page.wait_for_function(SCROLLABLE_JS, timeout = 10_000)
|
||||
except PWTimeout:
|
||||
fail(f"select viewport not scrollable: {page.evaluate(VIEWPORT_STATE_JS)}")
|
||||
|
||||
# Radix moves focus into the listbox after the content opens, so a fixed
|
||||
# burst of presses can land on the trigger and scroll nothing. Press until
|
||||
# it moves instead; a real regression still fails, just after more tries.
|
||||
kb_top = 0
|
||||
for _ in range(40):
|
||||
page.keyboard.press("ArrowDown")
|
||||
page.wait_for_timeout(100)
|
||||
kb_top = page.evaluate(
|
||||
"() => document.querySelector('[data-radix-select-viewport]').scrollTop"
|
||||
)
|
||||
kb_top = page.evaluate(SCROLL_TOP_JS)
|
||||
if kb_top > 0:
|
||||
break
|
||||
page.wait_for_timeout(50)
|
||||
if not kb_top > 0:
|
||||
fail(f"keyboard did not scroll the select viewport: {kb_top}")
|
||||
vp_box = page.locator("[data-radix-select-viewport]").bounding_box()
|
||||
fail(f"keyboard did not scroll the select viewport after 40 presses: {kb_top}")
|
||||
|
||||
vp_box = viewport.bounding_box()
|
||||
page.mouse.move(vp_box["x"] + vp_box["width"] / 2, vp_box["y"] + 40)
|
||||
page.mouse.wheel(0, -400)
|
||||
page.wait_for_timeout(300)
|
||||
wheel_top = page.evaluate(
|
||||
"() => document.querySelector('[data-radix-select-viewport]').scrollTop"
|
||||
)
|
||||
if not wheel_top < kb_top:
|
||||
try:
|
||||
page.wait_for_function(
|
||||
"top => document.querySelector('[data-radix-select-viewport]').scrollTop < top",
|
||||
arg = kb_top,
|
||||
timeout = 10_000,
|
||||
)
|
||||
except PWTimeout:
|
||||
wheel_top = page.evaluate(SCROLL_TOP_JS)
|
||||
fail(f"wheel did not scroll the select viewport: {kb_top} -> {wheel_top}")
|
||||
page.keyboard.press("Escape")
|
||||
page.set_viewport_size({"width": 1440, "height": 900})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue