diff --git a/rpcs3/Emu/Cell/PPUThread.cpp b/rpcs3/Emu/Cell/PPUThread.cpp
index 319226c864..d8e7ce326e 100644
--- a/rpcs3/Emu/Cell/PPUThread.cpp
+++ b/rpcs3/Emu/Cell/PPUThread.cpp
@@ -1445,12 +1445,6 @@ extern void ppu_initialize(const ppu_module& info)
 	// Compiler mutex (global)
 	static shared_mutex jmutex;
 
-	// Get jit core allocator instance
-	const auto jcores = g_fxo->get<jit_core_allocator>();
-
-	// Worker threads
-	std::vector<std::thread> jthreads;
-
 	// Global variables to initialize
 	std::vector<std::pair<std::string, u64>> globals;
 
@@ -1460,6 +1454,15 @@ extern void ppu_initialize(const ppu_module& info)
 	// Difference between function name and current location
 	const u32 reloc = info.name.empty() ? 0 : info.segs.at(0).addr;
 
+	// Info sent to threads
+	std::vector<std::pair<std::string, ppu_module>> workload;
+
+	// Info to load to main JIT instance (true - compiled)
+	std::vector<std::pair<std::string, bool>> link_workload;
+
+	// Sync variable to acquire workloads
+	atomic_t<u32> work_cv = 0;
+
 	while (jit_mod.vars.empty() && fpos < info.funcs.size())
 	{
 		// Initialize compiler instance
@@ -1609,34 +1612,56 @@ extern void ppu_initialize(const ppu_module& info)
 			globals.emplace_back(fmt::format("__seg%u_%x", i, suffix), info.segs[i].addr);
 		}
 
+		link_workload.emplace_back(obj_name, false);
+
 		// Check object file
 		if (fs::is_file(cache_path + obj_name + ".gz") || fs::is_file(cache_path + obj_name))
 		{
 			if (!jit)
 			{
-				ppu_log.success("LLVM: Already exists: %s", obj_name);
+				ppu_log.success("LLVM: Module exists: %s", obj_name);
 				continue;
 			}
 
-			std::lock_guard lock(jmutex);
-			jit->add(cache_path + obj_name);
-
-			ppu_log.success("LLVM: Loaded module %s", obj_name);
 			continue;
 		}
 
+		// Adjust information (is_compiled)
+		link_workload.back().second = true;
+
+		// Fill workload list for compilation
+		workload.emplace_back(std::move(obj_name), std::move(part));
+
 		// Update progress dialog
 		g_progr_ptotal++;
+	}
 
-		// Create worker thread for compilation
-		jthreads.emplace_back([&jit, obj_name = obj_name, part = std::move(part), &cache_path, jcores]()
+	// Create worker threads for compilation (TODO: how many threads)
+	{
+		u32 thread_count = Emu.GetMaxThreads();
+
+		if (workload.size() < thread_count)
+		{
+			thread_count = ::size32(workload);
+		}
+
+		struct thread_index_allocator
+		{
+			atomic_t<u64> index = 0;
+		};
+
+		named_thread_group threads(fmt::format("PPUW.%u.", ++g_fxo->get<thread_index_allocator>()->index), thread_count, [&]()
 		{
 			// Set low priority
 			thread_ctrl::set_native_priority(-1);
 
-			// Allocate "core"
+			for (u32 i = work_cv++; i < workload.size(); i = work_cv++)
 			{
-				std::lock_guard jlock(jcores->sem);
+				// Keep allocating workload
+				const auto [obj_name, part] = std::as_const(workload)[i];
+
+				// Allocate "core"
+				std::lock_guard jlock(g_fxo->get<jit_core_allocator>()->sem);
 
 				if (!Emu.IsStopped())
 				{
@@ -1645,28 +1670,37 @@ extern void ppu_initialize(const ppu_module& info)
 					// Use another JIT instance
 					jit_compiler jit2({}, g_cfg.core.llvm_cpu, 0x1);
 					ppu_initialize2(jit2, part, cache_path, obj_name);
+
+					ppu_log.success("LLVM: Compiled module %s", obj_name);
 				}
 
 				g_progr_pdone++;
 			}
+		});
 
-			if (Emu.IsStopped() || !jit || !fs::is_file(cache_path + obj_name + ".gz"))
+		threads.join();
+
+		if (Emu.IsStopped() || !get_current_cpu_thread())
+		{
+			return;
+		}
+
+		std::lock_guard lock(jmutex);
+
+		for (auto [obj_name, is_compiled] : link_workload)
+		{
+			if (Emu.IsStopped())
 			{
-				return;
+				break;
 			}
 
-			// Proceed with original JIT instance
-			std::lock_guard lock(jmutex);
 			jit->add(cache_path + obj_name);
 
-			ppu_log.success("LLVM: Compiled module %s", obj_name);
-		});
-	}
-
-	// Join worker threads
-	for (auto& thread : jthreads)
-	{
-		thread.join();
+			if (!is_compiled)
+			{
+				ppu_log.success("LLVM: Loaded module %s", obj_name);
+			}
+		}
 	}
 
 	if (Emu.IsStopped() || !get_current_cpu_thread())