comphead commented on PR #6121:
URL: 
https://github.com/apache/datafusion-comet/pull/6121#issuecomment-5783729435

   Checked the redraw against the sources. Separating region from authority is 
the right axis and worth landing. Four things I would change first.
   
   **1. The reservation-ceiling label is false for one pool type and imprecise 
for the other.**
   
   `parse_memory_pool_config` gives `greedy_unified` a `pool_size` of `0` 
(`native/core/src/execution/memory_pools/config.rs:57-62`) and 
`CometUnifiedMemoryPool` never reads a limit, so 
`spark.comet.exec.memoryPool.fraction` has no effect there at all. Under the 
default `fair_unified` the check is `pool_size / num_consumers` against the 
pool's *total* used, not the requester's own (`fair_pool.rs:149-163`), so the 
ceiling is a fraction of a fraction.
   
   Neither is usually the binding constraint. 
`ExecutionMemoryPool.acquireMemory` caps a task at `maxPoolSize / 
numActiveTasks` and parks it below `poolSize / (2 * numActiveTasks)`. With 8 
concurrent tasks a Comet plan gets roughly 1/8 of the off-heap pool, not all of 
it.
   
   **2. `COBJ --> ONPOOL` is a new inaccuracy.** The unified memory manager 
accounts only for what consumers explicitly reserve plus cached block sizes. 
Comet's plans, `CometVector`s and iterators are ordinary heap objects in user 
memory, the `1 - spark.memory.fraction` slice. The old undifferentiated `HEAP` 
box was vaguer but less wrong.
   
   **3. The single `OFFPOOL` box hides the execution/storage split.** 
`spark.memory.offHeap.size` is divided by `spark.memory.storageFraction` into 
off-heap execution and off-heap storage pools 
(`UnifiedMemoryManager.scala:64,98-102`). Comet only touches the execution 
half, and reaches the rest only by evicting `OFF_HEAP` cached blocks.
   
   **4. Scope is missing.** The pool, the reservation and the fair share are 
all per task attempt (`task_shared.rs`), but the diagram is drawn per 
container, so a reader cannot see that `spark.executor.cores / spark.task.cpus` 
of these run at once.
   
   Two things that predate this PR but sit next to the diagram:
   
   - `memory_management.md` still says to compare `jemalloc_allocated` against 
"the summed `thread_NNN_comet_memory_reserved` values". Since #5934 there is an 
`alloc-accounting` feature emitting `native_allocated` 
(`native/core/src/alloc_accounting.rs`), and `jni_api.rs:252-255` warns that 
the per-thread counters must *not* be summed, because a shared pool reports its 
full reservation on every thread that references it. 
`comet_memory_reserved_total` is the figure to use. `tracing.md` has this 
right, and the "No signal for real native usage" open problem is stale for the 
same reason.
   - Each `createPlan` builds its own `RuntimeEnv` (`jni_api.rs:618,793`) and 
therefore its own `FileMetadataCache`, capped at DataFusion's 50 MiB default. A 
shuffle runs two plans per task, so eight concurrent tasks can hold up to 
roughly 800 MiB of Parquet metadata that no pool sees. Worth a node.
   
   <details>
   <summary>An alternative that covers the above, if useful</summary>
   
   ```mermaid
   %%{init: {'flowchart': {'wrappingWidth': 400}}}%%
   flowchart LR
     subgraph CG["Executor container, cgroup memory.max = executor.memory + 
executor.memoryOverhead + memory.offHeap.size"]
       direction TB
   
       subgraph NAT["Native heap, allocated by Rust in this process"]
         direction TB
         NRES["Declared reservations<br>ExternalSorter, grouped aggregate, hash 
and SMJ joins, ShuffleRepartitioner<br>seen by the pool and by 
native_allocated"]
         NUND["Undeclared Rust allocations<br>expression kernels, array 
builders, Parquet page and decompression buffers,<br>per-plan FileMetadataCache 
at 50 MiB each, object_store, tokio, FFI batches in flight<br>seen only by 
native_allocated"]
         NOPAQ["Outside Rust's GlobalAlloc<br>libzstd, aws-lc-sys, libhdfs, 
jemalloc retained pages, fragmentation, Arrow padding<br>seen by nothing but 
RSS"]
       end
   
       subgraph OFF["JVM off-heap, allocated by Unsafe and Java Arrow"]
         direction TB
         TUNG["Spark Tungsten pages"]
         JSH["Comet shuffle pages<br>CometUnifiedShuffleMemoryAllocator"]
         ARW["CometArrowAllocator<br>RootAllocator with no limit"]
       end
   
       subgraph HEAP["JVM heap, bounded by spark.executor.memory"]
         direction TB
         UNIF["Spark unified region<br>spark.memory.fraction, default 0.6"]
         USER["User memory, the remaining 0.4<br>Comet plans, CometVectors, 
iterators, Spark internals"]
       end
   
       subgraph REST["Neither heap"]
         direction TB
         NONHEAP["JVM non-heap<br>metaspace, code cache, JVM and tokio thread 
stacks, Netty"]
         PAGEC["Page cache from spill and shuffle files<br>charged to the 
cgroup, reclaimable under pressure"]
       end
     end
   
     GATE1["Comet gate, fair_unified only<br>rejects when pool total plus 
request exceeds memory_limit / num_consumers<br>memory_limit is 
memory.offHeap.size scaled by 
spark.comet.exec.memoryPool.fraction<br>greedy_unified has no such gate"]
     GATE2["Spark gate, one per task 
attempt<br>TaskMemoryManager.acquireExecutionMemory<br>grants at most pool / 
numActiveTasks, blocks below pool / 2 x numActiveTasks"]
   
     OFFEX["Off-heap EXECUTION pool<br>one per executor, shared by every task 
attempt"]
     OFFST["Off-heap STORAGE pool<br>OFF_HEAP cached blocks, 
spark.memory.storageFraction"]
     ONPOOL["On-heap execution and storage pools"]
     GCONLY["Bounded by the heap, budgeted by nobody"]
     NOONE["No bound and no budget<br>spark.executor.memoryOverhead is the only 
slack"]
   
     NRES -->|"try_grow over JNI. One way only: NativeMemoryConsumer.spill 
returns 0"| GATE1
     GATE1 --> GATE2
     TUNG --> GATE2
     JSH --> GATE2
     GATE2 --> OFFEX
     OFFEX <-->|"borrows, evicting cached blocks"| OFFST
     UNIF --> ONPOOL
     USER --> GCONLY
     NUND --> NOONE
     NOPAQ --> NOONE
     ARW --> NOONE
     NONHEAP --> NOONE
     PAGEC --> NOONE
   
     classDef budgeted fill:#c7ecd0,stroke:#2f6b46,color:#000
     classDef gate fill:#fff2b2,stroke:#8a7420,color:#000
     classDef heaponly fill:#cfe4fb,stroke:#2f5680,color:#000
     classDef unbudgeted fill:#f6c69a,stroke:#8a4b1f,color:#000
     class NRES,TUNG,JSH,OFFEX,OFFST,UNIF,ONPOOL budgeted
     class GATE1,GATE2 gate
     class USER,GCONLY heaponly
     class NUND,NOPAQ,ARW,NONHEAP,PAGEC,NOONE unbudgeted
   ```
   
   </details>
   
   Two gate nodes put the fraction and the `1/numActiveTasks` divisor where 
they apply, the off-heap pool becomes two boxes with a borrow edge, the heap 
separates the unified region from user memory, and the native heap is graded by 
which counter can observe it, so the picture doubles as a measurement guide. 
The one-way relationship rides on an edge label rather than needing an absent 
arrow.
   
   If that is too much for one figure, the natural split is to move the gates 
and the pool pair into a separate "what a single reservation passes through" 
diagram under *Where Comet's budget comes from*, and leave this one 
region-to-authority only.
   
   I have not rendered the fence. `dev/ci/check-mermaid.py` is the authority 
and it does not run on my machine either.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to