Repository: numba/numba

Issue:
config.OPT is not overridden with override_config('OPT', 0)
Hi, 

I'm trying to migrate Numba to LLVM's new pass manager APIs introduced in [1046](https://github.com/numba/llvmlite/pull/1046) and I think I might have encountered a bug.
Some of the tests (eg test_debuginfo.py) make use of this snippet to override default config flags
```
with override_config('OPT', 0):
    fn.compile(sig)
    return fn.inspect_llvm(sig)
```
For example, when we execute the test `test_debuginfo.py:test_DILocation` `core/codegen.py:_optimize_final_module` makes a call to `_mpm_cheap.run()` and `_mpm_full.run()` with `config.OPT` correctly overriden to 0 (as expected).

However these pass manager objects are already created and exist even before `_optimize_final_module` was called (core/codegen.py:line 1211) with default `config.OPT` levels, meaning `override_config` snippet didn't have any effect.

I might be missing something here and this might not actually be a bug but nonetheless I would like to propose a solution/code-change  to shift creating of these pass manager objects inside `_optimize_final_module` function where they are used. It would also solve the problem of using same pass manager object and calling run() on it multiple times.

Discussion:

Patch:
diff --git a/docs/upcoming_changes/9670.improvement.rst b/docs/upcoming_changes/9670.improvement.rst
new file mode 100644
index 000000000..73ef2adb2
--- /dev/null
+++ b/docs/upcoming_changes/9670.improvement.rst
@@ -0,0 +1,8 @@
+Improvements in how Pass Manager objects are created for optimizations
+----------------------------------------------------------------------
+
+Move creation of ``ModulePassManager`` object to ``_optimize_final_module``
+function, preventing the usage of the same pass manager object for
+compiling multiple Python functions. This would allow for better control
+while optimizing unrelated modules and possibly under different settings
+(degree of vectorization, optimization level, etc.).
diff --git a/numba/core/codegen.py b/numba/core/codegen.py
index fe51c44c5..1fe953fbb 100644
--- a/numba/core/codegen.py
+++ b/numba/core/codegen.py
@@ -668,18 +668,26 @@ class CPUCodeLibrary(CodeLibrary):
         """
         Internal: optimize this library's final module.
         """
+
+        mpm_cheap = self._codegen._module_pass_manager(loop_vectorize=self._codegen._loopvect,
+                                   slp_vectorize=False,
+                                   opt=self._codegen._opt_level,
+                                   cost="cheap")
+
+        mpm_full = self._codegen._module_pass_manager()
+
         cheap_name = "Module passes (cheap optimization for refprune)"
         with self._recorded_timings.record(cheap_name):
             # A cheaper optimisation pass is run first to try and get as many
             # refops into the same function as possible via inlining
-            self._codegen._mpm_cheap.run(self._final_module)
+            mpm_cheap.run(self._final_module)
         # Refop pruning is then run on the heavily inlined function
         if not config.LLVM_REFPRUNE_PASS:
             self._final_module = remove_redundant_nrt_refct(self._final_module)
         full_name = "Module passes (full optimization)"
         with self._recorded_timings.record(full_name):
             # The full optimisation suite is then run on the refop pruned IR
-            self._codegen._mpm_full.run(self._final_module)
+            mpm_full.run(self._final_module)
 
     def _get_module_for_linking(self):
         """
@@ -1197,22 +1205,15 @@ class CPUCodegen(Codegen):
             # guarantee that this will increase runtime performance, it may
             # detriment it, this is here to give the user an easily accessible
             # option to try.
-            loopvect = True
-            opt_level = 3
+            self._loopvect = True
+            self._opt_level = 3
         else:
             # The default behaviour is to do an opt=0 pass to try and inline as
             # much as possible with the cheapest cost of doing so. This is so
             # that the ref-op pruner pass that runs after the cheap pass will
             # have the largest possible scope for working on pruning references.
-            loopvect = False
-            opt_level = 0
-
-        self._mpm_cheap = self._module_pass_manager(loop_vectorize=loopvect,
-                                                    slp_vectorize=False,
-                                                    opt=opt_level,
-                                                    cost="cheap")
-
-        self._mpm_full = self._module_pass_manager()
+            self._loopvect = False
+            self._opt_level = 0
 
         self._engine.set_object_cache(self._library_class._object_compiled_hook,
                                       self._library_class._object_getbuffer_hook)


Test patch:
diff --git a/numba/tests/test_debuginfo.py b/numba/tests/test_debuginfo.py
index d306d5f46..720c7f48c 100644
--- a/numba/tests/test_debuginfo.py
+++ b/numba/tests/test_debuginfo.py
@@ -221,8 +221,9 @@ class TestDebugInfoEmission(TestCase):
         self.assertGreater(len(blocks), 1)
         block = blocks[0]
 
-        # Find non-call instr and check the sequence is as expected
-        instrs = [x for x in block.instructions if x.opcode != 'call']
+        # Find non-call/non-memory instr and check the sequence is as expected
+        instrs = [x for x in block.instructions if x.opcode not in
+                  ['call', 'load', 'store']]
         op_expect = {'fadd', 'fmul', 'fdiv'}
         started = False
         for x in instrs:
diff --git a/numba/tests/test_optimisation_pipelines.py b/numba/tests/test_optimisation_pipelines.py
new file mode 100644
index 000000000..a11e66324
--- /dev/null
+++ b/numba/tests/test_optimisation_pipelines.py
@@ -0,0 +1,58 @@
+import unittest
+
+from numba.tests.support import (TestCase, override_config)
+from numba import njit
+from numba.core import types
+import llvmlite.binding as llvm
+
+
+class TestPassManagerOptimization(TestCase):
+    """ Tests that pass manager is not overriding the intended
+    optimization level.
+    """
+
+    def _get_llvmir(self, fn, sig):
+        with override_config('OPT', 0):
+            fn.compile(sig)
+            return fn.inspect_llvm(sig)
+
+    def test_override_config(self):
+        @njit(debug=True, error_model='numpy')
+        def foo(a):
+            b = a + 1.23
+            c = b * 2.34
+            d = b / c
+            print(d)
+            return d
+
+        sig = (types.float64,)
+        full_ir = self._get_llvmir(foo, sig=sig)
+
+        module = llvm.parse_assembly(full_ir)
+
+        name = foo.overloads[foo.signatures[0]].fndesc.mangled_name
+        funcs = [x for x in module.functions if x.name == name]
+        self.assertEqual(len(funcs), 1)
+        func = funcs[0]
+        blocks = [x for x in func.blocks]
+        self.assertGreater(len(blocks), 1)
+        block = blocks[0]
+
+        # Find sequence with non-debug instructions
+        instrs = [x for x in block.instructions if x.opcode != 'call']
+        op_expect = {'fadd', 'fmul', 'fdiv'}
+        started = False
+        for x in instrs:
+            if x.opcode in op_expect:
+                op_expect.remove(x.opcode)
+                if not started:
+                    started = True
+            elif op_expect and started:
+                break
+
+        self.assertGreater(len(op_expect), 0,
+                           "Function was optimized unexpectedly")
+
+
+if __name__ == '__main__':
+    unittest.main()