Skip to content

Commit b67c1ce

Browse files
yoneymeta-codesync[bot]
authored andcommitted
Add free-threaded JIT tuple subscript tests
Summary: Cover generic and LoadArrayItem tuple paths, plus concurrent guard-failure deopts. Reviewed By: alexmalyshev Differential Revision: D115201601 fbshipit-source-id: 2ef8b364ac8433b525904c6254e8a3a9917d5029
1 parent d51466a commit b67c1ce

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
# pyre-strict
4+
5+
"""Free-threaded JIT regression tests for tuple subscripts."""
6+
7+
import dis
8+
import os
9+
import threading
10+
import unittest
11+
from collections.abc import Callable, Sequence
12+
from concurrent.futures import ThreadPoolExecutor
13+
14+
import cinderx.jit
15+
from cinderx.test_support import run_in_subprocess
16+
17+
18+
class JITTupleTest(unittest.TestCase):
19+
def warm_up_tuple_opcode(
20+
self,
21+
read_item: Callable[[Sequence[object]], object],
22+
) -> None:
23+
values = (object(),)
24+
# Specialize in CPython first so Simplify can use the resulting type
25+
# guards to emit LoadArrayItem.
26+
cinderx.jit.jit_suppress(read_item)
27+
for _ in range(100):
28+
read_item(values)
29+
cinderx.jit.jit_unsuppress(read_item)
30+
opnames = {
31+
instruction.opname
32+
for instruction in dis.get_instructions(read_item, adaptive=True)
33+
}
34+
self.assertIn("BINARY_OP_SUBSCR_TUPLE_INT", opnames)
35+
36+
def exercise_concurrent_reads(
37+
self,
38+
read_item: Callable[[Sequence[object]], object],
39+
values: Sequence[object],
40+
) -> int:
41+
worker_count = 10
42+
iterations = 10_000
43+
start = threading.Barrier(worker_count)
44+
expected = values[0]
45+
46+
@cinderx.jit.jit_suppress
47+
def reader(_: int) -> bool:
48+
start.wait()
49+
for _ in range(iterations):
50+
if read_item(values) is not expected:
51+
return False
52+
return True
53+
54+
with ThreadPoolExecutor(max_workers=worker_count) as executor:
55+
results = list(executor.map(reader, range(worker_count)))
56+
57+
self.assertEqual(results, [True] * worker_count)
58+
return worker_count * iterations
59+
60+
@unittest.skipUnless(hasattr(os, "fork"), "fork not available on Windows")
61+
@run_in_subprocess
62+
def test_concurrent_subscript_without_specialized_opcodes(self) -> None:
63+
"""Keep generic HIR when compiling an adaptive tuple opcode."""
64+
cinderx.jit.disable_specialized_opcodes()
65+
66+
def read_item(values: Sequence[object]) -> object:
67+
return values[0]
68+
69+
self.warm_up_tuple_opcode(read_item)
70+
71+
self.assertTrue(cinderx.jit.force_compile(read_item))
72+
opcode_counts = cinderx.jit.get_function_hir_opcode_counts(read_item)
73+
if opcode_counts is None:
74+
self.fail("No HIR opcode counts for compiled read_item")
75+
self.assertIn("BinaryOp", opcode_counts)
76+
self.assertNotIn("LoadArrayItem", opcode_counts)
77+
78+
values = (object(),)
79+
self.exercise_concurrent_reads(read_item, values)
80+
81+
@unittest.skipUnless(hasattr(os, "fork"), "fork not available on Windows")
82+
@run_in_subprocess
83+
def test_concurrent_subscript_with_simplify(self) -> None:
84+
"""Keep the direct LoadArrayItem path for exact tuples.
85+
86+
Exact tuples are immutable, so Simplify can retain the borrowed
87+
LoadArrayItem path that mutable lists cannot use in free-threaded
88+
builds. The HIR assertions pin that optimization decision.
89+
"""
90+
cinderx.jit.enable_specialized_opcodes()
91+
92+
def read_item(values: Sequence[object]) -> object:
93+
return values[0]
94+
95+
self.warm_up_tuple_opcode(read_item)
96+
97+
self.assertTrue(cinderx.jit.force_compile(read_item))
98+
opcode_counts = cinderx.jit.get_function_hir_opcode_counts(read_item)
99+
if opcode_counts is None:
100+
self.fail("No HIR opcode counts for compiled read_item")
101+
self.assertIn("LoadArrayItem", opcode_counts)
102+
self.assertNotIn("BinaryOp", opcode_counts)
103+
104+
values = (object(),)
105+
self.exercise_concurrent_reads(read_item, values)
106+
107+
@unittest.skipUnless(hasattr(os, "fork"), "fork not available on Windows")
108+
@run_in_subprocess
109+
def test_concurrent_deopt_on_guard_failure(self) -> None:
110+
"""Concurrent list calls deopt at the exact-tuple guard."""
111+
cinderx.jit.enable_specialized_opcodes()
112+
113+
def read_item(values: Sequence[object]) -> object:
114+
return values[0]
115+
116+
self.warm_up_tuple_opcode(read_item)
117+
self.assertTrue(cinderx.jit.force_compile(read_item))
118+
119+
cinderx.jit.get_and_clear_runtime_stats()
120+
call_count = self.exercise_concurrent_reads(read_item, [object()])
121+
deopts = cinderx.jit.get_and_clear_runtime_stats()["deopt"]
122+
if not isinstance(deopts, list):
123+
self.fail("Deopt runtime stats are not a list")
124+
read_item_deopts = [
125+
deopt
126+
for deopt in deopts
127+
if deopt["normal"]["func_qualname"] == read_item.__qualname__
128+
]
129+
self.assertTrue(read_item_deopts)
130+
for deopt in read_item_deopts:
131+
self.assertEqual(deopt["normal"]["reason"], "GuardFailure")
132+
self.assertEqual(deopt["normal"]["description"], "GuardType")
133+
self.assertEqual(
134+
sum(deopt["int"]["count"] for deopt in read_item_deopts), call_count
135+
)

0 commit comments

Comments
 (0)