concretizer: refactor argument passing for reuse

Reuse previously was a very invasive change that required parameters to be added to all
the methods that called `concretize()` on a `Spec` object. With the addition of
concretizer configuration, we can use the config system to simplify this argument
passing and keep the code cleaner.

We decided that concretizer config options should be read at `Solver` instantiation
time, and if config changes between instnatiation of a particular solver and
`solve()` invocation, the `Solver` should use the settings from `__init__()`.

- [x] remove `reuse` keyword argument from most concretize functions
- [x] refactor usages to use `spack.config.override("concretizer:reuse", True)`
- [x] rework argument passing in `Solver` so that parameters are set from config
      at instantiation time
This commit is contained in:
Todd Gamblin 2022-02-15 08:43:36 -08:00 committed by Greg Becker
parent d33973df6c
commit b1ff9c05bc
6 changed files with 106 additions and 107 deletions

View File

@ -88,11 +88,11 @@ def solve(parser, args):
'hashes': args.long or args.very_long 'hashes': args.long or args.very_long
} }
# process dump options # process output options
dump = re.split(r'\s*,\s*', args.show) show = re.split(r'\s*,\s*', args.show)
if 'all' in dump: if 'all' in show:
dump = show_options show = show_options
for d in dump: for d in show:
if d not in show_options: if d not in show_options:
raise ValueError( raise ValueError(
"Invalid option for '--show': '%s'\nchoose from: (%s)" "Invalid option for '--show': '%s'\nchoose from: (%s)"
@ -105,23 +105,28 @@ def solve(parser, args):
specs = spack.cmd.parse_specs(args.specs) specs = spack.cmd.parse_specs(args.specs)
# set up solver parameters # set up solver parameters
# Note: reuse and other concretizer prefs are passed as configuration
solver = asp.Solver() solver = asp.Solver()
solver.dump = dump output = sys.stdout if "asp" in show else None
solver.models = models result = solver.solve(
solver.timers = args.timers specs,
solver.stats = args.stats out=output,
models=models,
result = solver.solve(specs) timers=args.timers,
if 'solutions' not in dump: stats=args.stats,
setup_only=(set(show) == {'asp'})
)
if 'solutions' not in show:
return return
# die if no solution was found # die if no solution was found
result.raise_if_unsat() result.raise_if_unsat()
# dump the solutions as concretized specs # show the solutions as concretized specs
if 'solutions' in dump: if 'solutions' in show:
opt, _, _ = min(result.answers) opt, _, _ = min(result.answers)
if ("opt" in dump) and (not args.format):
if ("opt" in show) and (not args.format):
tty.msg("Best of %d considered solutions." % result.nmodels) tty.msg("Best of %d considered solutions." % result.nmodels)
tty.msg("Optimization Criteria:") tty.msg("Optimization Criteria:")

View File

@ -751,7 +751,6 @@ def _concretize_specs_together_new(*abstract_specs, **kwargs):
solver = spack.solver.asp.Solver() solver = spack.solver.asp.Solver()
solver.tests = kwargs.get('tests', False) solver.tests = kwargs.get('tests', False)
solver.reuse = kwargs.get('reuse', False)
result = solver.solve(abstract_specs) result = solver.solve(abstract_specs)
result.raise_if_unsat() result.raise_if_unsat()
@ -789,15 +788,10 @@ def make_concretization_repository(abstract_specs):
abstract_specs = [spack.spec.Spec(s) for s in abstract_specs] abstract_specs = [spack.spec.Spec(s) for s in abstract_specs]
concretization_repository = make_concretization_repository(abstract_specs) concretization_repository = make_concretization_repository(abstract_specs)
concretization_kwargs = {
'tests': kwargs.get('tests', False),
'reuse': kwargs.get('reuse', False)
}
with spack.repo.additional_repository(concretization_repository): with spack.repo.additional_repository(concretization_repository):
# Spec from a helper package that depends on all the abstract_specs # Spec from a helper package that depends on all the abstract_specs
concretization_root = spack.spec.Spec('concretizationroot') concretization_root = spack.spec.Spec('concretizationroot')
concretization_root.concretize(**concretization_kwargs) concretization_root.concretize(tests=kwargs.get("tests", False))
# Retrieve the direct dependencies # Retrieve the direct dependencies
concrete_specs = [ concrete_specs = [
concretization_root[spec.name].copy() for spec in abstract_specs concretization_root[spec.name].copy() for spec in abstract_specs

View File

@ -1083,7 +1083,7 @@ def is_develop(self, spec):
"""Returns true when the spec is built from local sources""" """Returns true when the spec is built from local sources"""
return spec.name in self.dev_specs return spec.name in self.dev_specs
def concretize(self, force=False, tests=False, reuse=None): def concretize(self, force=False, tests=False):
"""Concretize user_specs in this environment. """Concretize user_specs in this environment.
Only concretizes specs that haven't been concretized yet unless Only concretizes specs that haven't been concretized yet unless
@ -1097,8 +1097,6 @@ def concretize(self, force=False, tests=False, reuse=None):
already concretized already concretized
tests (bool or list or set): False to run no tests, True to test tests (bool or list or set): False to run no tests, True to test
all packages, or a list of package names to run tests for some all packages, or a list of package names to run tests for some
reuse (bool): if True try to maximize reuse of already installed
specs, if False don't account for installation status.
Returns: Returns:
List of specs that have been concretized. Each entry is a tuple of List of specs that have been concretized. Each entry is a tuple of
@ -1112,15 +1110,15 @@ def concretize(self, force=False, tests=False, reuse=None):
# Pick the right concretization strategy # Pick the right concretization strategy
if self.concretization == 'together': if self.concretization == 'together':
return self._concretize_together(tests=tests, reuse=reuse) return self._concretize_together(tests=tests)
if self.concretization == 'separately': if self.concretization == 'separately':
return self._concretize_separately(tests=tests, reuse=reuse) return self._concretize_separately(tests=tests)
msg = 'concretization strategy not implemented [{0}]' msg = 'concretization strategy not implemented [{0}]'
raise SpackEnvironmentError(msg.format(self.concretization)) raise SpackEnvironmentError(msg.format(self.concretization))
def _concretize_together(self, tests=False, reuse=None): def _concretize_together(self, tests=False):
"""Concretization strategy that concretizes all the specs """Concretization strategy that concretizes all the specs
in the same DAG. in the same DAG.
""" """
@ -1153,14 +1151,14 @@ def _concretize_together(self, tests=False, reuse=None):
self.specs_by_hash = {} self.specs_by_hash = {}
concrete_specs = spack.concretize.concretize_specs_together( concrete_specs = spack.concretize.concretize_specs_together(
*self.user_specs, tests=tests, reuse=reuse *self.user_specs, tests=tests
) )
concretized_specs = [x for x in zip(self.user_specs, concrete_specs)] concretized_specs = [x for x in zip(self.user_specs, concrete_specs)]
for abstract, concrete in concretized_specs: for abstract, concrete in concretized_specs:
self._add_concrete_spec(abstract, concrete) self._add_concrete_spec(abstract, concrete)
return concretized_specs return concretized_specs
def _concretize_separately(self, tests=False, reuse=None): def _concretize_separately(self, tests=False):
"""Concretization strategy that concretizes separately one """Concretization strategy that concretizes separately one
user spec after the other. user spec after the other.
""" """
@ -1185,7 +1183,7 @@ def _concretize_separately(self, tests=False, reuse=None):
): ):
if uspec not in old_concretized_user_specs: if uspec not in old_concretized_user_specs:
root_specs.append(uspec) root_specs.append(uspec)
arguments.append((uspec_constraints, tests, reuse)) arguments.append((uspec_constraints, tests))
# Ensure we don't try to bootstrap clingo in parallel # Ensure we don't try to bootstrap clingo in parallel
if spack.config.get('config:concretizer') == 'clingo': if spack.config.get('config:concretizer') == 'clingo':
@ -2009,7 +2007,7 @@ def _tree_to_display(spec):
print('') print('')
def _concretize_from_constraints(spec_constraints, tests=False, reuse=None): def _concretize_from_constraints(spec_constraints, tests=False):
# Accept only valid constraints from list and concretize spec # Accept only valid constraints from list and concretize spec
# Get the named spec even if out of order # Get the named spec even if out of order
root_spec = [s for s in spec_constraints if s.name] root_spec = [s for s in spec_constraints if s.name]
@ -2028,7 +2026,7 @@ def _concretize_from_constraints(spec_constraints, tests=False, reuse=None):
if c not in invalid_constraints: if c not in invalid_constraints:
s.constrain(c) s.constrain(c)
try: try:
return s.concretized(tests=tests, reuse=reuse) return s.concretized(tests=tests)
except spack.spec.InvalidDependencyError as e: except spack.spec.InvalidDependencyError as e:
invalid_deps_string = ['^' + d for d in e.invalid_deps] invalid_deps_string = ['^' + d for d in e.invalid_deps]
invalid_deps = [c for c in spec_constraints invalid_deps = [c for c in spec_constraints
@ -2048,9 +2046,9 @@ def _concretize_from_constraints(spec_constraints, tests=False, reuse=None):
def _concretize_task(packed_arguments): def _concretize_task(packed_arguments):
spec_constraints, tests, reuse = packed_arguments spec_constraints, tests = packed_arguments
with tty.SuppressOutput(msg_enabled=False): with tty.SuppressOutput(msg_enabled=False):
return _concretize_from_constraints(spec_constraints, tests, reuse) return _concretize_from_constraints(spec_constraints, tests)
def make_repo_path(root): def make_repo_path(root):

View File

@ -9,7 +9,6 @@
import itertools import itertools
import os import os
import pprint import pprint
import sys
import types import types
import warnings import warnings
@ -474,18 +473,16 @@ def bootstrap_clingo():
class PyclingoDriver(object): class PyclingoDriver(object):
def __init__(self, cores=True, asp=None): def __init__(self, cores=True):
"""Driver for the Python clingo interface. """Driver for the Python clingo interface.
Arguments: Arguments:
cores (bool): whether to generate unsatisfiable cores for better cores (bool): whether to generate unsatisfiable cores for better
error reporting. error reporting.
asp (file-like): optional stream to write a text-based ASP program
for debugging or verification.
""" """
bootstrap_clingo() bootstrap_clingo()
self.out = asp or llnl.util.lang.Devnull() self.out = llnl.util.lang.Devnull()
self.cores = cores self.cores = cores
def title(self, name, char): def title(self, name, char):
@ -528,9 +525,30 @@ def fact(self, head, assumption=False):
self.assumptions.append(atom) self.assumptions.append(atom)
def solve( def solve(
self, solver_setup, specs, dump=None, nmodels=0, self,
timers=False, stats=False, setup,
specs,
nmodels=0,
timers=False,
stats=False,
out=None,
setup_only=False
): ):
"""Set up the input and solve for dependencies of ``specs``.
Arguments:
setup (SpackSolverSetup): An object to set up the ASP problem.
specs (list): List of ``Spec`` objects to solve for.
nmodels (list): Number of models to consider (default 0 for unlimited).
timers (bool): Print out coarse timers for different solve phases.
stats (bool): Whether to output Clingo's internal solver statistics.
out: Optional output stream for the generated ASP program.
setup_only (bool): if True, stop after setup and don't solve (default False).
"""
# allow solve method to override the output stream
if out is not None:
self.out = out
timer = spack.util.timer.Timer() timer = spack.util.timer.Timer()
# Initialize the control object for the solver # Initialize the control object for the solver
@ -545,9 +563,13 @@ def solve(
self.assumptions = [] self.assumptions = []
with self.control.backend() as backend: with self.control.backend() as backend:
self.backend = backend self.backend = backend
solver_setup.setup(self, specs) setup.setup(self, specs)
timer.phase("setup") timer.phase("setup")
# If we're only doing setup, just return an empty solve result
if setup_only:
return Result(specs)
# read in the main ASP program and display logic -- these are # read in the main ASP program and display logic -- these are
# handwritten, not generated, so we load them as resources # handwritten, not generated, so we load them as resources
parent_dir = os.path.dirname(__file__) parent_dir = os.path.dirname(__file__)
@ -2033,49 +2055,36 @@ class Solver(object):
``reuse (bool)`` ``reuse (bool)``
Whether to try to reuse existing installs/binaries Whether to try to reuse existing installs/binaries
``show (tuple)``
What information to print to the console while running. Options are:
* asp: asp program text
* opt: optimization criteria for best model
* output: raw clingo output
* solutions: models found by asp program
* all: all of the above
``models (int)``
Number of models to search (default: 0 for unlimited)
``timers (bool)``
Print out coarse fimers for different solve phases.
``stats (bool)``
Print out detailed stats from clingo
``tests (bool or tuple)``
If ``True``, concretize test dependencies for all packages. If
a tuple of package names, concretize test dependencies for named
packages. If ``False``, do not concretize test dependencies.
""" """
def __init__(self): def __init__(self):
self.set_default_configuration() self.driver = PyclingoDriver()
def set_default_configuration(self): # These properties are settable via spack configuration, and overridable
# These properties are settable via spack configuration. `None` # by setting them directly as properties.
# means go with the configuration setting; user can override. self.reuse = spack.config.get("concretizer:reuse", False)
self.reuse = None
# these are concretizer settings
self.dump = ()
self.models = 0
self.timers = False
self.stats = False
self.tests = False
def solve(self, specs):
driver = PyclingoDriver()
if "asp" in self.dump:
driver.out = sys.stdout
def solve(
self,
specs,
out=None,
models=0,
timers=False,
stats=False,
tests=False,
setup_only=False,
):
"""
Arguments:
specs (list): List of ``Spec`` objects to solve for.
out: Optionally write the generate ASP program to a file-like object.
models (int): Number of models to search (default: 0 for unlimited).
timers (bool): Print out coarse fimers for different solve phases.
stats (bool): Print out detailed stats from clingo.
tests (bool or tuple): If True, concretize test dependencies for all packages.
If a tuple of package names, concretize test dependencies for named
packages (defaults to False: do not concretize test dependencies).
setup_only (bool): if True, stop after setup and don't solve (default False).
"""
# Check upfront that the variants are admissible # Check upfront that the variants are admissible
for root in specs: for root in specs:
for s in root.traverse(): for s in root.traverse():
@ -2083,12 +2092,15 @@ def solve(self, specs):
continue continue
spack.spec.Spec.ensure_valid_variants(s) spack.spec.Spec.ensure_valid_variants(s)
if self.reuse is None: setup = SpackSolverSetup(reuse=self.reuse, tests=tests)
self.reuse = spack.config.get("concretizer:reuse", False) return self.driver.solve(
setup,
setup = SpackSolverSetup(reuse=self.reuse, tests=self.tests) specs,
return driver.solve( nmodels=models,
setup, specs, self.dump, self.models, self.timers, self.stats timers=timers,
stats=stats,
out=out,
setup_only=setup_only,
) )

View File

@ -2605,7 +2605,7 @@ def ensure_no_deprecated(root):
msg += " For each package listed, choose another spec\n" msg += " For each package listed, choose another spec\n"
raise SpecDeprecatedError(msg) raise SpecDeprecatedError(msg)
def _new_concretize(self, tests=False, reuse=None): def _new_concretize(self, tests=False):
import spack.solver.asp import spack.solver.asp
if not self.name: if not self.name:
@ -2616,10 +2616,7 @@ def _new_concretize(self, tests=False, reuse=None):
return return
solver = spack.solver.asp.Solver() solver = spack.solver.asp.Solver()
solver.reuse = reuse result = solver.solve([self], tests=tests)
solver.tests = tests
result = solver.solve([self])
result.raise_if_unsat() result.raise_if_unsat()
# take the best answer # take the best answer
@ -2637,23 +2634,17 @@ def _new_concretize(self, tests=False, reuse=None):
self._dup(concretized) self._dup(concretized)
self._mark_concrete() self._mark_concrete()
def concretize(self, tests=False, reuse=None): def concretize(self, tests=False):
"""Concretize the current spec. """Concretize the current spec.
Args: Args:
tests (bool or list): if False disregard 'test' dependencies, tests (bool or list): if False disregard 'test' dependencies,
if a list of names activate them for the packages in the list, if a list of names activate them for the packages in the list,
if True activate 'test' dependencies for all packages. if True activate 'test' dependencies for all packages.
reuse (bool): if True try to maximize reuse of already installed
specs, if False don't account for installation status.
""" """
if spack.config.get('config:concretizer') == "clingo": if spack.config.get('config:concretizer') == "clingo":
self._new_concretize(tests, reuse=reuse) self._new_concretize(tests)
else: else:
if reuse:
msg = ('maximizing reuse of installed specs is not '
'possible with the original concretizer')
raise spack.error.SpecError(msg)
self._old_concretize(tests) self._old_concretize(tests)
def _mark_root_concrete(self, value=True): def _mark_root_concrete(self, value=True):
@ -2678,7 +2669,7 @@ def _mark_concrete(self, value=True):
s.clear_cached_hashes() s.clear_cached_hashes()
s._mark_root_concrete(value) s._mark_root_concrete(value)
def concretized(self, tests=False, reuse=None): def concretized(self, tests=False):
"""This is a non-destructive version of concretize(). """This is a non-destructive version of concretize().
First clones, then returns a concrete version of this package First clones, then returns a concrete version of this package
@ -2688,11 +2679,9 @@ def concretized(self, tests=False, reuse=None):
tests (bool or list): if False disregard 'test' dependencies, tests (bool or list): if False disregard 'test' dependencies,
if a list of names activate them for the packages in the list, if a list of names activate them for the packages in the list,
if True activate 'test' dependencies for all packages. if True activate 'test' dependencies for all packages.
reuse (bool): if True try to maximize reuse of already installed
specs, if False don't account for installation status.
""" """
clone = self.copy(caches=True) clone = self.copy(caches=True)
clone.concretize(tests=tests, reuse=reuse) clone.concretize(tests=tests)
return clone return clone
def flat_dependencies(self, **kwargs): def flat_dependencies(self, **kwargs):

View File

@ -1345,7 +1345,7 @@ def test_non_default_provider_of_multiple_virtuals(self):
('mpich~debug', True) ('mpich~debug', True)
]) ])
def test_concrete_specs_are_not_modified_on_reuse( def test_concrete_specs_are_not_modified_on_reuse(
self, mutable_database, spec_str, expect_installed self, mutable_database, spec_str, expect_installed, config
): ):
if spack.config.get('config:concretizer') == 'original': if spack.config.get('config:concretizer') == 'original':
pytest.skip('Original concretizer cannot reuse specs') pytest.skip('Original concretizer cannot reuse specs')
@ -1354,7 +1354,8 @@ def test_concrete_specs_are_not_modified_on_reuse(
# when reused specs are added to the mix. This prevents things # when reused specs are added to the mix. This prevents things
# like additional constraints being added to concrete specs in # like additional constraints being added to concrete specs in
# the answer set produced by clingo. # the answer set produced by clingo.
s = spack.spec.Spec(spec_str).concretized(reuse=True) with spack.config.override("concretizer:reuse", True):
s = spack.spec.Spec(spec_str).concretized()
assert s.package.installed is expect_installed assert s.package.installed is expect_installed
assert s.satisfies(spec_str, strict=True) assert s.satisfies(spec_str, strict=True)