Skip to content

Commit 96ba6f2

Browse files
markuslavinmemfrob
authored and
memfrob
committed
[NPM] Automatic 'opt' pipeline reducer script.
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
1 parent 3b5187f commit 96ba6f2

File tree

4 files changed

+549
-0
lines changed

4 files changed

+549
-0
lines changed

llvm/utils/pipeline.py

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Automatically formatted with yapf (https://github.com/google/yapf)
2+
"""Utility functions for creating and manipulating LLVM 'opt' NPM pipeline objects."""
3+
4+
5+
def fromStr(pipeStr):
6+
"""Create pipeline object from string representation."""
7+
stack = []
8+
curr = []
9+
tok = ''
10+
kind = ''
11+
for c in pipeStr:
12+
if c == ',':
13+
if tok != '':
14+
curr.append([None, tok])
15+
tok = ''
16+
elif c == '(':
17+
stack.append([kind, curr])
18+
kind = tok
19+
curr = []
20+
tok = ''
21+
elif c == ')':
22+
if tok != '':
23+
curr.append([None, tok])
24+
tok = ''
25+
oldKind = kind
26+
oldCurr = curr
27+
[kind, curr] = stack.pop()
28+
curr.append([oldKind, oldCurr])
29+
else:
30+
tok += c
31+
if tok != '':
32+
curr.append([None, tok])
33+
return curr
34+
35+
36+
def toStr(pipeObj):
37+
"""Create string representation of pipeline object."""
38+
res = ''
39+
lastIdx = len(pipeObj) - 1
40+
for i, c in enumerate(pipeObj):
41+
if c[0]:
42+
res += c[0] + '('
43+
res += toStr(c[1])
44+
res += ')'
45+
else:
46+
res += c[1]
47+
if i != lastIdx:
48+
res += ','
49+
return res
50+
51+
52+
def count(pipeObj):
53+
"""Count number of passes (pass-managers excluded) in pipeline object."""
54+
cnt = 0
55+
for c in pipeObj:
56+
if c[0]:
57+
cnt += count(c[1])
58+
else:
59+
cnt += 1
60+
return cnt
61+
62+
63+
def split(pipeObj, splitIndex):
64+
"""Create two new pipeline objects by splitting pipeObj in two directly after pass with index splitIndex."""
65+
def splitInt(src, splitIndex, dstA, dstB, idx):
66+
for s in src:
67+
if s[0]:
68+
dstA2 = []
69+
dstB2 = []
70+
idx = splitInt(s[1], splitIndex, dstA2, dstB2, idx)
71+
dstA.append([s[0], dstA2])
72+
dstB.append([s[0], dstB2])
73+
else:
74+
if idx <= splitIndex:
75+
dstA.append([None, s[1]])
76+
else:
77+
dstB.append([None, s[1]])
78+
idx += 1
79+
return idx
80+
81+
listA = []
82+
listB = []
83+
splitInt(pipeObj, splitIndex, listA, listB, 0)
84+
return [listA, listB]
85+
86+
87+
def remove(pipeObj, removeIndex):
88+
"""Create new pipeline object by removing pass with index removeIndex from pipeObj."""
89+
def removeInt(src, removeIndex, dst, idx):
90+
for s in src:
91+
if s[0]:
92+
dst2 = []
93+
idx = removeInt(s[1], removeIndex, dst2, idx)
94+
dst.append([s[0], dst2])
95+
else:
96+
if idx != removeIndex:
97+
dst.append([None, s[1]])
98+
idx += 1
99+
return idx
100+
101+
dst = []
102+
removeInt(pipeObj, removeIndex, dst, 0)
103+
return dst
104+
105+
106+
def copy(srcPipeObj):
107+
"""Create copy of pipeline object srcPipeObj."""
108+
def copyInt(dst, src):
109+
for s in src:
110+
if s[0]:
111+
dst2 = []
112+
copyInt(dst2, s[1])
113+
dst.append([s[0], dst2])
114+
else:
115+
dst.append([None, s[1]])
116+
117+
dstPipeObj = []
118+
copyInt(dstPipeObj, srcPipeObj)
119+
return dstPipeObj
120+
121+
122+
def prune(srcPipeObj):
123+
"""Create new pipeline object by removing empty pass-managers (those with count = 0) from srcPipeObj."""
124+
def pruneInt(dst, src):
125+
for s in src:
126+
if s[0]:
127+
if count(s[1]):
128+
dst2 = []
129+
pruneInt(dst2, s[1])
130+
dst.append([s[0], dst2])
131+
else:
132+
dst.append([None, s[1]])
133+
134+
dstPipeObj = []
135+
pruneInt(dstPipeObj, srcPipeObj)
136+
return dstPipeObj
137+
138+
139+
if __name__ == "__main__":
140+
import unittest
141+
142+
class Test(unittest.TestCase):
143+
def test_0(self):
144+
pipeStr = 'a,b,A(c,B(d,e),f),g'
145+
pipeObj = fromStr(pipeStr)
146+
147+
self.assertEqual(7, count(pipeObj))
148+
149+
self.assertEqual(pipeObj, pipeObj)
150+
self.assertEqual(pipeObj, prune(pipeObj))
151+
self.assertEqual(pipeObj, copy(pipeObj))
152+
153+
self.assertEqual(pipeStr, toStr(pipeObj))
154+
self.assertEqual(pipeStr, toStr(prune(pipeObj)))
155+
self.assertEqual(pipeStr, toStr(copy(pipeObj)))
156+
157+
[pipeObjA, pipeObjB] = split(pipeObj, 3)
158+
self.assertEqual('a,b,A(c,B(d))', toStr(pipeObjA))
159+
self.assertEqual('A(B(e),f),g', toStr(pipeObjB))
160+
161+
self.assertEqual('b,A(c,B(d,e),f),g', toStr(remove(pipeObj, 0)))
162+
self.assertEqual('a,b,A(c,B(d,e),f)', toStr(remove(pipeObj, 6)))
163+
164+
pipeObjC = remove(pipeObj, 4)
165+
self.assertEqual('a,b,A(c,B(d),f),g', toStr(pipeObjC))
166+
pipeObjC = remove(pipeObjC, 3)
167+
self.assertEqual('a,b,A(c,B(),f),g', toStr(pipeObjC))
168+
pipeObjC = prune(pipeObjC)
169+
self.assertEqual('a,b,A(c,f),g', toStr(pipeObjC))
170+
171+
unittest.main()
172+
exit(0)

llvm/utils/reduce_pipeline.py

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/usr/bin/env python3
2+
3+
# Automatically formatted with yapf (https://github.com/google/yapf)
4+
5+
# Script for automatic 'opt' pipeline reduction for when using the new
6+
# pass-manager (NPM). Based around the '-print-pipeline-passes' option.
7+
#
8+
# The reduction algorithm consists of several phases (steps).
9+
#
10+
# Step #0: Verify that input fails with the given pipeline and make note of the
11+
# error code.
12+
#
13+
# Step #1: Split pipeline in two starting from front and move forward as long as
14+
# first pipeline exits normally and the second pipeline fails with the expected
15+
# error code. Move on to step #2 with the IR from the split point and the
16+
# pipeline from the second invocation.
17+
#
18+
# Step #2: Remove passes from end of the pipeline as long as the pipeline fails
19+
# with the expected error code.
20+
#
21+
# Step #3: Make several sweeps over the remaining pipeline trying to remove one
22+
# pass at a time. Repeat sweeps until unable to remove any more passes.
23+
#
24+
# Usage example:
25+
# reduce_pipeline.py --opt-binary=./build-all-Debug/bin/opt --input=input.ll --output=output.ll --passes=PIPELINE [EXTRA-OPT-ARGS ...]
26+
27+
import argparse
28+
import pipeline
29+
import shutil
30+
import subprocess
31+
import tempfile
32+
33+
parser = argparse.ArgumentParser(
34+
description=
35+
'Automatic opt pipeline reducer. Unrecognized arguments are forwarded to opt.'
36+
)
37+
parser.add_argument('--opt-binary',
38+
action='store',
39+
dest='opt_binary',
40+
default='opt')
41+
parser.add_argument('--passes', action='store', dest='passes', required=True)
42+
parser.add_argument('--input', action='store', dest='input', required=True)
43+
parser.add_argument('--output', action='store', dest='output')
44+
parser.add_argument('--dont-expand-passes',
45+
action='store_true',
46+
dest='dont_expand_passes',
47+
help='Do not expand pipeline before starting reduction.')
48+
parser.add_argument(
49+
'--dont-remove-empty-pm',
50+
action='store_true',
51+
dest='dont_remove_empty_pm',
52+
help='Do not remove empty pass-managers from the pipeline during reduction.'
53+
)
54+
[args, extra_opt_args] = parser.parse_known_args()
55+
56+
print('The following extra args will be passed to opt: {}'.format(
57+
extra_opt_args))
58+
59+
lst = pipeline.fromStr(args.passes)
60+
passes = '-passes={}'.format(pipeline.toStr(lst))
61+
ll_input = args.input
62+
63+
# Step #-1
64+
# Launch 'opt' once with '-print-pipeline-passes' to expand pipeline before
65+
# starting reduction. Allows specifying a default pipelines (e.g.
66+
# '-passes=default<O3>').
67+
if not args.dont_expand_passes:
68+
run_args = [
69+
args.opt_binary, '-disable-symbolication', '-disable-output',
70+
'-print-pipeline-passes', passes, ll_input
71+
]
72+
run_args.extend(extra_opt_args)
73+
opt = subprocess.run(run_args,
74+
stdout=subprocess.PIPE,
75+
stderr=subprocess.PIPE)
76+
if opt.returncode != 0:
77+
print('Failed to expand passes. Aborting.')
78+
print(run_args)
79+
print('exitcode: {}'.format(opt.returncode))
80+
print(opt.stderr.decode())
81+
exit(1)
82+
stdout = opt.stdout.decode()
83+
stdout = stdout[:stdout.rfind('\n')]
84+
print('Expanded pass sequence: {}'.format(stdout))
85+
passes = '-passes={}'.format(stdout)
86+
87+
# Step #0
88+
# Confirm that the given input, passes and options result in failure.
89+
print('---Starting step #0---')
90+
run_args = [
91+
args.opt_binary, '-disable-symbolication', '-disable-output', passes,
92+
ll_input
93+
]
94+
run_args.extend(extra_opt_args)
95+
opt = subprocess.run(run_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
96+
if opt.returncode >= 0:
97+
print('Input does not result in failure as expected. Aborting.')
98+
print(run_args)
99+
print('exitcode: {}'.format(opt.returncode))
100+
print(opt.stderr.decode())
101+
exit(1)
102+
103+
expected_error_returncode = opt.returncode
104+
print('-passes="{}"'.format(pipeline.toStr(lst)))
105+
106+
# Step #1
107+
# Try to narrow down the failing pass sequence by splitting the pipeline in two
108+
# opt invocations (A and B) starting with invocation A only running the first
109+
# pipeline pass and invocation B the remaining. Keep moving the split point
110+
# forward as long as invocation A exits normally and invocation B fails with
111+
# the expected error. This will accomplish two things first the input IR will be
112+
# further reduced and second, with that IR, the reduced pipeline for invocation
113+
# B will be sufficient to reproduce.
114+
print('---Starting step #1---')
115+
prevLstB = None
116+
prevIntermediate = None
117+
tmpd = tempfile.TemporaryDirectory()
118+
119+
for idx in range(pipeline.count(lst)):
120+
[lstA, lstB] = pipeline.split(lst, idx)
121+
if not args.dont_remove_empty_pm:
122+
lstA = pipeline.prune(lstA)
123+
lstB = pipeline.prune(lstB)
124+
passesA = '-passes=' + pipeline.toStr(lstA)
125+
passesB = '-passes=' + pipeline.toStr(lstB)
126+
127+
intermediate = 'intermediate-0.ll' if idx % 2 else 'intermediate-1.ll'
128+
intermediate = tmpd.name + '/' + intermediate
129+
run_args = [
130+
args.opt_binary, '-disable-symbolication', '-S', '-o', intermediate,
131+
passesA, ll_input
132+
]
133+
run_args.extend(extra_opt_args)
134+
optA = subprocess.run(run_args,
135+
stdout=subprocess.PIPE,
136+
stderr=subprocess.PIPE)
137+
run_args = [
138+
args.opt_binary, '-disable-symbolication', '-disable-output', passesB,
139+
intermediate
140+
]
141+
run_args.extend(extra_opt_args)
142+
optB = subprocess.run(run_args,
143+
stdout=subprocess.PIPE,
144+
stderr=subprocess.PIPE)
145+
if not (optA.returncode == 0
146+
and optB.returncode == expected_error_returncode):
147+
break
148+
prevLstB = lstB
149+
prevIntermediate = intermediate
150+
if prevLstB:
151+
lst = prevLstB
152+
ll_input = prevIntermediate
153+
print('-passes="{}"'.format(pipeline.toStr(lst)))
154+
155+
# Step #2
156+
# Try removing passes from the end of the remaining pipeline while still
157+
# reproducing the error.
158+
print('---Starting step #2---')
159+
prevLstA = None
160+
for idx in reversed(range(pipeline.count(lst))):
161+
[lstA, lstB] = pipeline.split(lst, idx)
162+
if not args.dont_remove_empty_pm:
163+
lstA = pipeline.prune(lstA)
164+
passesA = '-passes=' + pipeline.toStr(lstA)
165+
run_args = [
166+
args.opt_binary, '-disable-symbolication', '-disable-output', passesA,
167+
ll_input
168+
]
169+
run_args.extend(extra_opt_args)
170+
optA = subprocess.run(run_args,
171+
stdout=subprocess.PIPE,
172+
stderr=subprocess.PIPE)
173+
if optA.returncode != expected_error_returncode:
174+
break
175+
prevLstA = lstA
176+
if prevLstA:
177+
lst = prevLstA
178+
print('-passes="{}"'.format(pipeline.toStr(lst)))
179+
180+
# Step #3
181+
# Now that we have a pipeline that is reduced both front and back we do
182+
# exhaustive sweeps over the remainder trying to remove one pass at a time.
183+
# Repeat as long as reduction is possible.
184+
print('---Starting step #3---')
185+
while True:
186+
keepGoing = False
187+
for idx in range(pipeline.count(lst)):
188+
candLst = pipeline.remove(lst, idx)
189+
if not args.dont_remove_empty_pm:
190+
candLst = pipeline.prune(candLst)
191+
passes = '-passes=' + pipeline.toStr(candLst)
192+
run_args = [
193+
args.opt_binary, '-disable-symbolication', '-disable-output',
194+
passes, ll_input
195+
]
196+
run_args.extend(extra_opt_args)
197+
opt = subprocess.run(run_args,
198+
stdout=subprocess.PIPE,
199+
stderr=subprocess.PIPE)
200+
if opt.returncode == expected_error_returncode:
201+
lst = candLst
202+
keepGoing = True
203+
if not keepGoing:
204+
break
205+
print('-passes="{}"'.format(pipeline.toStr(lst)))
206+
207+
print('---FINISHED---')
208+
if args.output:
209+
shutil.copy(ll_input, args.output)
210+
print('Wrote output to \'{}\'.'.format(args.output))
211+
print('-passes="{}"'.format(pipeline.toStr(lst)))
212+
exit(0)

0 commit comments

Comments
 (0)