Skip to content

Commit 45d3a1b

Browse files
committed
tests/compiler: Fix kspgettype segfault and add test
1 parent cef2e7f commit 45d3a1b

4 files changed

Lines changed: 189 additions & 93 deletions

File tree

devito/petsc/config.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ def core_metadata():
5353
}
5454

5555

56+
# Maximum number of bytes (including the null terminator) reserved for a
57+
# KSPType string in the profiler struct.
58+
KSPTYPE_MAX_LEN = 64
59+
60+
5661
def get_petsc_type_mappings():
5762
try:
5863
petsc_precision = petsc_variables['PETSC_PRECISION']
@@ -74,7 +79,11 @@ def get_petsc_type_mappings():
7479
petsc_type_to_ctype = {v: k for k, v in printer_mapper.items()}
7580
# Add other PETSc types
7681
petsc_type_to_ctype.update({
77-
'KSPType': ctypes.c_char_p,
82+
# KSPType is `const char*` into KSP-owned memory. Using c_char_p
83+
# would dereference that pointer when Python reads the struct, which
84+
# segfaults after SNESDestroy frees the KSP. An inline char array
85+
# (c_char * N) stores a copy, so it is safe to read at any time.
86+
'KSPType': ctypes.c_char * KSPTYPE_MAX_LEN,
7887
'KSPConvergedReason': petsc_type_to_ctype['PetscInt'],
7988
'KSPNormType': petsc_type_to_ctype['PetscInt'],
8089
})

devito/petsc/iet/logging.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from devito.tools import frozendict
77

88
from devito.petsc.iet.nodes import petsc_call
9-
from devito.petsc.logging import petsc_return_variable_dict, PetscInfo
9+
from devito.petsc.logging import petsc_return_variable_dict, PetscInfo, KSPTYPE_MAX_LEN
10+
from devito.petsc.types import KSPType
1011

1112

1213
class PetscLogger:
@@ -22,14 +23,13 @@ def __init__(self, level, **kwargs):
2223
self.section_mapper = kwargs.get('section_mapper', {})
2324
self.inject_solve = kwargs.get('inject_solve', None)
2425

25-
# TODO: fix the segfault with kspgettype
2626
if level <= PERF:
2727
funcs = [
2828
# KSP specific
2929
'kspgetiterationnumber',
3030
'kspgettolerances',
3131
'kspgetconvergedreason',
32-
# 'kspgettype',
32+
'kspgettype',
3333
'kspgetnormtype',
3434
# SNES specific
3535
'snesgetiterationnumber',
@@ -94,10 +94,22 @@ def calls(self):
9494
petsc_call(return_variable.name, [input] + by_ref_output)
9595
)
9696
# TODO: Perform a PetscCIntCast here?
97-
exprs = [
98-
DummyExpr(FieldFromPointer(i._C_symbol, struct), i._C_symbol)
99-
for i in output_params
100-
]
101-
calls.extend(exprs)
97+
for i in output_params:
98+
if isinstance(i, KSPType):
99+
# KSPType is `const char*` into KSP-owned memory. After
100+
# SNESDestroy that pointer is invalid, so we copy the string
101+
# into the inline char buffer in the profiler struct instead
102+
# of storing the raw pointer.
103+
calls.append(
104+
petsc_call('PetscStrncpy', [
105+
FieldFromPointer(i._C_symbol, struct),
106+
i._C_symbol,
107+
KSPTYPE_MAX_LEN
108+
])
109+
)
110+
else:
111+
calls.append(
112+
DummyExpr(FieldFromPointer(i._C_symbol, struct), i._C_symbol)
113+
)
102114

103115
return tuple(calls)

devito/petsc/logging.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import os
22
from collections import namedtuple
33
from dataclasses import dataclass
4+
from functools import cached_property
5+
6+
from cgen import Struct, Value
47

58
from devito.types import CompositeObject
69

710
from devito.petsc.types import (
811
PetscInt, PetscScalar, KSPType, KSPConvergedReason, KSPNormType
912
)
10-
from devito.petsc.config import petsc_type_to_ctype
13+
from devito.petsc.config import petsc_type_to_ctype, KSPTYPE_MAX_LEN
1114

1215

1316
class PetscEntry:
@@ -161,6 +164,22 @@ def __init__(self, name, pname, petsc_option_mapper, sobjs, section_mapper,
161164
def fields(self):
162165
return self._fields
163166

167+
@cached_property
168+
def _C_typedecl(self):
169+
"""
170+
Override generated C struct declaration so that KSPType fields
171+
are emitted as ``char name[KSPTYPE_MAX_LEN]`` rather than ``KSPType name``
172+
(i.e. ``const char *``). This avoids a segfault when Python reads the
173+
profiler struct after SNESDestroy has freed the KSP.
174+
"""
175+
entries = []
176+
for field in self._fields:
177+
if str(field.dtype) == 'KSPType':
178+
entries.append(Value('char', '%s[%d]' % (field.name, KSPTYPE_MAX_LEN)))
179+
else:
180+
entries.append(Value(str(field.dtype), field.name))
181+
return Struct(self.dtype._type_.__name__, entries)
182+
164183
@property
165184
def prefix(self):
166185
# If users provide an options prefix, use it in the summary;

tests/test_petsc.py

Lines changed: 139 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ def command_line():
3030
# Random prefixes to validate command line argument parsing
3131
prefix = (
3232
'd17weqroeg', 'riabfodkj5', 'fir8o3lsak',
33-
'zwejklqn25', 'qtr2vfvwiu')
34-
33+
'zwejklqn25', 'qtr2vfvwiu'
34+
)
3535
petsc_option = (
3636
('ksp_rtol',),
3737
('ksp_rtol', 'ksp_atol'),
@@ -52,6 +52,9 @@ def command_line():
5252
for o, v in zip(opt, val, strict=True):
5353
argv.extend([f'-{p}_{o}', str(v)])
5454
expected[p] = zip(opt, val)
55+
56+
if os.environ.get('DEVITO_PYTEST_FLAG', '0') == '2':
57+
argv.extend(['-malloc_dump'])
5558
return argv, expected
5659

5760

@@ -1696,7 +1699,8 @@ def setup_class(self):
16961699
self.eq2 = Eq(self.g.laplace, self.h)
16971700

16981701
@skipif('petsc')
1699-
def test_different_solver_params(self):
1702+
@pytest.mark.parallel(mode=[1, 2, 4, 6, 8])
1703+
def test_different_solver_params(self, mode):
17001704
# Explicitly set the solver parameters
17011705
solver1 = petscsolve(
17021706
self.eq1, target=self.e, solver_parameters={'ksp_rtol': '1e-10'}
@@ -1717,7 +1721,8 @@ def test_different_solver_params(self):
17171721
in str(op._func_table['SetPetscOptions1'].root)
17181722

17191723
@skipif('petsc')
1720-
def test_options_prefix(self):
1724+
@pytest.mark.parallel(mode=[1, 2, 4, 6, 8])
1725+
def test_options_prefix(self, mode):
17211726
solver1 = petscsolve(self.eq1, self.e,
17221727
solver_parameters={'ksp_rtol': '1e-10'},
17231728
options_prefix='poisson1')
@@ -1740,7 +1745,8 @@ def test_options_prefix(self):
17401745
in str(op._func_table['SetPetscOptions1'].root)
17411746

17421747
@skipif('petsc')
1743-
def test_options_no_value(self):
1748+
@pytest.mark.parallel(mode=[1, 2, 4, 6, 8])
1749+
def test_options_no_value(self, mode):
17441750
"""
17451751
Test solver parameters that do not require a value, such as
17461752
`snes_view` and `ksp_view`.
@@ -1929,84 +1935,84 @@ def test_command_line_priority_tols3(self, command_line, log_level):
19291935
for opt, val in expected[prefix]:
19301936
assert entry.KSPGetTolerances[opt.removeprefix('ksp_')] == val
19311937

1932-
# @skipif('petsc')
1933-
# @pytest.mark.parametrize('log_level', ['PERF', 'DEBUG'])
1934-
# def test_command_line_priority_ksp_type(self, command_line, log_level):
1935-
# """
1936-
# Test the solver parameter 'ksp_type' specified via the command line
1937-
# take precedence over the one specified in the `solver_parameters` dict.
1938-
# """
1939-
# prefix = 'zwejklqn25'
1940-
# _, expected = command_line
1941-
1942-
# # Set `ksp_type`` in the solver parameters, which should be overridden
1943-
# # by the command line value (which is set to `cg` -
1944-
# # see the `command_line` fixture).
1945-
# params = {'ksp_type': 'richardson'}
1946-
1947-
# solver1 = petscsolve(
1948-
# self.eq1, target=self.e,
1949-
# solver_parameters=params,
1950-
# options_prefix=prefix
1951-
# )
1952-
# with switchconfig(language='petsc', log_level=log_level):
1953-
# op = Operator(solver1)
1954-
# summary = op.apply()
1955-
1956-
# petsc_summary = summary.petsc
1957-
# entry = petsc_summary.get_entry('section0', prefix)
1958-
# for _, val in expected[prefix]:
1959-
# assert entry.KSPGetType == val
1960-
# assert not entry.KSPGetType == params['ksp_type']
1961-
1962-
# @skipif('petsc')
1963-
# def test_command_line_priority_ccode(self, command_line):
1964-
# """
1965-
# Verify that if an option is set via the command line,
1966-
# the corresponding entry in `linear_solve_defaults` or `solver_parameters`
1967-
# is not set or cleared in the generated code. (The command line option
1968-
# will have already been set in the global PetscOptions database
1969-
# during PetscInitialize().)
1970-
# """
1971-
# prefix = 'qtr2vfvwiu'
1972-
1973-
# solver = petscsolve(
1974-
# self.eq1, target=self.e,
1975-
# # Specify a solver parameter that is not set via the
1976-
# # command line (see the `command_line` fixture for this prefix).
1977-
# solver_parameters={'ksp_rtol': '1e-10'},
1978-
# options_prefix=prefix
1979-
# )
1980-
# with switchconfig(language='petsc'):
1981-
# op = Operator(solver)
1982-
1983-
# set_options_callback = str(op._func_table['SetPetscOptions0'].root)
1984-
# clear_options_callback = str(op._func_table['ClearPetscOptions0'].root)
1985-
1986-
# # Check that the `ksp_rtol` option IS set and cleared explicitly
1987-
# # since it is NOT set via the command line.
1988-
# assert f'PetscOptionsSetValue(NULL,"-{prefix}_ksp_rtol","1e-10")' \
1989-
# in set_options_callback
1990-
# assert f'PetscOptionsClearValue(NULL,"-{prefix}_ksp_rtol")' \
1991-
# in clear_options_callback
1992-
1993-
# # Check that the `ksp_divtol` and `ksp_type` options are NOT set
1994-
# # or cleared explicitly since they ARE set via the command line.
1995-
# assert f'PetscOptionsSetValue(NULL,"-{prefix}_div_tol",' \
1996-
# not in set_options_callback
1997-
# assert f'PetscOptionsSetValue(NULL,"-{prefix}_ksp_type",' \
1998-
# not in set_options_callback
1999-
# assert f'PetscOptionsClearValue(NULL,"-{prefix}_div_tol"));' \
2000-
# not in clear_options_callback
2001-
# assert f'PetscOptionsClearValue(NULL,"-{prefix}_ksp_type"));' \
2002-
# not in clear_options_callback
2003-
2004-
# # Check that options specifed by the `linear_solver_defaults`
2005-
# # are still set and cleared
2006-
# assert f'PetscOptionsSetValue(NULL,"-{prefix}_ksp_atol",' \
2007-
# in set_options_callback
2008-
# assert f'PetscOptionsClearValue(NULL,"-{prefix}_ksp_atol"));' \
2009-
# in clear_options_callback
1938+
@skipif('petsc')
1939+
@pytest.mark.parametrize('log_level', ['PERF', 'DEBUG'])
1940+
def test_command_line_priority_ksp_type(self, command_line, log_level):
1941+
"""
1942+
Test the solver parameter 'ksp_type' specified via the command line
1943+
take precedence over the one specified in the `solver_parameters` dict.
1944+
"""
1945+
prefix = 'zwejklqn25'
1946+
_, expected = command_line
1947+
1948+
# Set `ksp_type`` in the solver parameters, which should be overridden
1949+
# by the command line value (which is set to `cg` -
1950+
# see the `command_line` fixture).
1951+
params = {'ksp_type': 'richardson'}
1952+
1953+
solver1 = petscsolve(
1954+
self.eq1, target=self.e,
1955+
solver_parameters=params,
1956+
options_prefix=prefix
1957+
)
1958+
with switchconfig(language='petsc', log_level=log_level):
1959+
op = Operator(solver1)
1960+
summary = op.apply()
1961+
1962+
petsc_summary = summary.petsc
1963+
entry = petsc_summary.get_entry('section0', prefix)
1964+
for _, val in expected[prefix]:
1965+
assert entry.KSPGetType == val
1966+
assert not entry.KSPGetType == params['ksp_type']
1967+
1968+
@skipif('petsc')
1969+
def test_command_line_priority_ccode(self, command_line):
1970+
"""
1971+
Verify that if an option is set via the command line,
1972+
the corresponding entry in `linear_solve_defaults` or `solver_parameters`
1973+
is not set or cleared in the generated code. (The command line option
1974+
will have already been set in the global PetscOptions database
1975+
during PetscInitialize().)
1976+
"""
1977+
prefix = 'qtr2vfvwiu'
1978+
1979+
solver = petscsolve(
1980+
self.eq1, target=self.e,
1981+
# Specify a solver parameter that is not set via the
1982+
# command line (see the `command_line` fixture for this prefix).
1983+
solver_parameters={'ksp_rtol': '1e-10'},
1984+
options_prefix=prefix
1985+
)
1986+
with switchconfig(language='petsc'):
1987+
op = Operator(solver)
1988+
1989+
set_options_callback = str(op._func_table['SetPetscOptions0'].root)
1990+
clear_options_callback = str(op._func_table['ClearPetscOptions0'].root)
1991+
1992+
# Check that the `ksp_rtol` option IS set and cleared explicitly
1993+
# since it is NOT set via the command line.
1994+
assert f'PetscOptionsSetValue(NULL,"-{prefix}_ksp_rtol","1e-10")' \
1995+
in set_options_callback
1996+
assert f'PetscOptionsClearValue(NULL,"-{prefix}_ksp_rtol")' \
1997+
in clear_options_callback
1998+
1999+
# Check that the `ksp_divtol` and `ksp_type` options are NOT set
2000+
# or cleared explicitly since they ARE set via the command line.
2001+
assert f'PetscOptionsSetValue(NULL,"-{prefix}_div_tol",' \
2002+
not in set_options_callback
2003+
assert f'PetscOptionsSetValue(NULL,"-{prefix}_ksp_type",' \
2004+
not in set_options_callback
2005+
assert f'PetscOptionsClearValue(NULL,"-{prefix}_div_tol"));' \
2006+
not in clear_options_callback
2007+
assert f'PetscOptionsClearValue(NULL,"-{prefix}_ksp_type"));' \
2008+
not in clear_options_callback
2009+
2010+
# Check that options specifed by the `linear_solver_defaults`
2011+
# are still set and cleared
2012+
assert f'PetscOptionsSetValue(NULL,"-{prefix}_ksp_atol",' \
2013+
in set_options_callback
2014+
assert f'PetscOptionsClearValue(NULL,"-{prefix}_ksp_atol"));' \
2015+
in clear_options_callback
20102016

20112017

20122018
class TestHashing:
@@ -2197,6 +2203,34 @@ def test_get_ksp_type(self):
21972203
assert entry2['KSPGetType'] == 'cg'
21982204
assert entry2['kspgettype'] == 'cg'
21992205

2206+
@skipif('petsc')
2207+
def test_get_ksp_type_large_grid(self):
2208+
"""
2209+
Test for a dangling-pointer segfault when reading KSPGetType
2210+
after op.apply(). KSPType is ``const char*`` into KSP-owned memory;
2211+
after SNESDestroy that pointer is invalid. The crash only appeared
2212+
reliably on large grids (n=257) because the freed KSP memory must be
2213+
reclaimed by the heap before Python reads the profiler struct.
2214+
"""
2215+
get_info = ['kspgettype']
2216+
grid = Grid(shape=(257, 257), dtype=np.float64)
2217+
e = Function(name='e', grid=grid, space_order=2)
2218+
f = Function(name='f', grid=grid, space_order=2)
2219+
eq = Eq(e.laplace, f)
2220+
2221+
solver = petscsolve(
2222+
eq, target=e,
2223+
solver_parameters={'ksp_type': 'cg'},
2224+
options_prefix='test_ksp_type',
2225+
get_info=get_info
2226+
)
2227+
with switchconfig(language='petsc'):
2228+
op = Operator(solver)
2229+
summary = op.apply()
2230+
2231+
entry = summary.petsc.get_entry('section0', 'test_ksp_type')
2232+
assert entry.KSPGetType == 'cg'
2233+
22002234

22012235
class TestPrinter:
22022236

@@ -2781,3 +2815,25 @@ def define(self, dimensions):
27812815
f"rank {rank}: expected {expected[rank]}, got {actual}"
27822816

27832817
# TODO: add 2d and 3d tests
2818+
2819+
2820+
# @skipif('petsc')
2821+
# def test_apply_memory():
2822+
2823+
# nx = 81
2824+
# ny = 81
2825+
2826+
# grid = Grid(shape=(nx, ny), extent=(2., 2.), dtype=np.float64)
2827+
2828+
# u = Function(name='u', grid=grid, dtype=np.float64, space_order=2)
2829+
# v = Function(name='v', grid=grid, dtype=np.float64, space_order=2)
2830+
2831+
# v.data[:] = 5.0
2832+
2833+
# eq = Eq(v, u.laplace, subdomain=grid.interior)
2834+
2835+
# petsc = petscsolve([eq], u)
2836+
2837+
# with switchconfig(language='petsc'):
2838+
# op = Operator(petsc)
2839+
# op.apply()

0 commit comments

Comments
 (0)