concretizer: handle compiler preferences with optimization

- [x] Add support for packages.yaml and command-line compiler preferences.
- [x] Rework compiler version propagation to use optimization rather than
  hard logic constraints
This commit is contained in:
Todd Gamblin 2020-01-02 00:54:54 -08:00
parent 1859ff31c9
commit f365373a3d
4 changed files with 122 additions and 69 deletions

View File

@ -16,7 +16,6 @@
import spack.cmd.common.arguments as arguments import spack.cmd.common.arguments as arguments
import spack.package import spack.package
import spack.solver.asp as asp import spack.solver.asp as asp
from spack.util.string import plural
description = "concretize a specs using an ASP solver" description = "concretize a specs using an ASP solver"
section = 'developer' section = 'developer'

View File

@ -50,10 +50,11 @@ class PackagePrefs(object):
provider_spec_list.sort(key=kf) provider_spec_list.sort(key=kf)
""" """
def __init__(self, pkgname, component, vpkg=None): def __init__(self, pkgname, component, vpkg=None, all=True):
self.pkgname = pkgname self.pkgname = pkgname
self.component = component self.component = component
self.vpkg = vpkg self.vpkg = vpkg
self.all = all
self._spec_order = None self._spec_order = None
@ -66,7 +67,7 @@ def __call__(self, spec):
""" """
if self._spec_order is None: if self._spec_order is None:
self._spec_order = self._specs_for_pkg( self._spec_order = self._specs_for_pkg(
self.pkgname, self.component, self.vpkg) self.pkgname, self.component, self.vpkg, self.all)
spec_order = self._spec_order spec_order = self._spec_order
# integer is the index of the first spec in order that satisfies # integer is the index of the first spec in order that satisfies
@ -114,12 +115,13 @@ def order_for_package(cls, pkgname, component, vpkg=None, all=True):
return [] return []
@classmethod @classmethod
def _specs_for_pkg(cls, pkgname, component, vpkg=None): def _specs_for_pkg(cls, pkgname, component, vpkg=None, all=True):
"""Given a sort order specified by the pkgname/component/second_key, """Given a sort order specified by the pkgname/component/second_key,
return a list of CompilerSpecs, VersionLists, or Specs for return a list of CompilerSpecs, VersionLists, or Specs for
that sorting list. that sorting list.
""" """
pkglist = cls.order_for_package(pkgname, component, vpkg) pkglist = cls.order_for_package(
pkgname, component, vpkg, all)
spec_type = _spec_type(component) spec_type = _spec_type(component)
return [spec_type(s) for s in pkglist] return [spec_type(s) for s in pkglist]

View File

@ -141,6 +141,7 @@ def __init__(self, out):
self.func = AspFunctionBuilder() self.func = AspFunctionBuilder()
self.possible_versions = {} self.possible_versions = {}
self.possible_virtuals = None self.possible_virtuals = None
self.possible_compilers = []
def title(self, name, char): def title(self, name, char):
self.out.write('\n') self.out.write('\n')
@ -260,7 +261,7 @@ def spec_versions(self, spec):
return [self.one_of(*predicates)] return [self.one_of(*predicates)]
return [] return []
def compiler_defaults(self): def available_compilers(self):
"""Facts about available compilers.""" """Facts about available compilers."""
compilers = spack.compilers.all_compiler_specs() compilers = spack.compilers.all_compiler_specs()
@ -276,28 +277,44 @@ def compiler_defaults(self):
for v in sorted(compiler_versions[compiler])), for v in sorted(compiler_versions[compiler])),
fn.compiler(compiler)) fn.compiler(compiler))
def package_compiler_defaults(self, pkg): def compilers_for_default_arch(self):
"""Add facts about the default compiler. default_arch = spack.spec.ArchSpec(spack.architecture.sys_type())
return [
compiler.spec
for compiler in spack.compilers.compilers_for_arch(default_arch)
]
TODO: integrate full set of preferences into the solve (this only def compiler_defaults(self):
TODO: considers the top preference) """Set compiler defaults, given a list of possible compilers."""
""" self.h2("Default compiler preferences")
# get list of all compilers
compiler_list = spack.compilers.all_compiler_specs()
if not compiler_list:
raise spack.compilers.NoCompilersError()
# prefer package preferences, then latest version compiler_list = self.possible_compilers.copy()
ppk = spack.package_prefs.PackagePrefs(pkg.name, 'compiler')
compiler_list = sorted( compiler_list = sorted(
compiler_list, key=lambda x: (x.name, x.version), reverse=True) compiler_list, key=lambda x: (x.name, x.version), reverse=True)
compiler_list = sorted(compiler_list, key=ppk) ppk = spack.package_prefs.PackagePrefs("all", 'compiler', all=False)
matches = sorted(compiler_list, key=ppk)
# write out default rules for this package's compilers for i, cspec in enumerate(matches):
default_compiler = compiler_list[0] f = fn.default_compiler_preference(cspec.name, cspec.version, i)
self.fact(fn.node_compiler_default(pkg.name, default_compiler.name)) self.fact(f)
self.fact(fn.node_compiler_default_version(
pkg.name, default_compiler.name, default_compiler.version)) def package_compiler_defaults(self, pkg):
"""Facts about packages' compiler prefs."""
packages = spack.config.get("packages")
pkg_prefs = packages.get(pkg)
if not pkg_prefs or "compiler" not in pkg_prefs:
return
compiler_list = self.possible_compilers.copy()
compiler_list = sorted(
compiler_list, key=lambda x: (x.name, x.version), reverse=True)
ppk = spack.package_prefs.PackagePrefs(pkg.name, 'compiler', all=False)
matches = sorted(compiler_list, key=ppk)
for i, cspec in enumerate(matches):
self.fact(fn.node_compiler_preference(
pkg.name, cspec.name, cspec.version, i))
def pkg_rules(self, pkg): def pkg_rules(self, pkg):
pkg = packagize(pkg) pkg = packagize(pkg)
@ -422,8 +439,8 @@ class Head(object):
arch_os = fn.arch_os_set arch_os = fn.arch_os_set
arch_target = fn.arch_target_set arch_target = fn.arch_target_set
variant = fn.variant_set variant = fn.variant_set
node_compiler = fn.node_compiler_set node_compiler = fn.node_compiler
node_compiler_version = fn.node_compiler_version_set node_compiler_version = fn.node_compiler_version
class Body(object): class Body(object):
node = fn.node node = fn.node
@ -464,10 +481,28 @@ class Body(object):
# compiler and compiler version # compiler and compiler version
if spec.compiler: if spec.compiler:
clauses.append(f.node_compiler(spec.name, spec.compiler.name)) clauses.append(f.node_compiler(spec.name, spec.compiler.name))
clauses.append(
fn.node_compiler_hard(spec.name, spec.compiler.name))
if spec.compiler.concrete: if spec.compiler.concrete:
clauses.append(f.node_compiler_version( clauses.append(f.node_compiler_version(
spec.name, spec.compiler.name, spec.compiler.version)) spec.name, spec.compiler.name, spec.compiler.version))
elif spec.compiler.versions:
compiler_list = spack.compilers.all_compiler_specs()
possible_compiler_versions = [
f.node_compiler_version(
spec.name, spec.compiler.name, compiler.version
)
for compiler in compiler_list
if compiler.satisfies(spec.compiler)
]
clauses.append(self.one_of(*possible_compiler_versions))
for version in possible_compiler_versions:
clauses.append(
fn.node_compiler_version_hard(
spec.name, spec.compiler.name, version))
# TODO # TODO
# external_path # external_path
# external_module # external_module
@ -525,11 +560,18 @@ def generate_asp_program(self, specs):
for name in pkg_names: for name in pkg_names:
pkg = spack.repo.path.get_pkg_class(name) pkg = spack.repo.path.get_pkg_class(name)
possible.update( possible.update(
pkg.possible_dependencies(virtuals=self.possible_virtuals) pkg.possible_dependencies(
virtuals=self.possible_virtuals,
deptype=("build", "link", "run")
)
) )
pkgs = set(possible) | set(pkg_names) pkgs = set(possible) | set(pkg_names)
# get possible compilers
self.possible_compilers = self.compilers_for_default_arch()
# read the main ASP program from concrtize.lp
concretize_lp = pkgutil.get_data('spack.solver', 'concretize.lp') concretize_lp = pkgutil.get_data('spack.solver', 'concretize.lp')
self.out.write(concretize_lp.decode("utf-8")) self.out.write(concretize_lp.decode("utf-8"))
@ -537,6 +579,7 @@ def generate_asp_program(self, specs):
self.build_version_dict(possible, specs) self.build_version_dict(possible, specs)
self.h1('General Constraints') self.h1('General Constraints')
self.available_compilers()
self.compiler_defaults() self.compiler_defaults()
self.arch_defaults() self.arch_defaults()
self.virtual_providers() self.virtual_providers()
@ -628,10 +671,13 @@ def call_actions_for_functions(self, function_strings):
for s in args] for s in args]
functions.append((name, args)) functions.append((name, args))
# Functions don't seem to be in particular order in output. # Functions don't seem to be in particular order in output. Sort
# Sort them here so that nodes are first, and so created # them here so that directives that build objects (like node and
# before directives that need them (depends_on(), etc.) # node_compiler) are called in the right order.
functions.sort(key=lambda f: f[0] != "node") functions.sort(key=lambda f: {
"node": -2,
"node_compiler": -1,
}.get(f[0], 0))
self._specs = {} self._specs = {}
for name, args in functions: for name, args in functions:
@ -639,6 +685,7 @@ def call_actions_for_functions(self, function_strings):
if not action: if not action:
print("%s(%s)" % (name, ", ".join(str(a) for a in args))) print("%s(%s)" % (name, ", ".join(str(a) for a in args)))
continue continue
assert action and callable(action) assert action and callable(action)
action(*args) action(*args)

View File

@ -143,45 +143,46 @@ arch_target_set(D, A) :- node(D), depends_on(P, D), arch_target_set(P, A).
%----------------------------------------------------------------------------- %-----------------------------------------------------------------------------
% one compiler per node % one compiler per node
{ node_compiler(P, C) : node_compiler(P, C) } = 1 :- node(P). 1 { node_compiler(P, C) : compiler(C) } 1 :- node(P).
{ node_compiler_version(P, C, V) : 1 { node_compiler_version(P, C, V) : compiler_version(C, V) } 1 :- node(P).
node_compiler_version(P, C, V) } = 1 :- node(P). 1 { compiler_weight(P, N) : compiler_weight(P, N) } 1 :- node(P).
% compiler fields are set if set to anything % dependencies imply we should try to match hard compiler constraints
node_compiler_set(P) :- node_compiler_set(P, _). % todo: look at what to do about intersecting constraints here. we'd
node_compiler_version_set(P, C) :- node_compiler_version_set(P, C, _). % ideally go with the "lowest" pref in the DAG
node_compiler_match_pref(P, C) :- node_compiler_hard(P, C).
node_compiler_match_pref(D, C)
:- depends_on(P, D), node_compiler_match_pref(P, C),
not node_compiler_hard(D, _).
compiler_match(P, 1) :- node_compiler(P, C), node_compiler_match_pref(P, C).
% avoid warnings: these are set by generated code and it's ok if they're not node_compiler_version_match_pref(P, C, V)
#defined node_compiler_set/2. :- node_compiler_version_hard(P, C, V).
#defined node_compiler_version_set/3. node_compiler_version_match_pref(D, C, V)
:- depends_on(P, D), node_compiler_version_match_pref(P, C, V),
not node_compiler_version_hard(D, C, _).
compiler_version_match(P, 1)
:- node_compiler_version(P, C, V),
node_compiler_version_match_pref(P, C, V).
% if compiler value of node is set to anything, it's the value. #defined node_compiler_hard/2.
node_compiler(P, C) #defined node_compiler_version_hard/3.
:- node(P), compiler(C), node_compiler_set(P, C).
node_compiler_version(P, C, V)
:- node(P), compiler(C), compiler_version(C, V), node_compiler(P, C),
node_compiler_version_set(P, C, V).
% node compiler versions can only be from the available compiler versions % compilers weighted by preference acccording to packages.yaml
:- node(P), compiler(C), node_compiler(P, C), node_compiler_version(P, C, V), compiler_weight(P, N)
not compiler_version(C, V). :- node_compiler(P, C), node_compiler_version(P, C, V),
node_compiler_preference(P, C, V, N).
% if no compiler is set, fall back to default. compiler_weight(P, N)
node_compiler(P, C) :- node_compiler(P, C), node_compiler_version(P, C, V),
:- node(P), compiler(C), not node_compiler_set(P), not node_compiler_preference(P, C, _, _),
node_compiler_default(P, C). default_compiler_preference(C, V, N).
node_compiler_version(P, C, V) compiler_weight(P, 100)
:- node(P), compiler(C), compiler_version(C, V), :- node_compiler(P, C), node_compiler_version(P, C, V),
not node_compiler_version_set(P, C, V), not node_compiler_preference(P, C, _, _),
node_compiler_default_version(P, C, V). not default_compiler_preference(C, _, _).
% propagate compiler, compiler version to dependencies
node_compiler_set(D, C)
:- node(D), compiler(C), depends_on(P, D), node_compiler_set(P, C).
node_compiler_version_set(D, C, V)
:- node(D), compiler(C), depends_on(P, D), node_compiler(D, C),
node_compiler_version_set(P, C, V).
#defined node_compiler_preference/4.
#defined default_compiler_preference/3.
%----------------------------------------------------------------------------- %-----------------------------------------------------------------------------
% How to optimize the spec (high to low priority) % How to optimize the spec (high to low priority)
@ -206,11 +207,15 @@ node_compiler_version_set(D, C, V)
root(D, 2) :- root(D), node(D). root(D, 2) :- root(D), node(D).
root(D, 1) :- not root(D), node(D). root(D, 1) :- not root(D), node(D).
% prefer default variants
#minimize { N*R@5,P,V,X : variant_not_default(P, V, X, N), root(P, R) }.
% prefer more recent versions.
#minimize{ N@4,P,V : version_weight(P, V, N) }.
% pick most preferred virtual providers % pick most preferred virtual providers
#minimize{ N*R@3,D : provider_weight(D, N), root(P, R) }. #minimize{ N*R@3,D : provider_weight(D, N), root(P, R) }.
% prefer default variants % compiler preferences
#minimize { N*R@2,P,V,X : variant_not_default(P, V, X, N), root(P, R) }. #maximize{ N@2,P : compiler_match(P, N) }.
#minimize{ N@1,P : compiler_weight(P, N) }.
% prefer more recent versions.
#minimize{ N@1,P,V : version_weight(P, V, N) }.