Files
TheRock/rocm-libraries/projects/hipblaslt/tensilelite/Tensile/Utilities/merge.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

538 lines
23 KiB
Python

################################################################################
#
# Copyright (C) 2022-2025 Advanced Micro Devices, Inc. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
################################################################################
import yaml
import os
import sys
import shutil
import argparse
from copy import deepcopy
from enum import IntEnum
verbosity = 1
def ensurePath(path):
if not os.path.exists(path):
os.makedirs(path)
return path
def allFiles(startDir):
current = os.listdir(startDir)
files = []
for filename in [_current for _current in current if os.path.splitext(_current)[-1].lower() == '.yaml']:
fullPath = os.path.join(startDir,filename)
if os.path.isdir(fullPath):
files = files + allFiles(fullPath)
else:
files.append(fullPath)
return files
def reindexSolutions(data):
for i, _ in enumerate(data[5]):
data[5][i]["SolutionIndex"] = i
return data
def fixSizeInconsistencies(sizes, fileType):
duplicates = list()
for i in range(0,len(sizes)):
currSize = sizes[i][0]
# >= so size will be trimmed when a SolutionTag is included
if len(currSize) >= 8:
currSize = currSize[:-4]
if currSize in (item for index in sizes for item in index):
duplicates.append(i-len(duplicates))
else:
sizes[i][0] = currSize
sizes_ = deepcopy(sizes)
if len(duplicates) > 0:
for i in duplicates:
sizes_.pop(i)
verbose(len(duplicates), "duplicate size(s) removed from", fileType, "logic file")
return sizes_, len(sizes_)
# remove dict keys "SolutionIndex" and "SolutionNameMin" from dict
def cmpHelper(sol):
return {k:v for k, v in sol.items() if k!="SolutionIndex" and k!="SolutionNameMin"}
def addKernel(solutionPool, solution):
for item in solutionPool:
if cmpHelper(item) == cmpHelper(solution):
index = item["SolutionIndex"]
debug("...Reuse previously existed kernel", end="")
break
else:
index = len(solutionPool)
_solution = deepcopy(solution) # if we don't we will see some subtle errors
_solution["SolutionIndex"] = index
solutionPool.append(_solution)
debug("...A new kernel has been added", end="")
debug("({}) {}".format(index, solutionPool[index]["SolutionNameMin"] if "SolutionNameMin" in solutionPool[index] else "(SolutionName N/A)"))
return solutionPool, index
# update dependant parameters if StaggerU == 0
def sanitizeSolutions(solList):
for sol in solList:
if "ProblemType" in sol:
del sol["ProblemType"] # remove ProblemType from solution
if sol.get("StaggerU") == 0:
sol["StaggerUMapping"] = 0
sol["StaggerUStride"] = 0
sol["_staggerStrideShift"] = 0
def removeUnusedKernels(oriData, prefix=""):
origNumSolutions = len(oriData[5])
kernelsInUse = [ index for _, [index, _] in oriData[7] ]
for i, solution in enumerate(oriData[5]):
solutionIndex = solution["SolutionIndex"]
oriData[5][i]["__InUse__"] = True if solutionIndex in kernelsInUse else False
# debug prints
for o in [o for o in oriData[5] if o["__InUse__"]==False]:
debug("{}Solution ({}) {} is unused".format(
prefix,
o["SolutionIndex"],
o["SolutionNameMin"] if "SolutionNameMin" in o else "(SolutionName N/A)"))
# filter out dangling kernels
oriData[5] = [ {k: v for k, v in o.items() if k != "__InUse__"}
for o in oriData[5] if o["__InUse__"]==True ]
# reindex solutions
idMap = {} # new = idMap[old]
for i, solution in enumerate(oriData[5]):
idMap[solution["SolutionIndex"]] = i
oriData[5][i]["SolutionIndex"] = i
for i, [size, [oldSolIndex, eff]] in enumerate(oriData[7]):
oriData[7][i] = [size, [idMap[oldSolIndex], eff]]
numInvalidRemoved = origNumSolutions - len(oriData[5])
return oriData, numInvalidRemoved
def loadData(filename):
try:
stream = open(filename, "r")
except IOError:
print("Cannot open file: ", filename)
sys.stdout.flush()
sys.exit(-1)
data = yaml.load(stream, yaml.SafeLoader)
return data
def compareDestFolderToYaml(originalDir, incFile, incData):
checkFolders = ["Equality", "GridBased"]
# Parsing destination folder and yaml attribute
destFolder = originalDir.rstrip('/').split('/')[-1]
incAttribute = incData[11] # the last item in yaml file
if not incAttribute:
sys.exit(f"[Error] Empty YAML attribute. Need to set Equality or GridBased in {incFile}.")
# Check Equality and GridBased folders only
if destFolder in checkFolders and destFolder != incAttribute:
restuls = f"\t{incFile} must be {destFolder} tuning"
sys.exit(f"[Error] Destination folder(={destFolder}) failed to match YAML attribute(={incAttribute}): \n{restuls}")
def compareProblemType(oriData, incData):
# ProblemType defined in originalFiles and incrementalFiles
oriProblemType = oriData[4] # header
incProblemType = incData[4] # header
# Delete waived ProblemType items in originalFiles
waivedItems = [item for item in oriProblemType if item not in incProblemType]
if waivedItems:
# Header ProblemType
for item in waivedItems:
oriProblemType.pop(item)
# Kernel ProblemType
for i, _ in enumerate(oriData[5]):
# TODO: delete this for loop if kernel ProblemType is removed in the future
if "ProblemType" not in oriData[5][i]:
continue
oriKernelProblemType = oriData[5][i]["ProblemType"]
for item in waivedItems:
try:
oriKernelProblemType.pop(item)
except KeyError:
oriSolutionIndex = oriData[5][i]["SolutionIndex"]
print(f"[Warning] Popping '{item}' failed in oriData(idx={oriSolutionIndex})")
results = ""
solIdx = 0
# Compare existing ProblemType items of originalFiles with incrementalFiles
for i, _ in enumerate(incData[5]):
if "ProblemType" not in incData[5][i]:
continue
# TODO: check header ProblemType if kernel ProblemType is removed in the future
incKernelProblemType = incData[5][i]["ProblemType"]
if oriProblemType != incKernelProblemType:
for item in oriProblemType:
if oriProblemType[item] != incKernelProblemType[item]:
results += f"\t{item}: {oriProblemType[item]} != {incKernelProblemType[item]}\n"
solIdx = i
break
if (results):
sys.exit(f"[Error] ProblemType in library logic doesn't match solution(idx={solIdx}): \n{results}")
# this is for complying the behavior of legacy merge script, where incremental logic
# file always replaces the base logic file even it's slower in performance -
# in the future we may let default force merge policy = False
def defaultForceMergePolicy(incFile):
if "arcturus" in incFile:
forceMerge = False
else:
forceMerge = True
return forceMerge
def msg(*args, **kwargs):
for i in args: print(i, end=" ")
print(**kwargs)
def verbose(*args, **kwargs):
if verbosity < 1: return
msg(*args, **kwargs)
def debug(*args, **kwargs):
if verbosity < 2: return
msg(*args, **kwargs)
# Tags distinguishing solution types
# Can be added to size key to allow solutions of each type to be present
# in logic file for a given size
class MfmaTag(IntEnum):
VALU = 0
MFMA = 1
def __str__(self):
return ["VALU", "MFMA"][self]
def __repr__(self):
return str(self)
class AlphaValueTag(IntEnum):
ANY = 0
ONE = 1
NEG_ONE = 2
ZERO = 3
def __str__(self):
return "Alpha="+["Any", "1", "-1", "0"][self]
def __repr__(self):
return str(self)
class BetaValueTag(IntEnum):
ANY = 0
ONE = 1
NEG_ONE = 2
ZERO = 3
def __str__(self):
return "Beta="+["Any", "1", "-1", "0"][self]
def __repr__(self):
return str(self)
def strToScalarValueTag(Class, value):
if value == "Any":
return Class.ANY
if value == 1:
return Class.ONE
if value == -1:
return Class.NEG_ONE
if value == 0:
return Class.ZERO
else:
raise RuntimeError("Unsupported value for Alpha/Beta scalar value")
class CEqualsDTag(IntEnum):
C_EQ_D = 0
C_NEQ_D = 1
def __str__(self):
return ["C=D", "C!=D"][self]
def __repr__(self):
return str(self)
# Tag of form (MFMATag, AlphaValueTag, BetaValueTag, CEqualsDTag)
def getSolutionTag(solution):
tagTuple = ()
if solution.get("EnableMatrixInstruction", False) or solution.get("MatrixInstruction", False):
tagTuple = tagTuple + (MfmaTag.MFMA,)
else:
tagTuple = tagTuple + (MfmaTag.VALU,)
return tagTuple
def findSolutionWithIndex(solutionData, solIndex):
# Check solution at the index corresponding to solIndex first
if solIndex < len(solutionData) and solutionData[solIndex]["SolutionIndex"] == solIndex:
return solutionData[solIndex]
else:
debug("Searching for index...")
solution = [s for s in solutionData if s["SolutionIndex"]==solIndex]
assert(len(solution) == 1)
return solution[0]
def addSolutionTagToKeys(solutionMap, solutionPool):
return [[[getSolutionTag(findSolutionWithIndex(solutionPool, idx))] + keys, [idx, eff]]
for [keys, [idx, eff]] in solutionMap]
def removeSolutionTagFromKeys(solutionMap):
return [[keys[1:], [idx, incEff]] for keys, [idx, incEff] in solutionMap]
# To be used with add_solution_tags to allow faster general solutions to supercede slower specific ones
def findFastestCompatibleSolution(origDict, sizeMapping):
tags = sizeMapping[0]
# Tag of form (MFMATag, AlphaValueTag, BetaValueTag, CEqualsDTag)
compatibleTagList = [tags]
# Add all compatible tags to the list
if tags[1] != AlphaValueTag.ANY:
compatibleTagList = compatibleTagList + [(t[0], AlphaValueTag.ANY) + t[2:] for t in compatibleTagList]
if tags[2] != BetaValueTag.ANY:
compatibleTagList = compatibleTagList + [t[:2] + (BetaValueTag.ANY,) + t[3:] for t in compatibleTagList]
if tags[3] != CEqualsDTag.C_NEQ_D:
compatibleTagList = compatibleTagList + [t[:3] + (CEqualsDTag.C_NEQ_D,) + t[4:] for t in compatibleTagList]
#Find the fastest efficiency of all compatible tags
maxEfficiency = 0
for tag in compatibleTagList:
result = origDict.get((tag,) + sizeMapping[1:], None)
if result:
_, eff = origDict[(tag,) + sizeMapping[1:]]
maxEfficiency = max(maxEfficiency, eff)
return maxEfficiency
# returns merged logic data as list
def mergeLogic(oriData, incData, forceMerge, trimSize=True, addSolutionTags=False, noEff=False):
origNumSizes = len(oriData[7])
origNumSolutions = len(oriData[5])
incData[7] = incData[7] or []
incNumSizes = len(incData[7])
incNumSolutions = len(incData[5])
verbose(origNumSizes, "sizes and", origNumSolutions, "kernels in base logic file")
verbose(incNumSizes, "sizes and", incNumSolutions, "kernels in incremental logic file")
# Add SolutionTag to distinguish solutions with different requirements
origTaggedSizes = addSolutionTagToKeys(oriData[7], oriData[5])
incTaggedSizes = addSolutionTagToKeys(incData[7], incData[5])
if addSolutionTags:
oriData[7] = origTaggedSizes
incData[7] = incTaggedSizes
# Print warning if addSolutionTags=False results in removed sizes
else:
origSet = {tuple(size) for size, [_, _] in oriData[7]}
origTaggedSet = {tuple(size) for size, [_, _] in origTaggedSizes}
incSet = {tuple(size) for size, [_, _] in incData[7]}
incTaggedSet = {tuple(size) for size, [_, _] in incTaggedSizes}
if len(origSet) != len(origTaggedSet):
verbose("Warning:", len(origTaggedSet) - len(origSet), "duplicate sizes are present in base logic",
"that may not be handled correctly unless --add_solution_tags is used")
if len(incSet) != len(incTaggedSet):
verbose("Warning:", len(incTaggedSet) - len(incSet), "duplicate sizes are present in incremental logic",
"that may not be handled correctly unless --add_solution_tags is used")
if trimSize:
# trim 8-tuple gemm size format to 4-tuple [m, n, b, k]
# TODO future gemm size could include dictionary format so need robust preprocessing
[oriData[7], origNumSizes] = fixSizeInconsistencies(oriData[7], "base")
[incData[7], incNumSizes] = fixSizeInconsistencies(incData[7], "incremental")
sanitizeSolutions(oriData[5])
sanitizeSolutions(incData[5])
oriData, numOrigRemoved = removeUnusedKernels(oriData, "Base logic file: ")
incData, numIncRemoved = removeUnusedKernels(incData, "Inc logic file: ")
solutionPool = deepcopy(oriData[5])
solutionMap = deepcopy(oriData[7])
origDict = {tuple(origSize): [i, origEff] for i, [origSize, [origIndex, origEff]] in enumerate(oriData[7])}
for incSize, [incIndex, incEff] in incData[7]:
incSolution = findSolutionWithIndex(incData[5], incIndex)
storeEff = incEff if noEff == False else 0.0
try:
j, origEff = origDict[tuple(incSize)]
if incEff > origEff or forceMerge:
if incEff > origEff:
verbose("[O]", incSize, "already exists and has improved in performance.", end="")
elif forceMerge:
verbose("[!]", incSize, "already exists but does not improve in performance.", end="")
verbose("Efficiency:", origEff, "->", incEff, "(force_merge=True)" if forceMerge else "")
solutionPool, index = addKernel(solutionPool, incSolution)
solutionMap[j][1] = [index, storeEff]
else:
verbose("[X]", incSize, "already exists but does not improve in performance.", end="")
verbose("Efficiency:", origEff, "->", incEff)
except KeyError:
if addSolutionTags and findFastestCompatibleSolution(origDict, tuple(incSize)) > incEff:
verbose("[X]", incSize, "has been rejected because a compatible solution already exists with higher performance")
else:
verbose("[-]", incSize, "has been added to solution table, Efficiency: N/A ->", incEff)
solutionPool, index = addKernel(solutionPool, incSolution)
solutionMap.append([incSize,[index, storeEff]])
verbose(numOrigRemoved, "unused kernels removed from base logic file")
verbose(numIncRemoved, "unused kernels removed from incremental logic file")
# Remove SolutionTag for yaml output
if addSolutionTags:
solutionMap = removeSolutionTagFromKeys(solutionMap)
mergedData = deepcopy(oriData)
mergedData[5] = solutionPool
mergedData[7] = solutionMap
mergedData, numReplaced = removeUnusedKernels(mergedData, "Merged data: ")
numSizesAdded = len(solutionMap)-len(oriData[7])
numSolutionsAdded = len(solutionPool)-len(oriData[5])
numSolutionsRemoved = numReplaced+numOrigRemoved # incremental file not counted
return [mergedData, numSizesAdded, numSolutionsAdded, numSolutionsRemoved]
def avoidRegressions(originalDir, incrementalDir, outputPath, forceMerge, trimSize=True, addSolutionTags=False, noEff=False):
originalFiles = allFiles(originalDir)
incrementalFiles = allFiles(incrementalDir)
ensurePath(outputPath)
incrementalFilesTemp = []
originalFileNames = [os.path.split(o)[-1] for o in originalFiles]
for file in incrementalFiles:
if os.path.split(file)[-1] in originalFileNames:
incrementalFilesTemp.append(file)
else:
outputFile = os.path.join(outputPath, os.path.split(file)[-1])
shutil.copyfile(file, outputFile)
msg("Copied", file, "to", outputFile)
incrementalFiles = incrementalFilesTemp
for incFile in incrementalFiles:
basename = os.path.split(incFile)[-1]
origFile = os.path.join(originalDir, basename)
forceMerge = defaultForceMergePolicy(incFile) if forceMerge is None else forceMerge
msg("Base logic file:", origFile, "| Incremental:", incFile, "| Merge policy: %s"%("Forced" if forceMerge else "Winner"), "| Trim size:", trimSize,
"| Add solution tags:", addSolutionTags)
oriData = loadData(origFile)
incData = loadData(incFile)
# Terminate when the destination folder doesn't match Incremental logic yaml
# For example, merge Gridbased yaml to Equality folder or Equality yaml to GridBased folder
compareDestFolderToYaml(originalDir, incFile, incData)
# Terminate when ProblemType of originalFiles and incrementalFiles mismatch
compareProblemType(oriData, incData)
# So far "SolutionIndex" in logic yamls has zero impact on actual 1-1 size mapping (but the order of the Solution does)
# since mergeLogic() takes that value very seriously so we reindex them here so it doesn't choke on duplicated SolutionIndex
oriData = reindexSolutions(oriData)
incData = reindexSolutions(incData)
mergedData, *stats = mergeLogic(oriData, incData, forceMerge, trimSize, addSolutionTags, noEff)
msg(stats[0], "size(s) and", stats[1], "kernel(s) added,", stats[2], "kernel(s) removed")
with open(os.path.join(outputPath, basename), "w") as outFile:
yaml.safe_dump(mergedData,outFile,default_flow_style=None)
msg("File written to", os.path.join(outputPath, basename))
msg("------------------------------")
# partialLogicFilePaths: list of full paths to partial logic files
# outputDir: Directory to write the final result to
# forceMerge:
# trimSize:
# Expects: that all the partial logic files
# have the same base name, but are located
# in different folders.
# Provides: one final logic file that is the
# merged result of all partial files.
# This is useful for when a tuning task is
# shared between multiple machines who each
# will provide a partial result.
def mergePartialLogics(partialLogicFilePaths, outputDir, forceMerge, trimSize=True, addSolutionTags=False):
logicFiles = deepcopy(partialLogicFilePaths)
ensurePath(outputDir)
baseLogicFile = logicFiles.pop(0)
baseLogicData = loadData(baseLogicFile)
msg("Base logic file:", baseLogicFile)
for f in logicFiles:
forceMerge = defaultForceMergePolicy(f) if forceMerge is None else forceMerge
msg("Incremental file:", f, "| Merge policy: %s"%("Forced" if forceMerge else "Winner"), "| Trim size:", trimSize)
incLogicData = loadData(f)
# So far "SolutionIndex" in logic yamls has zero impact on actual 1-1 size mapping (but the order of the Solution does)
# since mergeLogic() takes that value very seriously so we reindex them here so it doesn't choke on duplicated SolutionIndex
baseLogicData = reindexSolutions(baseLogicData)
incLogicData = reindexSolutions(incLogicData)
mergedData, *stats = mergeLogic(baseLogicData, incLogicData, forceMerge, trimSize, addSolutionTags)
msg(stats[0], "size(s) and", stats[1], "kernel(s) added,", stats[2], "kernel(s) removed")
# Use the merged data as the base data for the next partial logic file
baseLogicData = deepcopy(mergedData)
baseFileName = os.path.basename(baseLogicFile)
outputFilePath = os.path.join(outputDir, baseFileName)
with open(outputFilePath, "w") as outFile:
yaml.safe_dump(baseLogicData, outFile, default_flow_style=None)
msg("File written to", outputFilePath)
msg("------------------------------")
if __name__ == "__main__":
argParser = argparse.ArgumentParser()
argParser.add_argument("original_dir", help="The library logic directory without tuned sizes")
argParser.add_argument("incremental_dir", help="The incremental logic directory")
argParser.add_argument("output_dir", help="The output logic directory")
argParser.add_argument("-v", "--verbosity", help="0: summary, 1: verbose, 2: debug", default=1, type=int)
argParser.add_argument("--force_merge", help="Merge previously known sizes unconditionally. Default behavior if not arcturus", default="none")
argParser.add_argument("--notrim", help="Do not trim long size format down to short format (m,n,b,k). Default is --trim", action="store_false")
argParser.add_argument("--add_solution_tags", help="Add tags to the size key for solution properies, allowing for solutions with different requirements "
"to exist for the same size. Default doesn't add this tag.", action="store_true")
argParser.add_argument("--no_eff", help="force set eff as 0.0.", action="store_true")
args = argParser.parse_args(sys.argv[1:])
originalDir = args.original_dir
incrementalDir = args.incremental_dir
outputPath = args.output_dir
verbosity = args.verbosity
forceMerge = args.force_merge.lower()
trimSize = args.notrim
add_solution_tags = args.add_solution_tags
no_eff = args.no_eff
if forceMerge in ["none"]: forceMerge=None
elif forceMerge in ["true", "1"]: forceMerge=True
elif forceMerge in ["false", "0"]: forceMerge=False
msg("***************************************************************")
msg("Warning: merge.py is deprecated, please use TensileMergeLibrary")
msg("***************************************************************")
avoidRegressions(originalDir, incrementalDir, outputPath, forceMerge, trimSize, add_solution_tags, no_eff)