View Javadoc
1   package net.bmahe.genetics4j.gpu.spec.fitness;
2   
3   import java.util.HashMap;
4   import java.util.HashSet;
5   import java.util.List;
6   import java.util.Map;
7   import java.util.Map.Entry;
8   import java.util.Objects;
9   import java.util.Set;
10  import java.util.concurrent.CompletableFuture;
11  import java.util.concurrent.ConcurrentHashMap;
12  import java.util.concurrent.ExecutorService;
13  
14  import org.apache.commons.collections4.MapUtils;
15  import org.apache.logging.log4j.LogManager;
16  import org.apache.logging.log4j.Logger;
17  import org.jocl.CL;
18  import org.jocl.Pointer;
19  import org.jocl.Sizeof;
20  
21  import net.bmahe.genetics4j.core.Genotype;
22  import net.bmahe.genetics4j.gpu.opencl.OpenCLExecutionContext;
23  import net.bmahe.genetics4j.gpu.opencl.model.Device;
24  import net.bmahe.genetics4j.gpu.spec.fitness.cldata.CLData;
25  import net.bmahe.genetics4j.gpu.spec.fitness.kernelcontext.KernelExecutionContext;
26  import net.bmahe.genetics4j.gpu.spec.fitness.multistage.MultiStageDescriptor;
27  import net.bmahe.genetics4j.gpu.spec.fitness.multistage.StageDescriptor;
28  
29  /**
30   * GPU-accelerated fitness evaluator that executes multiple sequential OpenCL kernels for complex fitness computation.
31   * 
32   * <p>MultiStageFitness provides a framework for implementing fitness evaluation that requires multiple sequential GPU
33   * kernel executions, where each stage can use results from previous stages as input. This is ideal for complex fitness
34   * functions that require multiple computational phases, such as neural network training, multi-objective optimization,
35   * or hierarchical problem decomposition.
36   * 
37   * <p>Key features:
38   * <ul>
39   * <li><strong>Sequential execution</strong>: Multiple OpenCL kernels executed in sequence</li>
40   * <li><strong>Inter-stage data flow</strong>: Results from earlier stages used as inputs to later stages</li>
41   * <li><strong>Memory optimization</strong>: Automatic cleanup and reuse of intermediate results</li>
42   * <li><strong>Pipeline processing</strong>: Support for complex computational pipelines</li>
43   * <li><strong>Stage configuration</strong>: Individual configuration for each computational stage</li>
44   * </ul>
45   * 
46   * <p>Multi-stage computation architecture:
47   * <ul>
48   * <li><strong>Stage descriptors</strong>: Each stage defines its kernel, data loaders, and result allocators</li>
49   * <li><strong>Data reuse patterns</strong>: Previous stage results can be reused as arguments or size parameters</li>
50   * <li><strong>Memory lifecycle</strong>: Automatic management of intermediate results between stages</li>
51   * <li><strong>Static data sharing</strong>: Algorithm parameters shared across all stages</li>
52   * </ul>
53   * 
54   * <p>Typical usage pattern:
55   * 
56   * <pre>{@code
57   * // Define multi-stage descriptor with sequential kernels
58   * MultiStageDescriptor descriptor = MultiStageDescriptor.builder()
59   * 		.addStaticDataLoader("parameters", parametersLoader)
60   * 		.addStage(
61   * 				StageDescriptor.builder()
62   * 						.kernelName("preprocessing")
63   * 						.addDataLoader(0, inputDataLoader)
64   * 						.addResultAllocator(1, preprocessedResultAllocator)
65   * 						.build())
66   * 		.addStage(
67   * 				StageDescriptor.builder()
68   * 						.kernelName("fitness_evaluation")
69   * 						.reusePreviousResultAsArgument(1, 0) // Use previous result as input
70   * 						.addResultAllocator(1, fitnessResultAllocator)
71   * 						.build())
72   * 		.build();
73   * 
74   * // Define fitness extraction from final stage results
75   * FitnessExtractor<Double> extractor = (context, kernelCtx, executor, generation, genotypes, results) -> {
76   * 	float[] fitnessValues = results.extractFloatArray(context, 1);
77   * 	return Arrays.stream(fitnessValues).mapToDouble(f -> (double) f).boxed().collect(Collectors.toList());
78   * };
79   * 
80   * // Create multi-stage fitness evaluator
81   * MultiStageFitness<Double> fitness = MultiStageFitness.of(descriptor, extractor);
82   * }</pre>
83   * 
84   * <p>Stage execution workflow:
85   * <ol>
86   * <li><strong>Initialization</strong>: Load shared static data once before all evaluations</li>
87   * <li><strong>Stage iteration</strong>: For each stage in sequence:</li>
88   * <li><strong>Context computation</strong>: Calculate kernel execution parameters for the stage</li>
89   * <li><strong>Data preparation</strong>: Load stage-specific data and map previous results</li>
90   * <li><strong>Kernel execution</strong>: Execute the stage kernel with configured parameters</li>
91   * <li><strong>Result management</strong>: Store results for potential use in subsequent stages</li>
92   * <li><strong>Final extraction</strong>: Extract fitness values from the last stage results</li>
93   * <li><strong>Cleanup</strong>: Release all intermediate and final result memory</li>
94   * </ol>
95   * 
96   * <p>Inter-stage data flow patterns:
97   * <ul>
98   * <li><strong>Result reuse</strong>: Use previous stage output buffers as input to subsequent stages</li>
99   * <li><strong>Size propagation</strong>: Use previous stage result sizes as parameters for memory allocation</li>
100  * <li><strong>Memory optimization</strong>: Automatic cleanup of intermediate results no longer needed</li>
101  * <li><strong>Data type preservation</strong>: Maintain OpenCL data types across stage boundaries</li>
102  * </ul>
103  * 
104  * <p>Memory management strategy:
105  * <ul>
106  * <li><strong>Static data persistence</strong>: Shared parameters allocated once across all stages</li>
107  * <li><strong>Intermediate cleanup</strong>: Automatic release of stage results when no longer needed</li>
108  * <li><strong>Result chaining</strong>: Efficient memory reuse between consecutive stages</li>
109  * <li><strong>Final cleanup</strong>: Complete memory cleanup after fitness extraction</li>
110  * </ul>
111  * 
112  * <p>Performance optimization features:
113  * <ul>
114  * <li><strong>Pipeline efficiency</strong>: Minimized memory transfers between stages</li>
115  * <li><strong>Memory coalescing</strong>: Optimized data layouts for GPU memory access</li>
116  * <li><strong>Stage-specific tuning</strong>: Individual work group optimization per stage</li>
117  * <li><strong>Asynchronous execution</strong>: Non-blocking fitness computation</li>
118  * </ul>
119  * 
120  * @param <T> the fitness value type, must be Comparable for optimization algorithms
121  * @see OpenCLFitness
122  * @see MultiStageDescriptor
123  * @see StageDescriptor
124  * @see FitnessExtractor
125  */
126 public class MultiStageFitness<T extends Comparable<T>> extends OpenCLFitness<T> {
127 	public static final Logger logger = LogManager.getLogger(MultiStageFitness.class);
128 
129 	private final MultiStageDescriptor multiStageDescriptor;
130 	private final FitnessExtractor<T> fitnessExtractor;
131 
132 	private final Map<Device, Map<String, CLData>> staticData = new ConcurrentHashMap<>();
133 
134 	protected void clearStaticData(final Device device) {
135 		if (MapUtils.isEmpty(staticData) || MapUtils.isEmpty(staticData.get(device))) {
136 			return;
137 		}
138 
139 		final Map<String, CLData> mapData = staticData.get(device);
140 		for (final CLData clData : mapData.values()) {
141 			CL.clReleaseMemObject(clData.clMem());
142 		}
143 
144 		mapData.clear();
145 		staticData.remove(device);
146 	}
147 
148 	protected void clearData(final Map<Integer, CLData> data) {
149 		if (MapUtils.isEmpty(data)) {
150 			return;
151 		}
152 
153 		for (final CLData clData : data.values()) {
154 			CL.clReleaseMemObject(clData.clMem());
155 		}
156 
157 		data.clear();
158 	}
159 
160 	protected void clearResultData(final Map<Integer, CLData> resultData) {
161 		if (MapUtils.isEmpty(resultData)) {
162 			return;
163 		}
164 
165 		for (final CLData clData : resultData.values()) {
166 			CL.clReleaseMemObject(clData.clMem());
167 		}
168 
169 		resultData.clear();
170 	}
171 
172 	protected void prepareStaticData(final OpenCLExecutionContext openCLExecutionContext,
173 			final StageDescriptor stageDescriptor) {
174 		Objects.requireNonNull(openCLExecutionContext);
175 		Objects.requireNonNull(stageDescriptor);
176 
177 		final var device = openCLExecutionContext.device();
178 
179 		logger.trace("[{}] Preparing static data", device.name());
180 
181 		final var kernels = openCLExecutionContext.kernels();
182 		final var kernelName = stageDescriptor.kernelName();
183 
184 		final var kernel = kernels.get(kernelName);
185 
186 		final var mapStaticDataAsArgument = stageDescriptor.mapStaticDataAsArgument();
187 		for (final var entry : mapStaticDataAsArgument.entrySet()) {
188 			final var argumentName = entry.getKey();
189 			final var argumentIndex = entry.getValue();
190 
191 			final var staticDataMap = staticData.get(device);
192 
193 			if (staticDataMap.containsKey(argumentName) == false) {
194 				throw new IllegalArgumentException("Unknown static argument " + argumentName);
195 			}
196 
197 			final CLData clStaticData = staticDataMap.get(argumentName);
198 
199 			logger.trace("[{}] Index {} - Loading static data with name {}", device.name(), argumentIndex, argumentName);
200 
201 			CL.clSetKernelArg(kernel, argumentIndex, Sizeof.cl_mem, Pointer.to(clStaticData.clMem()));
202 		}
203 	}
204 
205 	private void allocateLocalMemory(OpenCLExecutionContext openCLExecutionContext, StageDescriptor stageDescriptor,
206 			long generation, List<Genotype> genotypes, final KernelExecutionContext kernelExecutionContext) {
207 		Objects.requireNonNull(openCLExecutionContext);
208 		Objects.requireNonNull(stageDescriptor);
209 		Objects.requireNonNull(kernelExecutionContext);
210 
211 		final var device = openCLExecutionContext.device();
212 
213 		logger.trace("[{}] Allocating local memory", device.name());
214 
215 		final var kernels = openCLExecutionContext.kernels();
216 		final var kernelName = stageDescriptor.kernelName();
217 
218 		final var kernel = kernels.get(kernelName);
219 
220 		final var localMemoryAllocators = stageDescriptor.localMemoryAllocators();
221 		if (MapUtils.isNotEmpty(localMemoryAllocators)) {
222 			for (final var entry : localMemoryAllocators.entrySet()) {
223 				final int argumentIdx = entry.getKey();
224 				final var localMemoryAllocator = entry.getValue();
225 
226 				final var size = localMemoryAllocator
227 						.load(openCLExecutionContext, kernelExecutionContext, generation, genotypes);
228 				logger.trace("[{}] Index {} - Setting local data with size of {}", device.name(), argumentIdx, size);
229 
230 				CL.clSetKernelArg(kernel, argumentIdx, size, null);
231 			}
232 		}
233 	}
234 
235 	protected void loadData(final OpenCLExecutionContext openCLExecutionContext, final StageDescriptor stageDescriptor,
236 			final Map<Integer, CLData> data, final long generation, final List<Genotype> genotypes) {
237 		Objects.requireNonNull(openCLExecutionContext);
238 		Objects.requireNonNull(stageDescriptor);
239 		Objects.requireNonNull(data);
240 
241 		final var device = openCLExecutionContext.device();
242 
243 		logger.trace("[{}] Loading data", device.name());
244 
245 		final var kernels = openCLExecutionContext.kernels();
246 		final var kernelName = stageDescriptor.kernelName();
247 
248 		final var kernel = kernels.get(kernelName);
249 
250 		final var dataLoaders = stageDescriptor.dataLoaders();
251 		if (MapUtils.isNotEmpty(dataLoaders)) {
252 			for (final var entry : dataLoaders.entrySet()) {
253 				final int argumentIdx = entry.getKey();
254 				final var dataLoader = entry.getValue();
255 
256 				final var clDdata = dataLoader.load(openCLExecutionContext, generation, genotypes);
257 
258 				if (data.put(argumentIdx, clDdata) != null) {
259 					throw new IllegalArgumentException("Multiple data configured for index " + argumentIdx);
260 				}
261 				logger.trace("[{}] Index {} - Loading data of size {}", device.name(), argumentIdx, clDdata.size());
262 
263 				CL.clSetKernelArg(kernel, argumentIdx, Sizeof.cl_mem, Pointer.to(clDdata.clMem()));
264 			}
265 		}
266 	}
267 
268 	@Override
269 	public void beforeAllEvaluations(final OpenCLExecutionContext openCLExecutionContext,
270 			final ExecutorService executorService) {
271 		super.beforeAllEvaluations(openCLExecutionContext, executorService);
272 
273 		final var device = openCLExecutionContext.device();
274 
275 		logger.trace("[{}] Loading static data", device.name());
276 		clearStaticData(device);
277 
278 		final var staticDataLoaders = multiStageDescriptor.staticDataLoaders();
279 		for (final var entry : staticDataLoaders.entrySet()) {
280 			final String argumentName = entry.getKey();
281 			final var dataSupplier = entry.getValue();
282 
283 			if (logger.isTraceEnabled()) {
284 				final var deviceName = openCLExecutionContext.device().name();
285 				logger.trace("[{}] Loading static data for entry name {}", deviceName, argumentName);
286 			}
287 			final CLData clData = dataSupplier.load(openCLExecutionContext);
288 
289 			final var mapData = staticData.computeIfAbsent(device, k -> new HashMap<>());
290 			if (mapData.put(argumentName, clData) != null) {
291 				throw new IllegalArgumentException("Multiple data configured with name " + argumentName);
292 			}
293 		}
294 	}
295 
296 	/**
297 	 * Constructs a MultiStageFitness with the specified stage descriptor and fitness extractor.
298 	 * 
299 	 * @param _multiStageDescriptor configuration for multi-stage kernel execution and data management
300 	 * @param _fitnessExtractor     function to extract fitness values from final stage results
301 	 * @throws IllegalArgumentException if any parameter is null
302 	 */
303 	public MultiStageFitness(final MultiStageDescriptor _multiStageDescriptor,
304 			final FitnessExtractor<T> _fitnessExtractor) {
305 		Objects.requireNonNull(_multiStageDescriptor);
306 		Objects.requireNonNull(_fitnessExtractor);
307 
308 		this.multiStageDescriptor = _multiStageDescriptor;
309 		this.fitnessExtractor = _fitnessExtractor;
310 	}
311 
312 	@Override
313 	public CompletableFuture<List<T>> compute(final OpenCLExecutionContext openCLExecutionContext,
314 			final ExecutorService executorService, final long generation, final List<Genotype> genotypes) {
315 		Objects.requireNonNull(openCLExecutionContext);
316 
317 		return CompletableFuture.supplyAsync(() -> {
318 
319 			List<T> finalResults = null;
320 
321 			final var device = openCLExecutionContext.device();
322 
323 			final Map<Integer, CLData> data = new ConcurrentHashMap<>();
324 			Map<Integer, CLData> resultData = new ConcurrentHashMap<>();
325 
326 			final var stageDescriptors = multiStageDescriptor.stageDescriptors();
327 			for (int i = 0; i < stageDescriptors.size(); i++) {
328 				final StageDescriptor stageDescriptor = stageDescriptors.get(i);
329 				final var kernels = openCLExecutionContext.kernels();
330 				final var kernelName = stageDescriptor.kernelName();
331 
332 				final var kernel = kernels.get(kernelName);
333 
334 				logger.debug("[{}] Executing {}-th stage for kernel {}", device.name(), i, kernelName);
335 
336 				/**
337 				 * Compute the Kernel Execution Context
338 				 */
339 				final var kernelExecutionContextComputer = stageDescriptor.kernelExecutionContextComputer();
340 				final var kernelExecutionContext = kernelExecutionContextComputer
341 						.compute(openCLExecutionContext, kernelName, generation, genotypes);
342 
343 				/**
344 				 * Map previous results to new arguments
345 				 */
346 				final Map<Integer, CLData> oldResultData = new HashMap<>(resultData);
347 				resultData = new ConcurrentHashMap<>();
348 				final Map<Integer, Integer> reusePreviousResultSizeAsArguments = stageDescriptor
349 						.reusePreviousResultSizeAsArguments();
350 				final Map<Integer, Integer> reusePreviousResultAsArguments = stageDescriptor
351 						.reusePreviousResultAsArguments();
352 				final Set<CLData> reusedArguments = new HashSet<>();
353 				if (MapUtils.isNotEmpty(reusePreviousResultAsArguments)
354 						|| MapUtils.isNotEmpty(reusePreviousResultSizeAsArguments)) {
355 
356 					if (MapUtils.isNotEmpty(reusePreviousResultAsArguments)) {
357 						for (final Entry<Integer, Integer> entry : reusePreviousResultAsArguments.entrySet()) {
358 							final var oldKeyArgument = entry.getKey();
359 							final var newKeyArgument = entry.getValue();
360 
361 							final var previousResultData = oldResultData.get(oldKeyArgument);
362 							if (previousResultData == null) {
363 								logger.error(
364 										"[{}] Could not find previous argument with index {}. Known previous arguments: {}",
365 										device.name(),
366 										oldKeyArgument,
367 										oldResultData);
368 
369 								throw new IllegalArgumentException(
370 										"Could not find previous argument with index " + oldKeyArgument);
371 							}
372 
373 							logger.trace(
374 									"[{}] Index {} - Reuse previous result that had index {}",
375 									device.name(),
376 									newKeyArgument,
377 									oldKeyArgument);
378 							CL.clSetKernelArg(kernel, newKeyArgument, Sizeof.cl_mem, Pointer.to(previousResultData.clMem()));
379 							reusedArguments.add(previousResultData);
380 						}
381 					}
382 
383 					if (MapUtils.isNotEmpty(reusePreviousResultSizeAsArguments)) {
384 						for (final Entry<Integer, Integer> entry : reusePreviousResultSizeAsArguments.entrySet()) {
385 							final var oldKeyArgument = entry.getKey();
386 							final var newKeyArgument = entry.getValue();
387 
388 							final var previousResultData = oldResultData.get(oldKeyArgument);
389 							if (previousResultData == null) {
390 								logger.error(
391 										"[{}] Could not find previous argument with index {}. Known previous arguments: {}",
392 										device.name(),
393 										oldKeyArgument,
394 										oldResultData);
395 
396 								throw new IllegalArgumentException(
397 										"Could not find previous argument with index " + oldKeyArgument);
398 							}
399 
400 							if (logger.isTraceEnabled()) {
401 								logger.trace(
402 										"[{}] Index {} - Setting previous result size of {} of previous argument index {}",
403 										device.name(),
404 										newKeyArgument,
405 										previousResultData.size(),
406 										oldKeyArgument);
407 							}
408 
409 							CL.clSetKernelArg(
410 									kernel,
411 									newKeyArgument,
412 									Sizeof.cl_int,
413 									Pointer.to(new int[] { previousResultData.size() }));
414 						}
415 					}
416 
417 					// Clean up unused results
418 					final var previousResultsToKeep = reusePreviousResultAsArguments.keySet();
419 					for (Entry<Integer, CLData> entry2 : oldResultData.entrySet()) {
420 						if (previousResultsToKeep.contains(entry2.getKey()) == false) {
421 							CL.clReleaseMemObject(entry2.getValue().clMem());
422 						}
423 					}
424 				}
425 
426 				prepareStaticData(openCLExecutionContext, stageDescriptor);
427 				loadData(openCLExecutionContext, stageDescriptor, data, generation, genotypes);
428 				allocateLocalMemory(openCLExecutionContext, stageDescriptor, generation, genotypes, kernelExecutionContext);
429 
430 				/**
431 				 * Allocate memory for results
432 				 */
433 				final var resultAllocators = stageDescriptor.resultAllocators();
434 				if (MapUtils.isNotEmpty(resultAllocators)) {
435 					logger.trace("[{}] Result allocators: {}", device.name(), resultAllocators);
436 
437 					for (final var entry : resultAllocators.entrySet()) {
438 						final int argumentIdx = entry.getKey();
439 						final var resultAllocator = entry.getValue();
440 
441 						final var clDdata = resultAllocator
442 								.load(openCLExecutionContext, kernelExecutionContext, generation, genotypes);
443 
444 						if (resultData.put(argumentIdx, clDdata) != null) {
445 							throw new IllegalArgumentException(
446 									"Multiple result allocators configured for index " + argumentIdx);
447 						}
448 						if (logger.isTraceEnabled()) {
449 							logger.trace(
450 									"[{}] Index {} - Allocate result data memory of type {} and size {}",
451 									device.name(),
452 									argumentIdx,
453 									clDdata.clType(),
454 									clDdata.size());
455 						}
456 
457 						CL.clSetKernelArg(kernel, argumentIdx, Sizeof.cl_mem, Pointer.to(clDdata.clMem()));
458 					}
459 				} else {
460 					logger.trace("[{}] No result allocator found", device.name());
461 				}
462 
463 				final var clCommandQueue = openCLExecutionContext.clCommandQueue();
464 				final var globalWorkDimensions = kernelExecutionContext.globalWorkDimensions();
465 				final var globalWorkSize = kernelExecutionContext.globalWorkSize();
466 				final long[] workGroupSize = kernelExecutionContext.workGroupSize().orElse(null);
467 
468 				logger.trace(
469 						"[{}] Starting computation on kernel {} for {} genotypes and global work size {} and local work size {}",
470 						device.name(),
471 						kernelName,
472 						genotypes.size(),
473 						globalWorkSize,
474 						workGroupSize);
475 				try {
476 					final long startTime = System.nanoTime();
477 					CL.clEnqueueNDRangeKernel(
478 							clCommandQueue,
479 							kernel,
480 							globalWorkDimensions,
481 							null,
482 							globalWorkSize,
483 							workGroupSize,
484 							0,
485 							null,
486 							null);
487 
488 					// CL.clFinish(openCLExecutionContext.clCommandQueue());
489 
490 					final long endTime = System.nanoTime();
491 					final long duration = endTime - startTime;
492 					if (logger.isDebugEnabled()) {
493 						final var deviceName = openCLExecutionContext.device().name();
494 						logger.debug(
495 								"[{}] -  Stage {} - Took {} microsec for {} genotypes",
496 								deviceName,
497 								i,
498 								duration / 1000.,
499 								genotypes.size());
500 					}
501 				} catch (Exception e) {
502 					logger.error("[{}] Failure to compute", device.name(), e);
503 					throw e;
504 				}
505 
506 				if (i == stageDescriptors.size() - 1) {
507 					finalResults = fitnessExtractor.compute(
508 							openCLExecutionContext,
509 							kernelExecutionContext,
510 							executorService,
511 							generation,
512 							genotypes,
513 							new ResultExtractor(Map.of(device, resultData)));
514 
515 					clearResultData(resultData);
516 				}
517 
518 				for (final CLData clData : reusedArguments) {
519 					CL.clReleaseMemObject(clData.clMem());
520 				}
521 				clearData(data);
522 			}
523 
524 			if (finalResults == null) {
525 				throw new IllegalStateException("final results cannot be null");
526 			}
527 			return finalResults;
528 		}, executorService);
529 	}
530 
531 	@Override
532 	public void afterEvaluation(OpenCLExecutionContext openCLExecutionContext, ExecutorService executorService,
533 			long generation, List<Genotype> genotypes) {
534 		super.afterEvaluation(openCLExecutionContext, executorService, generation, genotypes);
535 
536 		final var device = openCLExecutionContext.device();
537 		logger.trace("[{}] Releasing data", device.name());
538 	}
539 
540 	@Override
541 	public void afterAllEvaluations(final OpenCLExecutionContext openCLExecutionContext,
542 			final ExecutorService executorService) {
543 		super.afterAllEvaluations(openCLExecutionContext, executorService);
544 
545 		final var device = openCLExecutionContext.device();
546 		logger.trace("[{}] Releasing static data", device.name());
547 		clearStaticData(device);
548 	}
549 
550 	/**
551 	 * Creates a new MultiStageFitness instance with the specified configuration.
552 	 * 
553 	 * @param <U>                  the fitness value type
554 	 * @param multiStageDescriptor configuration for multi-stage kernel execution and data management
555 	 * @param fitnessExtractor     function to extract fitness values from final stage results
556 	 * @return a new MultiStageFitness instance
557 	 * @throws IllegalArgumentException if any parameter is null
558 	 */
559 	public static <U extends Comparable<U>> MultiStageFitness<U> of(final MultiStageDescriptor multiStageDescriptor,
560 			final FitnessExtractor<U> fitnessExtractor) {
561 		Objects.requireNonNull(multiStageDescriptor);
562 		Objects.requireNonNull(fitnessExtractor);
563 
564 		return new MultiStageFitness<>(multiStageDescriptor, fitnessExtractor);
565 	}
566 }