Inside the OCaml compiler codebase, profiling and debugging builds are handled by using a different extension suffix: .p.cmx and .d.cmx, for example. In my experience, this way of doing things is rather fragile, because it is easy to get in a situation where we would expect the compiler to go read foo.bar.cmi or foo.bar.cmx but it instead reads foo.cmi or foo.cmx (or doesn't find them and fails or continues silently without cross-module information). In my experience, using "-tag debug" instead is less convenient but more robust, and this is what I recommend.
(This problem could be solved by having a richer language for telling the compiler where to read files from and where to produce output files. I think this would be of independent interest, have other benefits, but it would also be a lot of work.)
Note that if you go the "separate ocamlbuild calls" route, you do not necessarily need to clean between each build, you can use different build directories instead:
ocamlbuild -tag debug -build-dir _build_debug targets.otarget
ocamlbuild -tag "package(bisect-ppx)" -build-dir _build_coverage targets.otarget
...
In particular, this approach gives you incremental rebuilds during development -- the clean-heavy solution does not.
Note that to appease ocamlbuild hygiene checker you then need to explicitly mention that the *other* build directories should be ignored. (It used to be ignored by default in older OCaml versions, but 0.9.2 will complain about them.) You can use a generic rule in your _tags file for that:
<_build*>: -traverse