def _run_scorers(
  args,
  scorers: List[Scorer],
  scoringArgs: ScoringArgs,
  runParallel: bool = True,
  maxWorkers: Optional[int] = None,
  dataLoader: Optional[CommunityNotesDataLoader] = None,
) -> List[ModelResult]:
  """Applies all Community Notes models to user ratings and returns merged result.

  Each model must return a scoredNotes DF and may return helpfulnessScores and auxiliaryNoteInfo.
  scoredNotes and auxiliaryNoteInfo will be forced to contain one row per note to guarantee
  that all notes can be assigned a status during meta scoring regardless of whether any
  individual scoring algorithm scored the note.

  Args:
    scorers (List[Scorer]): Instantiated Scorer objects for note ranking.
    noteTopics: DF pairing notes with topics
    ratings (pd.DataFrame): Complete DF containing all ratings after preprocessing.
    noteStatusHistory (pd.DataFrame): one row per note; history of when note had each status
    userEnrollment (pd.DataFrame): The enrollment state for each contributor

  Returns:
    List[ModelResult]
  """
  # Apply scoring algorithms
  overallStartTime = time.perf_counter()

  if runParallel:
    shms, scoringArgsSharedMemory = _save_dfs_to_shared_memory(scoringArgs)

    with concurrent.futures.ProcessPoolExecutor(
      mp_context=multiprocessing.get_context("forkserver"),
      max_workers=maxWorkers,
    ) as executor:
      logger.info(f"Starting parallel scorer execution with {len(scorers)} scorers.")
      # Pass mostly-empty scoringArgs: the data is too large to be copied in-memory to
      # each process, so must be re-loaded from disk by every scorer's dataLoader.
      scoringArgs.remove_large_args_for_multiprocessing()
      futures = [
        executor.submit(
          _run_scorer_in_parallel,
          args=args,
          scorer=scorer,
          scoringArgs=copy.deepcopy(scoringArgs),
          dataLoader=dataLoader,
          scoringArgsSharedMemory=copy.deepcopy(scoringArgsSharedMemory),
        )
        for scorer in scorers
      ]
      modelResultsAndTimes = [f.result() for f in futures]
      logger.info("Got model results from all scorers.")

      for i, shm in enumerate(shms):
        logger.info(f"Closing shared memory segment {i}/{len(shms)}")
        shm.close()
        logger.info(f"Unlinking shared memory segment {i}/{len(shms)}")
        shm.unlink()
      logger.info("All shared memory segments cleaned up")
  else:
    modelResultsAndTimes = [
      _run_scorer_in_series(
        scorer=scorer,
        scoringArgs=scoringArgs,
      )
      for scorer in scorers
    ]

  modelResultsTuple, scorerTimesTuple = zip(*modelResultsAndTimes)

  overallTime = time.perf_counter() - overallStartTime
  logger.info(
    f"""----
    Completed individual scorers. Ran in parallel: {runParallel}.  Succeeded in {overallTime:.2f} seconds. 
    Individual scorers: (name, runtime): {list(zip(
      [scorer.get_name() for scorer in scorers],
      ['{:.2f}'.format(t/60.0) + " mins" for t in scorerTimesTuple]
    ))}
    ----"""
  )
  return list(modelResultsTuple)
Applies all Community Notes models to user ratings and returns merged result.

  Each model must return a scoredNotes DF and may return helpfulnessScores and auxiliaryNoteInfo.
  scoredNotes and auxiliaryNoteInfo will be forced to contain one row per note to guarantee
  that all notes can be assigned a status during meta scoring regardless of whether any
  individual scoring algorithm scored the note.

  Args:
    scorers (List[Scorer]): Instantiated Scorer objects for note ranking.
    noteTopics: DF pairing notes with topics
    ratings (pd.DataFrame): Complete DF containing all ratings after preprocessing.
    noteStatusHistory (pd.DataFrame): one row per note; history of when note had each status
    userEnrollment (pd.DataFrame): The enrollment state for each contributor

  Returns:
    List[ModelResult]
  
sourcecode/scoring/run_scoring.py