Skip to content

Commit

Permalink
[NPM] Automatic 'opt' pipeline reducer script.
Browse files Browse the repository at this point in the history
Script for automatic 'opt' pipeline reduction for when using the new
pass-manager (NPM). Based around the '-print-pipeline-passes' option.

The reduction algorithm consists of several phases (steps).

Step #0: Verify that input fails with the given pipeline and make note of the
error code.

Step #1: Split pipeline in two starting from front and move forward as long as
first pipeline exits normally and the second pipeline fails with the expected
error code. Move on to step #2 with the IR from the split point and the
pipeline from the second invocation.

Step #2: Remove passes from end of the pipeline as long as the pipeline fails
with the expected error code.

Step #3: Make several sweeps over the remaining pipeline trying to remove one
pass at a time. Repeat sweeps until unable to remove any more passes.

Usage example:
./utils/reduce_pipeline.py --opt-binary=./build-all-Debug/bin/opt --input=input.ll --output=output.ll --passes=PIPELINE [EXTRA-OPT-ARGS ...]

Differential Revision: https://reviews.llvm.org/D110908
  • Loading branch information
markuslavin authored and memfrob committed Oct 4, 2022
1 parent 3b5187f commit 96ba6f2
Show file tree
Hide file tree
Showing 4 changed files with 549 additions and 0 deletions.
172 changes: 172 additions & 0 deletions llvm/utils/pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Automatically formatted with yapf (https://github.com/google/yapf)
"""Utility functions for creating and manipulating LLVM 'opt' NPM pipeline objects."""


def fromStr(pipeStr):
"""Create pipeline object from string representation."""
stack = []
curr = []
tok = ''
kind = ''
for c in pipeStr:
if c == ',':
if tok != '':
curr.append([None, tok])
tok = ''
elif c == '(':
stack.append([kind, curr])
kind = tok
curr = []
tok = ''
elif c == ')':
if tok != '':
curr.append([None, tok])
tok = ''
oldKind = kind
oldCurr = curr
[kind, curr] = stack.pop()
curr.append([oldKind, oldCurr])
else:
tok += c
if tok != '':
curr.append([None, tok])
return curr


def toStr(pipeObj):
"""Create string representation of pipeline object."""
res = ''
lastIdx = len(pipeObj) - 1
for i, c in enumerate(pipeObj):
if c[0]:
res += c[0] + '('
res += toStr(c[1])
res += ')'
else:
res += c[1]
if i != lastIdx:
res += ','
return res


def count(pipeObj):
"""Count number of passes (pass-managers excluded) in pipeline object."""
cnt = 0
for c in pipeObj:
if c[0]:
cnt += count(c[1])
else:
cnt += 1
return cnt


def split(pipeObj, splitIndex):
"""Create two new pipeline objects by splitting pipeObj in two directly after pass with index splitIndex."""
def splitInt(src, splitIndex, dstA, dstB, idx):
for s in src:
if s[0]:
dstA2 = []
dstB2 = []
idx = splitInt(s[1], splitIndex, dstA2, dstB2, idx)
dstA.append([s[0], dstA2])
dstB.append([s[0], dstB2])
else:
if idx <= splitIndex:
dstA.append([None, s[1]])
else:
dstB.append([None, s[1]])
idx += 1
return idx

listA = []
listB = []
splitInt(pipeObj, splitIndex, listA, listB, 0)
return [listA, listB]


def remove(pipeObj, removeIndex):
"""Create new pipeline object by removing pass with index removeIndex from pipeObj."""
def removeInt(src, removeIndex, dst, idx):
for s in src:
if s[0]:
dst2 = []
idx = removeInt(s[1], removeIndex, dst2, idx)
dst.append([s[0], dst2])
else:
if idx != removeIndex:
dst.append([None, s[1]])
idx += 1
return idx

dst = []
removeInt(pipeObj, removeIndex, dst, 0)
return dst


def copy(srcPipeObj):
"""Create copy of pipeline object srcPipeObj."""
def copyInt(dst, src):
for s in src:
if s[0]:
dst2 = []
copyInt(dst2, s[1])
dst.append([s[0], dst2])
else:
dst.append([None, s[1]])

dstPipeObj = []
copyInt(dstPipeObj, srcPipeObj)
return dstPipeObj


def prune(srcPipeObj):
"""Create new pipeline object by removing empty pass-managers (those with count = 0) from srcPipeObj."""
def pruneInt(dst, src):
for s in src:
if s[0]:
if count(s[1]):
dst2 = []
pruneInt(dst2, s[1])
dst.append([s[0], dst2])
else:
dst.append([None, s[1]])

dstPipeObj = []
pruneInt(dstPipeObj, srcPipeObj)
return dstPipeObj


if __name__ == "__main__":
import unittest

class Test(unittest.TestCase):
def test_0(self):
pipeStr = 'a,b,A(c,B(d,e),f),g'
pipeObj = fromStr(pipeStr)

self.assertEqual(7, count(pipeObj))

self.assertEqual(pipeObj, pipeObj)
self.assertEqual(pipeObj, prune(pipeObj))
self.assertEqual(pipeObj, copy(pipeObj))

self.assertEqual(pipeStr, toStr(pipeObj))
self.assertEqual(pipeStr, toStr(prune(pipeObj)))
self.assertEqual(pipeStr, toStr(copy(pipeObj)))

[pipeObjA, pipeObjB] = split(pipeObj, 3)
self.assertEqual('a,b,A(c,B(d))', toStr(pipeObjA))
self.assertEqual('A(B(e),f),g', toStr(pipeObjB))

self.assertEqual('b,A(c,B(d,e),f),g', toStr(remove(pipeObj, 0)))
self.assertEqual('a,b,A(c,B(d,e),f)', toStr(remove(pipeObj, 6)))

pipeObjC = remove(pipeObj, 4)
self.assertEqual('a,b,A(c,B(d),f),g', toStr(pipeObjC))
pipeObjC = remove(pipeObjC, 3)
self.assertEqual('a,b,A(c,B(),f),g', toStr(pipeObjC))
pipeObjC = prune(pipeObjC)
self.assertEqual('a,b,A(c,f),g', toStr(pipeObjC))

unittest.main()
exit(0)
212 changes: 212 additions & 0 deletions llvm/utils/reduce_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#!/usr/bin/env python3

# Automatically formatted with yapf (https://github.com/google/yapf)

# Script for automatic 'opt' pipeline reduction for when using the new
# pass-manager (NPM). Based around the '-print-pipeline-passes' option.
#
# The reduction algorithm consists of several phases (steps).
#
# Step #0: Verify that input fails with the given pipeline and make note of the
# error code.
#
# Step #1: Split pipeline in two starting from front and move forward as long as
# first pipeline exits normally and the second pipeline fails with the expected
# error code. Move on to step #2 with the IR from the split point and the
# pipeline from the second invocation.
#
# Step #2: Remove passes from end of the pipeline as long as the pipeline fails
# with the expected error code.
#
# Step #3: Make several sweeps over the remaining pipeline trying to remove one
# pass at a time. Repeat sweeps until unable to remove any more passes.
#
# Usage example:
# reduce_pipeline.py --opt-binary=./build-all-Debug/bin/opt --input=input.ll --output=output.ll --passes=PIPELINE [EXTRA-OPT-ARGS ...]

import argparse
import pipeline
import shutil
import subprocess
import tempfile

parser = argparse.ArgumentParser(
description=
'Automatic opt pipeline reducer. Unrecognized arguments are forwarded to opt.'
)
parser.add_argument('--opt-binary',
action='store',
dest='opt_binary',
default='opt')
parser.add_argument('--passes', action='store', dest='passes', required=True)
parser.add_argument('--input', action='store', dest='input', required=True)
parser.add_argument('--output', action='store', dest='output')
parser.add_argument('--dont-expand-passes',
action='store_true',
dest='dont_expand_passes',
help='Do not expand pipeline before starting reduction.')
parser.add_argument(
'--dont-remove-empty-pm',
action='store_true',
dest='dont_remove_empty_pm',
help='Do not remove empty pass-managers from the pipeline during reduction.'
)
[args, extra_opt_args] = parser.parse_known_args()

print('The following extra args will be passed to opt: {}'.format(
extra_opt_args))

lst = pipeline.fromStr(args.passes)
passes = '-passes={}'.format(pipeline.toStr(lst))
ll_input = args.input

# Step #-1
# Launch 'opt' once with '-print-pipeline-passes' to expand pipeline before
# starting reduction. Allows specifying a default pipelines (e.g.
# '-passes=default<O3>').
if not args.dont_expand_passes:
run_args = [
args.opt_binary, '-disable-symbolication', '-disable-output',
'-print-pipeline-passes', passes, ll_input
]
run_args.extend(extra_opt_args)
opt = subprocess.run(run_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if opt.returncode != 0:
print('Failed to expand passes. Aborting.')
print(run_args)
print('exitcode: {}'.format(opt.returncode))
print(opt.stderr.decode())
exit(1)
stdout = opt.stdout.decode()
stdout = stdout[:stdout.rfind('\n')]
print('Expanded pass sequence: {}'.format(stdout))
passes = '-passes={}'.format(stdout)

# Step #0
# Confirm that the given input, passes and options result in failure.
print('---Starting step #0---')
run_args = [
args.opt_binary, '-disable-symbolication', '-disable-output', passes,
ll_input
]
run_args.extend(extra_opt_args)
opt = subprocess.run(run_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if opt.returncode >= 0:
print('Input does not result in failure as expected. Aborting.')
print(run_args)
print('exitcode: {}'.format(opt.returncode))
print(opt.stderr.decode())
exit(1)

expected_error_returncode = opt.returncode
print('-passes="{}"'.format(pipeline.toStr(lst)))

# Step #1
# Try to narrow down the failing pass sequence by splitting the pipeline in two
# opt invocations (A and B) starting with invocation A only running the first
# pipeline pass and invocation B the remaining. Keep moving the split point
# forward as long as invocation A exits normally and invocation B fails with
# the expected error. This will accomplish two things first the input IR will be
# further reduced and second, with that IR, the reduced pipeline for invocation
# B will be sufficient to reproduce.
print('---Starting step #1---')
prevLstB = None
prevIntermediate = None
tmpd = tempfile.TemporaryDirectory()

for idx in range(pipeline.count(lst)):
[lstA, lstB] = pipeline.split(lst, idx)
if not args.dont_remove_empty_pm:
lstA = pipeline.prune(lstA)
lstB = pipeline.prune(lstB)
passesA = '-passes=' + pipeline.toStr(lstA)
passesB = '-passes=' + pipeline.toStr(lstB)

intermediate = 'intermediate-0.ll' if idx % 2 else 'intermediate-1.ll'
intermediate = tmpd.name + '/' + intermediate
run_args = [
args.opt_binary, '-disable-symbolication', '-S', '-o', intermediate,
passesA, ll_input
]
run_args.extend(extra_opt_args)
optA = subprocess.run(run_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
run_args = [
args.opt_binary, '-disable-symbolication', '-disable-output', passesB,
intermediate
]
run_args.extend(extra_opt_args)
optB = subprocess.run(run_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if not (optA.returncode == 0
and optB.returncode == expected_error_returncode):
break
prevLstB = lstB
prevIntermediate = intermediate
if prevLstB:
lst = prevLstB
ll_input = prevIntermediate
print('-passes="{}"'.format(pipeline.toStr(lst)))

# Step #2
# Try removing passes from the end of the remaining pipeline while still
# reproducing the error.
print('---Starting step #2---')
prevLstA = None
for idx in reversed(range(pipeline.count(lst))):
[lstA, lstB] = pipeline.split(lst, idx)
if not args.dont_remove_empty_pm:
lstA = pipeline.prune(lstA)
passesA = '-passes=' + pipeline.toStr(lstA)
run_args = [
args.opt_binary, '-disable-symbolication', '-disable-output', passesA,
ll_input
]
run_args.extend(extra_opt_args)
optA = subprocess.run(run_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if optA.returncode != expected_error_returncode:
break
prevLstA = lstA
if prevLstA:
lst = prevLstA
print('-passes="{}"'.format(pipeline.toStr(lst)))

# Step #3
# Now that we have a pipeline that is reduced both front and back we do
# exhaustive sweeps over the remainder trying to remove one pass at a time.
# Repeat as long as reduction is possible.
print('---Starting step #3---')
while True:
keepGoing = False
for idx in range(pipeline.count(lst)):
candLst = pipeline.remove(lst, idx)
if not args.dont_remove_empty_pm:
candLst = pipeline.prune(candLst)
passes = '-passes=' + pipeline.toStr(candLst)
run_args = [
args.opt_binary, '-disable-symbolication', '-disable-output',
passes, ll_input
]
run_args.extend(extra_opt_args)
opt = subprocess.run(run_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if opt.returncode == expected_error_returncode:
lst = candLst
keepGoing = True
if not keepGoing:
break
print('-passes="{}"'.format(pipeline.toStr(lst)))

print('---FINISHED---')
if args.output:
shutil.copy(ll_input, args.output)
print('Wrote output to \'{}\'.'.format(args.output))
print('-passes="{}"'.format(pipeline.toStr(lst)))
exit(0)
Loading

0 comments on commit 96ba6f2

Please sign in to comment.