    def _gen_makefile_for_profilers(self, file_pairs, profiler_dir):
        makefile_template = jinja2.Template(
            """
all: {{targets}}
.PHONY: all clean

{{commands}}

clean:
\trm -f {{targets}}
"""
        )

        # normalize the profiler dir: add / at the end
        profiler_dir = os.path.join(profiler_dir, "")

        # deduplicate targets from different ops
        target_to_sources = {}
        for source, target in file_pairs:
            if target not in target_to_sources:
                target_to_sources[target] = set()
            if isinstance(source, str):
                target_to_sources[target].add(source)
            else:
                target_to_sources[target].update(source)

        # stabilize the order of sources per target
        target_to_sources = {
            target: sorted(sources) for target, sources in target_to_sources.items()
        }

        if self._combine_profiler_multi_sources():
            num_sources_before = sum(len(s) for s in target_to_sources.values())
            target_to_sources = self._combine_profiler_sources(
                target_to_sources=target_to_sources,
                num_builders=self._n_jobs,
            )
            num_sources_after = sum(len(s) for s in target_to_sources.values())

            _LOGGER.info(
                f"combined {num_sources_before} profiler sources into {num_sources_after}",
            )

        targets = []
        dependencies = {}
        for target, sources in target_to_sources.items():
            target = target.split(profiler_dir)[-1]
            if len(sources) == 1:
                # single-source profiler executable
                source = next(iter(sources))
                source = source.split(profiler_dir)[-1]
                dependencies[target] = [source]
            else:
                # multi-source profiler executable
                objects = []
                for source in sources:
                    # first compile the objects
                    source = source.split(profiler_dir)[-1]
                    obj = source.replace(".cu", ".obj")
                    if not os.path.exists(os.path.join(profiler_dir, obj)):
                        # compile the object only if it is absent
                        dependencies[obj] = [source]
                    objects.append(obj)
                # then link the objects into an executable
                dependencies[target] = objects
            targets.append(target)

        commands = []
        num_compiled_sources = 0
        target_names = set()
        for target, srcs in dependencies.items():
            # for each "target: srcs" pair,
            # generate two lines for the Makefile
            src_list = " ".join(srcs)
            dep_line = f"{target}: {src_list}"
            cmd_line = (
                Target.current()
                .compile_cmd(executable=(not target.endswith(".obj")))
                .format(target=target, src=src_list)
            )
            if self._do_trace:
                cmd_line = _augment_for_trace(cmd_line)
            else:
                cmd_line = _time_cmd(cmd_line)

            command = f"{dep_line}\n\t{cmd_line}\n"
            commands.append(command)

            # update compilation statistics
            num_compiled_sources += sum(1 for s in srcs if s.endswith(".cu"))
            if not target.endswith(".obj"):
                target_names.add(os.path.split(target)[-1])

        _LOGGER.info(f"compiling {num_compiled_sources} profiler sources")
        _LOGGER.info(f"linking {len(target_names)} profiler executables")

        makefile_str = makefile_template.render(
            targets=" ".join(set(targets)),
            commands="\n".join(commands),
        )

        # make the Makefile name dependent on the built target names
        target_names_str = "_".join(sorted(target_names))  # stable order
        makefile_suffix = sha1(target_names_str.encode("utf-8")).hexdigest()
        makefile_name = f"Makefile_{makefile_suffix}"
        dumpfile = os.path.join(profiler_dir, makefile_name)
        with open(dumpfile, "w+") as f:
            f.write(makefile_str)

        return makefile_name
all: {{targets}}
.PHONY: all clean

{{commands}}

clean:
\trm -f {{targets}}
python/aitemplate/backend/builder.py