rustfs_erasure_codec/core/
parallel.rs1use crate::Field;
2
3use super::{
4 CODE_SLICE_DEFAULT_CHUNK_BYTES, CODE_SLICE_LARGE_CHUNK_BYTES, CODE_SLICE_MIN_CHUNK_BYTES,
5 PARALLEL_MIN_SHARD_BYTES, ReedSolomon,
6};
7
8#[cfg(feature = "std")]
9pub const PARALLEL_POLICY_VERSION: u32 = 2;
10#[cfg(feature = "std")]
11const RS_PARALLEL_POLICY_MIN_PARALLEL_SHARD_BYTES_ENV: &str =
12 "RS_PARALLEL_POLICY_MIN_PARALLEL_SHARD_BYTES";
13#[cfg(feature = "std")]
14const RS_PARALLEL_POLICY_MIN_BYTES_PER_JOB_ENV: &str = "RS_PARALLEL_POLICY_MIN_BYTES_PER_JOB";
15#[cfg(feature = "std")]
16const RS_PARALLEL_POLICY_MAX_JOBS_ENV: &str = "RS_PARALLEL_POLICY_MAX_JOBS";
17#[cfg(feature = "std")]
18const RS_PARALLEL_POLICY_L2_CACHE_BYTES_ENV: &str = "RS_PARALLEL_POLICY_L2_CACHE_BYTES";
19
20#[cfg(feature = "std")]
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct ParallelPolicy {
23 pub min_parallel_shard_bytes: usize,
24 pub min_bytes_per_job: usize,
25 pub max_jobs: usize,
26 pub l2_cache_bytes: usize,
29}
30
31#[cfg(feature = "std")]
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct ParallelDecision {
34 pub use_parallel: bool,
35 pub jobs: usize,
36 pub chunk_len: usize,
37}
38
39#[cfg(feature = "std")]
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub(crate) struct RuntimeParallelPolicyCache {
42 pub available_parallelism: usize,
43 pub data: ParallelPolicy,
44 pub reconstruct_data: ParallelPolicy,
45 pub reconstruct_full_data: ParallelPolicy,
46 pub reconstruct_full_parity: ParallelPolicy,
47}
48
49#[cfg(feature = "std")]
50impl Default for ParallelPolicy {
51 fn default() -> Self {
52 Self {
53 min_parallel_shard_bytes: PARALLEL_MIN_SHARD_BYTES,
54 min_bytes_per_job: CODE_SLICE_LARGE_CHUNK_BYTES,
55 max_jobs: 0,
56 l2_cache_bytes: super::cache_detect::detect_l2_cache_bytes()
57 .unwrap_or(super::cache_detect::DEFAULT_L2_CACHE_BYTES),
58 }
59 }
60}
61
62#[cfg(feature = "std")]
63impl ParallelPolicy {
64 pub fn decide(
66 &self,
67 shard_size: usize,
68 data_shards: usize,
69 output_shards: usize,
70 available_parallelism: usize,
71 ) -> ParallelDecision {
72 if shard_size == 0 || data_shards == 0 || output_shards == 0 {
73 return ParallelDecision {
74 use_parallel: false,
75 jobs: 1,
76 chunk_len: shard_size,
77 };
78 }
79
80 let available_parallelism = available_parallelism.max(1);
81 let max_jobs = if self.max_jobs == 0 {
82 available_parallelism
83 } else {
84 core::cmp::min(self.max_jobs, available_parallelism)
85 }
86 .max(1);
87
88 let min_parallel_shard_bytes = self.min_parallel_shard_bytes.max(1);
89 let min_bytes_per_job = self.min_bytes_per_job.max(CODE_SLICE_MIN_CHUNK_BYTES);
90
91 let mut chunk_count = shard_size.div_ceil(min_bytes_per_job).max(1);
92
93 if self.l2_cache_bytes > 0 {
96 let active_shards = data_shards.saturating_add(output_shards).max(1);
97 let ideal_chunk = self.l2_cache_bytes / active_shards;
98 if ideal_chunk > 0 {
99 let cache_chunk_count = shard_size.div_ceil(ideal_chunk).max(1);
100 chunk_count = chunk_count.max(cache_chunk_count);
101 }
102 }
103 let max_useful_jobs = if output_shards <= 2 {
104 chunk_count
105 } else {
106 output_shards.saturating_mul(chunk_count)
107 }
108 .max(1);
109 let jobs = core::cmp::min(max_jobs, max_useful_jobs).max(1);
110
111 if shard_size < min_parallel_shard_bytes || jobs < 2 {
112 return ParallelDecision {
113 use_parallel: false,
114 jobs: 1,
115 chunk_len: core::cmp::min(Self::serial_chunk_len(shard_size), shard_size),
116 };
117 }
118
119 let chunks_per_output = core::cmp::max(1, jobs.div_ceil(output_shards));
120 let chunk_len = shard_size
121 .div_ceil(chunks_per_output)
122 .clamp(CODE_SLICE_MIN_CHUNK_BYTES, min_bytes_per_job);
123
124 ParallelDecision {
125 use_parallel: true,
126 jobs,
127 chunk_len: core::cmp::min(chunk_len, shard_size),
128 }
129 }
130
131 fn serial_chunk_len(shard_size: usize) -> usize {
132 if shard_size <= CODE_SLICE_MIN_CHUNK_BYTES {
133 shard_size
134 } else if shard_size <= CODE_SLICE_DEFAULT_CHUNK_BYTES {
135 CODE_SLICE_MIN_CHUNK_BYTES
136 } else if shard_size <= 4 * 1024 * 1024 {
137 CODE_SLICE_DEFAULT_CHUNK_BYTES
138 } else {
139 CODE_SLICE_LARGE_CHUNK_BYTES
140 }
141 }
142
143 pub fn with_l2_cache_bytes(mut self, bytes: usize) -> Self {
145 self.l2_cache_bytes = bytes;
146 self
147 }
148
149 pub fn with_env_overrides(self) -> Self {
151 let mut policy = self;
152 if let Some(value) = parse_env_usize(RS_PARALLEL_POLICY_MIN_PARALLEL_SHARD_BYTES_ENV)
153 && value > 0
154 {
155 policy.min_parallel_shard_bytes = value;
156 }
157 if let Some(value) = parse_env_usize(RS_PARALLEL_POLICY_MIN_BYTES_PER_JOB_ENV)
158 && value > 0
159 {
160 policy.min_bytes_per_job = value;
161 }
162 if let Some(value) = parse_env_usize(RS_PARALLEL_POLICY_MAX_JOBS_ENV) {
163 policy.max_jobs = value;
164 }
165 if let Some(value) = parse_env_usize(RS_PARALLEL_POLICY_L2_CACHE_BYTES_ENV) {
166 policy.l2_cache_bytes = value;
167 }
168 policy
169 }
170}
171
172#[cfg(feature = "std")]
173impl RuntimeParallelPolicyCache {
174 pub(crate) fn new(data: ParallelPolicy) -> Self {
175 Self {
176 available_parallelism: detect_available_parallelism(),
177 data,
178 reconstruct_data: data,
179 reconstruct_full_data: data,
180 reconstruct_full_parity: data,
181 }
182 }
183
184 pub(crate) fn reconstruct_policy(&self, data_only: bool) -> ParallelPolicy {
185 if data_only {
186 self.reconstruct_data
187 } else {
188 self.reconstruct_full_data
189 }
190 }
191
192 pub(crate) fn reconstruct_stage_policies(
193 &self,
194 data_only: bool,
195 ) -> (ParallelPolicy, ParallelPolicy) {
196 if data_only {
197 (self.reconstruct_data, self.reconstruct_data)
198 } else {
199 (self.reconstruct_full_data, self.reconstruct_full_parity)
200 }
201 }
202}
203
204#[cfg(feature = "std")]
205fn parse_env_usize(name: &str) -> Option<usize> {
206 std::env::var(name)
207 .ok()
208 .and_then(|value| value.parse::<usize>().ok())
209}
210
211#[cfg(feature = "std")]
212fn detect_available_parallelism() -> usize {
213 std::thread::available_parallelism()
214 .map(|parallelism| parallelism.get())
215 .unwrap_or(1)
216}
217
218impl<F: Field> ReedSolomon<F> {
219 #[cfg(feature = "std")]
221 pub fn parallel_policy(&self, shard_len: usize, output_shards: usize) -> ParallelDecision {
222 let decision = self.parallel_policy_with(
223 shard_len,
224 output_shards,
225 self.policy_cache.available_parallelism,
226 );
227
228 #[cfg(debug_assertions)]
229 if std::env::var_os("RS_PARALLEL_POLICY_DEBUG").is_some() {
230 eprintln!(
231 "rs-parallel-policy v{} shard_len={} outputs={} -> use_parallel={} jobs={} chunk_len={}",
232 PARALLEL_POLICY_VERSION,
233 shard_len,
234 output_shards,
235 decision.use_parallel,
236 decision.jobs,
237 decision.chunk_len
238 );
239 }
240
241 self.runtime_profile_metrics
242 .record_parallel_policy(decision);
243 decision
244 }
245
246 #[cfg(feature = "std")]
247 pub(crate) fn parallel_policy_with(
248 &self,
249 shard_len: usize,
250 output_shards: usize,
251 available_parallelism: usize,
252 ) -> ParallelDecision {
253 self.effective_parallel_policy().decide(
254 shard_len,
255 self.data_shard_count,
256 output_shards,
257 available_parallelism,
258 )
259 }
260
261 #[cfg(feature = "std")]
263 pub fn parallel_policy_version(&self) -> u32 {
264 PARALLEL_POLICY_VERSION
265 }
266
267 #[cfg(feature = "std")]
269 pub fn effective_parallel_policy(&self) -> ParallelPolicy {
270 self.policy_cache.data
271 }
272
273 #[cfg(feature = "std")]
274 pub(crate) fn resolve_policy_cache() -> RuntimeParallelPolicyCache {
275 Self::resolve_policy_cache_with_options(super::CodecOptions::default())
276 }
277
278 #[cfg(feature = "std")]
279 pub(crate) fn resolve_policy_cache_with_options(
280 options: super::CodecOptions,
281 ) -> RuntimeParallelPolicyCache {
282 let mut data = ParallelPolicy::default().with_env_overrides();
283 if options.max_parallel_jobs > 0 {
284 data.max_jobs = options.max_parallel_jobs;
285 }
286 if core::any::type_name::<F>() == core::any::type_name::<crate::galois_8::Field>() {
287 crate::galois_8::resolve_runtime_parallel_policy_cache(data)
288 } else {
289 RuntimeParallelPolicyCache::new(data)
290 }
291 }
292}
293
294#[cfg(all(test, feature = "std"))]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_default_policy_has_l2_cache() {
300 let policy = ParallelPolicy::default();
301 assert!(
302 policy.l2_cache_bytes > 0,
303 "L2 cache should be detected or defaulted"
304 );
305 }
306
307 #[test]
308 fn test_cache_aware_increases_chunk_count() {
309 let policy = ParallelPolicy {
311 l2_cache_bytes: 64 * 1024, ..Default::default()
313 };
314 let decision = policy.decide(1024 * 1024, 10, 4, 8);
315 assert!(decision.jobs > 1, "should parallelize with small cache");
318 }
319
320 #[test]
321 fn test_l2_cache_zero_disables_cache_aware() {
322 let policy = ParallelPolicy {
323 l2_cache_bytes: 0,
324 ..Default::default()
325 };
326 let decision = policy.decide(1024 * 1024, 10, 4, 8);
327 assert!(decision.jobs >= 1);
329 }
330
331 #[test]
332 fn test_with_l2_cache_bytes_builder() {
333 let policy = ParallelPolicy::default().with_l2_cache_bytes(512 * 1024);
334 assert_eq!(policy.l2_cache_bytes, 512 * 1024);
335 }
336
337 #[test]
338 fn test_cache_aware_small_shard_no_effect() {
339 let policy = ParallelPolicy {
341 l2_cache_bytes: 256 * 1024,
342 ..Default::default()
343 };
344 let decision = policy.decide(1024, 10, 4, 8);
345 assert!(!decision.use_parallel);
346 }
347}