async def find_failing_ops(
        self,
        inputs: Dict[str, np.array],
        compare_outputs: Callable[[proto.MIL_pb2.Operation, np.array, np.array], bool],
        output_names: Optional[List[str]] = None,
        skip_op: Callable[[proto.MIL_pb2.Operation], bool] = skip_op_by_type,
    ) -> List[proto.MIL_pb2.Operation]:
        """
        Identifies operations that produce different outputs in the reference and target models.

        This method compares the outputs of the reference and target models for specified operations,
        identifying those that fail the comparison criteria.

        Parameters
        ----------
        inputs : Dict[str, np.array]
            Input data for the models.

        compare_outputs : Callable[[proto.MIL_pb2.Operation, np.array, np.array], bool])
            A function to compare outputs of an operation between the two models.

        output_names : Optional[List[str]], optional)
            Names of specific outputs to compare. If None, all model outputs are compared. Defaults to None.

        skip_op: Callable[[proto.MIL_pb2.Operation], bool]
            A function that determines if an operation should be skipped.

        Notes
        -----
        - The method uses a breadth-first search strategy to traverse the operation graph.
        - An operation is considered a failure source if it fails comparison while its direct inputs do not.

        Returns
        -------
        List[proto.MIL_pb2.Operation]
            A list of operations that failed the comparison.

        Examples
        --------
        .. sourcecode:: python

            # Load the reference and target models
            reference_model = coremltools.models.MLModel(
                "model.mlpackage", compute_unit=coremltools.ComputeUnit.CPU_ONLY
            )
            target_model = coremltools.models.MLModel(
                "model.mlpackage", compute_unit=coremltools.ComputeUnit.CPU_AND_GPU
            )
            # Create an instance of MLModelComparator
            comparator = (
                coremltools.models.ml_program.experimental.debugging_utils.MLModelComparator(
                    reference_model, target_model
                )
            )
            # Prepare input data
            input_data = {"input_1": np.random.rand(1, 3, 224, 224).astype(np.float32)}

            # Define a custom comparison function
            def compare_outputs(op, reference_output, target_output):
                return np.allclose(reference_output, target_output, rtol=1e-3, atol=1e-3)

            # Find failing operations
            failing_ops = await comparator.find_failing_ops(
                inputs=input_data, compare_outputs=compare_outputs
            )
        """
        if output_names is None:
            output_names = [
                output_name
                for output_name in self._reference_model_inspector.model.output_description
            ]

        queue = []
        all_deps = {}
        for output_name in output_names:
            op = self._reference_output_name_to_op_map.get(output_name, None)
            if op is not None:
                queue.append(op)
                all_deps.update(
                    _get_unique_dependencies(
                        op=op,
                        output_name_to_op_map=self._reference_output_name_to_op_map,
                    )
                )


        result = []
        statuses = {}

        with tqdm.tqdm(total=len(all_deps), desc="\033[1mAnalyzing operations...\033[0m") as pbar:
            while len(queue) > 0:
                op = queue[0]
                status = statuses.get(id(op), None)
                if status is not None:
                    if _is_failure_source(
                        op=op,
                        statuses=statuses,
                        output_name_to_op_map=self._reference_output_name_to_op_map,
                    ):
                        """
                        The operation did not pass comparison check, but its dependencies did.
                        This suggests that the failure likely comes from this operation.
                        """
                        if op not in result:
                            result.append(op)

                    queue.pop(0)
                else:
                    status = await self._process_op(
                        op=op,
                        inputs=inputs,
                        queue=queue,
                        compare_outputs=compare_outputs,
                        skip_op=_skip_op_excluding_output_names(
                            skip_op=skip_op, output_names=output_names
                        ),
                    )
                    output_name = next((output.name for output in op.outputs), "")
                    pbar.set_description(
                        desc=f"\033[1mAnalyzed operation:\033[0m {output_name}, \033[1mtype:\033[0m {op.type}"
                    )
                    pbar.update(n=1)
                statuses[id(op)] = status

        self._visited = set()
        self._reference_outputs = {}
        self._target_outputs = {}

        return result

        Identifies operations that produce different outputs in the reference and target models.

        This method compares the outputs of the reference and target models for specified operations,
        identifying those that fail the comparison criteria.

        Parameters
        ----------
        inputs : Dict[str, np.array]
            Input data for the models.

        compare_outputs : Callable[[proto.MIL_pb2.Operation, np.array, np.array], bool])
            A function to compare outputs of an operation between the two models.

        output_names : Optional[List[str]], optional)
            Names of specific outputs to compare. If None, all model outputs are compared. Defaults to None.

        skip_op: Callable[[proto.MIL_pb2.Operation], bool]
            A function that determines if an operation should be skipped.

        Notes
        -----
        - The method uses a breadth-first search strategy to traverse the operation graph.
        - An operation is considered a failure source if it fails comparison while its direct inputs do not.

        Returns
        -------
        List[proto.MIL_pb2.Operation]
            A list of operations that failed the comparison.

        Examples
        --------
        .. sourcecode:: python

            # Load the reference and target models
            reference_model = coremltools.models.MLModel(
                "model.mlpackage", compute_unit=coremltools.ComputeUnit.CPU_ONLY
            )
            target_model = coremltools.models.MLModel(
                "model.mlpackage", compute_unit=coremltools.ComputeUnit.CPU_AND_GPU
            )
            # Create an instance of MLModelComparator
            comparator = (
                coremltools.models.ml_program.experimental.debugging_utils.MLModelComparator(
                    reference_model, target_model
                )
            )
            # Prepare input data
            input_data = {"input_1": np.random.rand(1, 3, 224, 224).astype(np.float32)}

            # Define a custom comparison function
            def compare_outputs(op, reference_output, target_output):
                return np.allclose(reference_output, target_output, rtol=1e-3, atol=1e-3)

            # Find failing operations
            failing_ops = await comparator.find_failing_ops(
                inputs=input_data, compare_outputs=compare_outputs
            )
        
coremltools/models/ml_program/experimental/debugging_utils.py