-
Notifications
You must be signed in to change notification settings - Fork 23
device/gpu: proactive eviction with adaptive percentage threshold #773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
e60a53f
cee37a1
c9f7ba7
dcf8468
a9904e9
1b834d9
ba65e9d
e4985ca
6f8224f
a22c93c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -715,6 +715,7 @@ parsec_device_memory_reserve( parsec_device_gpu_module_t* gpu_device, | |
| #endif | ||
| gpu_device->mem_block_size = eltsize; | ||
| gpu_device->mem_nb_blocks = mem_elem_per_gpu; | ||
| gpu_device->mem_evict_threshold = parsec_gpu_mem_evict_upper; | ||
|
|
||
| return PARSEC_SUCCESS; | ||
| } | ||
|
|
@@ -826,6 +827,95 @@ parsec_device_memory_release( parsec_device_gpu_module_t* gpu_device ) | |
| return PARSEC_SUCCESS; | ||
| } | ||
|
|
||
| /** | ||
| * Try to evict one entry from the clean LRU (gpu_mem_lru) by detaching it from | ||
| * its original data and freeing its zone allocation back to the zone allocator. | ||
| * | ||
| * @param[in] gpu_device the GPU device whose clean LRU is targeted | ||
| * @param[in,out] cycling_sentinel cycle-detector: caller initialises to NULL; | ||
| * updated to the first entry that could not be evicted. When | ||
| * we pop that entry again we know we have looped the entire LRU | ||
| * without finding a free-able entry. | ||
| * | ||
| * @return 1 if a zone block was freed, 0 if the LRU is empty or fully cycling. | ||
| */ | ||
| #if !defined(PARSEC_GPU_ALLOC_PER_TILE) | ||
| static int | ||
| parsec_device_try_evict_lru_one( parsec_device_gpu_module_t *gpu_device, | ||
| parsec_gpu_data_copy_t **cycling_sentinel ) | ||
| { | ||
| parsec_gpu_data_copy_t *lru_gpu_elem; | ||
| parsec_data_t *oldmaster; | ||
|
|
||
| retry: | ||
| lru_gpu_elem = (parsec_gpu_data_copy_t*)parsec_list_pop_front(&gpu_device->gpu_mem_lru); | ||
| if( NULL == lru_gpu_elem ) | ||
| return 0; | ||
| PARSEC_LIST_ITEM_SINGLETON(lru_gpu_elem); | ||
|
|
||
| if( *cycling_sentinel == lru_gpu_elem ) { | ||
| parsec_list_push_front(&gpu_device->gpu_mem_lru, (parsec_list_item_t*)lru_gpu_elem); | ||
| return 0; | ||
| } | ||
|
|
||
| /* Dangling reader: the copy is temporarily untracked in the LRU; skip it */ | ||
| if( 0 != lru_gpu_elem->readers ) | ||
| goto retry; | ||
|
|
||
| /* Outstanding object references: not safe to free yet; push back and note cycle */ | ||
| if( lru_gpu_elem->super.super.obj_reference_count > 1 ) { | ||
| parsec_list_push_back(&gpu_device->gpu_mem_lru, &lru_gpu_elem->super); | ||
| if( NULL == *cycling_sentinel ) *cycling_sentinel = lru_gpu_elem; | ||
| goto retry; | ||
| } | ||
|
|
||
| if( NULL != lru_gpu_elem->original ) { | ||
| oldmaster = lru_gpu_elem->original; | ||
| if( !parsec_atomic_trylock(&oldmaster->lock) ) { | ||
| parsec_list_push_back(&gpu_device->gpu_mem_lru, &lru_gpu_elem->super); | ||
| if( NULL == *cycling_sentinel ) *cycling_sentinel = lru_gpu_elem; | ||
| goto retry; | ||
| } | ||
| /* Guard against a concurrent d2d reader acquiring the copy */ | ||
| if( !parsec_atomic_cas_int32(&lru_gpu_elem->readers, 0, | ||
| -PARSEC_DEVICE_DATA_COPY_ATOMIC_SENTINEL) ) { | ||
| parsec_list_push_back(&gpu_device->gpu_mem_lru, &lru_gpu_elem->super); | ||
| if( NULL == *cycling_sentinel ) *cycling_sentinel = lru_gpu_elem; | ||
| parsec_atomic_unlock(&oldmaster->lock); | ||
| goto retry; | ||
| } | ||
| int do_unlock = oldmaster->super.obj_reference_count != 1; | ||
| parsec_data_copy_detach(oldmaster, lru_gpu_elem, gpu_device->super.device_index); | ||
| parsec_atomic_wmb(); | ||
| if( do_unlock ) | ||
| parsec_atomic_unlock(&oldmaster->lock); | ||
| } | ||
|
|
||
| #if defined(PARSEC_PROF_TRACE) | ||
| if( (gpu_device->trackable_events & PARSEC_PROFILE_GPU_TRACK_MEM_USE) && | ||
| (gpu_device->exec_stream[0]->prof_event_track_enable || | ||
| gpu_device->exec_stream[1]->prof_event_track_enable) ) { | ||
| parsec_profiling_trace_flags(gpu_device->exec_stream[0]->profiling, | ||
| parsec_gpu_free_memory_key, | ||
| (int64_t)lru_gpu_elem->device_private, | ||
| gpu_device->super.device_index, | ||
| NULL, PARSEC_PROFILING_EVENT_COUNTER); | ||
| parsec_profiling_trace_flags(gpu_device->exec_stream[0]->profiling, | ||
| parsec_gpu_use_memory_key_end, | ||
| (uint64_t)lru_gpu_elem->device_private, | ||
| gpu_device->super.device_index, NULL, 0); | ||
| } | ||
| #endif | ||
| assert( 0 != (lru_gpu_elem->flags & PARSEC_DATA_FLAG_PARSEC_OWNED) ); | ||
| zone_free(gpu_device->memory, (void*)lru_gpu_elem->device_private); | ||
| lru_gpu_elem->device_private = NULL; | ||
| gpu_device->super.nb_evictions++; | ||
| PARSEC_OBJ_RELEASE(lru_gpu_elem); | ||
| assert( NULL == lru_gpu_elem ); | ||
| return 1; | ||
| } | ||
| #endif /* !defined(PARSEC_GPU_ALLOC_PER_TILE) */ | ||
|
|
||
| /** | ||
| * Try to find memory space to move all data on the GPU. We attach a device_elem to | ||
| * a memory_elem as soon as a device_elem is available. If we fail to find enough | ||
|
|
@@ -854,6 +944,24 @@ parsec_device_data_reserve_space( parsec_device_gpu_module_t* gpu_device, | |
|
|
||
| (void)copy_readers_update; // potentially unused | ||
|
|
||
| #if !defined(PARSEC_GPU_ALLOC_PER_TILE) | ||
| /* Tier-1 proactive eviction: free clean LRU entries while zone usage exceeds | ||
| * gpu_device->mem_evict_threshold percent of total capacity. The threshold | ||
| * starts at parsec_gpu_mem_evict_upper (default 95%) and is lowered in 5-point | ||
| * steps (floor: parsec_gpu_mem_evict_lower, default 80%) whenever the device stalled | ||
| * because the reactive path also failed to find memory. */ | ||
| { | ||
| size_t total_capacity = (size_t)gpu_device->mem_nb_blocks * gpu_device->mem_block_size; | ||
| parsec_gpu_data_copy_t *cycling = NULL; | ||
| while( zone_in_use(gpu_device->memory) * 100 > | ||
| (size_t)gpu_device->mem_evict_threshold * total_capacity ) { | ||
| if( !parsec_device_try_evict_lru_one(gpu_device, &cycling) ) | ||
| break; | ||
| data_avail_epoch++; | ||
| } | ||
| } | ||
| #endif /* !defined(PARSEC_GPU_ALLOC_PER_TILE) */ | ||
|
|
||
| /** | ||
| * Parse all the input and output flows of data and ensure all have | ||
| * corresponding data on the GPU available. | ||
|
|
@@ -2576,6 +2684,62 @@ parsec_device_kernel_scheduler( parsec_device_module_t *module, | |
| gpu_device->super.device_index, gpu_device->super.name, | ||
| parsec_device_describe_gpu_task(tmp, MAX_TASK_STRLEN, gpu_task)); | ||
| } | ||
|
|
||
| /* Tier-2 proactive dirty-page writeback: when the clean LRU is empty and zone | ||
| * memory pressure exceeds the watermark, queue a D2H transfer on exec_stream[1] | ||
| * now so its latency overlaps with the upcoming H2D stage and kernel execution. | ||
| * This converts a potential blocking wait (dirty page eviction on the critical | ||
| * path) into an overlapped background transfer. | ||
| * | ||
| * In PARSEC_GPU_ALLOC_PER_TILE mode there is no zone allocator; fall back to the | ||
| * simple condition of clean LRU being empty. */ | ||
| if( !parsec_list_nolock_is_empty(&gpu_device->gpu_mem_owned_lru) && | ||
| parsec_list_nolock_is_empty(&gpu_device->gpu_mem_lru) ) { | ||
| #if !defined(PARSEC_GPU_ALLOC_PER_TILE) | ||
|
devreal marked this conversation as resolved.
|
||
| { | ||
| size_t total_capacity = (size_t)gpu_device->mem_nb_blocks * gpu_device->mem_block_size; | ||
| size_t in_use = zone_in_use(gpu_device->memory); | ||
| size_t threshold_bytes = (size_t)gpu_device->mem_evict_threshold * total_capacity / 100; | ||
| if( in_use > threshold_bytes ) { | ||
| /* Compute how many more bytes of dirty-page eviction are needed beyond | ||
| * what is already in-flight on exec_stream[1]. */ | ||
| size_t needed = in_use - threshold_bytes; | ||
| size_t still_needed = (needed > gpu_device->mem_evict_in_flight) ? | ||
| (needed - gpu_device->mem_evict_in_flight) : 0; | ||
| while( still_needed > 0 ) { | ||
| size_t selected = 0; | ||
| parsec_gpu_task_t *_w2r = parsec_gpu_create_w2r_task(gpu_device, es, | ||
| still_needed, &selected); | ||
| if( NULL == _w2r ) break; | ||
| PARSEC_DEBUG_VERBOSE(10, parsec_gpu_output_stream, | ||
| "GPU[%d:%s]: Proactive D2H writeback: clean LRU empty, " | ||
| "zone above %d%% threshold; needed %zu, selected %zu bytes", | ||
| gpu_device->super.device_index, gpu_device->super.name, | ||
| gpu_device->mem_evict_threshold, still_needed, selected); | ||
| PARSEC_PUSH_TASK(gpu_device->exec_stream[1]->fifo_pending, | ||
| (parsec_list_item_t*)_w2r); | ||
| still_needed = (still_needed > selected) ? (still_needed - selected) : 0; | ||
| } | ||
| } | ||
| } | ||
| #else | ||
| { | ||
| /* No zone allocator in ALLOC_PER_TILE mode: issue one D2H batch whenever | ||
| * the clean LRU is empty and the dirty LRU is non-empty. */ | ||
| size_t selected = 0; | ||
| parsec_gpu_task_t *_w2r = parsec_gpu_create_w2r_task(gpu_device, es, | ||
| SIZE_MAX, &selected); | ||
| if( NULL != _w2r ) { | ||
| PARSEC_DEBUG_VERBOSE(10, parsec_gpu_output_stream, | ||
| "GPU[%d:%s]: Proactive D2H writeback: clean LRU empty, selected %zu bytes", | ||
| gpu_device->super.device_index, gpu_device->super.name, selected); | ||
| PARSEC_PUSH_TASK(gpu_device->exec_stream[1]->fifo_pending, | ||
| (parsec_list_item_t*)_w2r); | ||
| } | ||
| } | ||
| #endif /* !defined(PARSEC_GPU_ALLOC_PER_TILE) */ | ||
| } | ||
|
|
||
| rc = parsec_device_progress_stream( gpu_device, | ||
| gpu_device->exec_stream[0], | ||
| parsec_device_kernel_push, | ||
|
|
@@ -2595,8 +2759,19 @@ parsec_device_kernel_scheduler( parsec_device_module_t *module, | |
| assert(NULL == progress_task); | ||
|
|
||
| /* TODO: check this */ | ||
| /* If we can extract data go for it, otherwise try to drain the pending tasks */ | ||
| gpu_task = parsec_gpu_create_w2r_task(gpu_device, es); | ||
| /* If we can extract data go for it, otherwise try to drain the pending tasks. | ||
| * Skip if evictions are already in flight to avoid a storm of D2H tasks. | ||
| * If there are no in-flight evictions and nothing left to queue from the dirty | ||
| * LRU, we are truly stuck: step the eviction threshold down so tier-1 starts | ||
| * freeing pages earlier on the next attempt. */ | ||
| if( 0 == gpu_device->mem_evict_in_flight ) { | ||
| size_t _sel = 0; | ||
| gpu_task = parsec_gpu_create_w2r_task(gpu_device, es, SIZE_MAX, &_sel); | ||
| if(gpu_device->mem_evict_threshold - 5 >= parsec_gpu_mem_evict_lower) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The threshold manipulation happens irrespective if the parsec_gpu_create_w2r_task was able to create some tasks or not. This is very aggressive, because you will lower the threshold to the minimum very quickly, way before there is any real pressure on the memory. Why lowering it so aggressively ? |
||
| /* We had to trigger a proactive D2H writeback so reduce the threshold */ | ||
| gpu_device->mem_evict_threshold -= 5; | ||
| } | ||
| } | ||
| if( NULL != gpu_task ) | ||
| goto get_data_out_of_device; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -180,6 +180,16 @@ static const parsec_symbol_t symb_gpu_d2h_task_param = { | |
|
|
||
| int32_t parsec_gpu_d2h_max_flows = 0; | ||
|
|
||
| /* Proactive eviction thresholds (percentage of total zone capacity). | ||
| * Registered as MCA parameters by each GPU backend component. | ||
| * mem_evict_upper: initial percentage at which proactive eviction begins (default 95). | ||
| * mem_evict_lower: floor to which the per-device threshold may adapt downwards (default 80). | ||
| * When a task stalls because no zone memory could be freed, the per-device | ||
| * mem_evict_threshold is lowered by 5 points (clamped to mem_evict_lower) so | ||
| * future eviction runs start sooner. */ | ||
| int32_t parsec_gpu_mem_evict_upper = 95; | ||
| int32_t parsec_gpu_mem_evict_lower = 80; | ||
|
|
||
| static const parsec_task_class_t parsec_gpu_d2h_task_class = { | ||
| .name = "GPU D2H data transfer", | ||
| .task_class_id = 0, | ||
|
|
@@ -223,16 +233,20 @@ static const parsec_task_class_t parsec_gpu_d2h_task_class = { | |
| */ | ||
| parsec_gpu_task_t* | ||
| parsec_gpu_create_w2r_task(parsec_device_gpu_module_t *gpu_device, | ||
| parsec_execution_stream_t *es) | ||
| parsec_execution_stream_t *es, | ||
| size_t required_size, | ||
| size_t *selected_size) | ||
| { | ||
| parsec_gpu_task_t *w2r_task = NULL; | ||
| parsec_gpu_d2h_task_t *d2h_task = NULL; | ||
| parsec_gpu_data_copy_t *gpu_copy; | ||
| parsec_list_item_t* item = (parsec_list_item_t*)gpu_device->gpu_mem_owned_lru.ghost_element.list_next; | ||
| int nb_cleaned = 0; | ||
| size_t _selected = 0; | ||
|
|
||
| /* Find a data copy that has no pending users on the GPU, and can be | ||
| * safely moved back on the main memory */ | ||
| /* Find data copies with no pending GPU readers that can be safely moved back to | ||
| * main memory. Stop once nb_cleaned reaches the max-flows cap or we have | ||
| * accumulated at least required_size bytes. */ | ||
| while(nb_cleaned < parsec_gpu_d2h_max_flows) { | ||
| /* Break at the end of the list */ | ||
| if( item == &(gpu_device->gpu_mem_owned_lru.ghost_element) ) { | ||
|
|
@@ -247,7 +261,7 @@ parsec_gpu_create_w2r_task(parsec_device_gpu_module_t *gpu_device, | |
| d2h_task = (parsec_gpu_d2h_task_t*)parsec_thread_mempool_allocate(es->context_mempool); | ||
| if( PARSEC_UNLIKELY(NULL == d2h_task) ) { /* we're running out of memory. Bail out. */ | ||
| parsec_atomic_unlock( &gpu_copy->original->lock ); | ||
| return NULL; | ||
| break; | ||
| } | ||
| PARSEC_OBJ_CONSTRUCT(d2h_task, parsec_task_t); | ||
| } | ||
|
|
@@ -260,17 +274,22 @@ parsec_gpu_create_w2r_task(parsec_device_gpu_module_t *gpu_device, | |
| PARSEC_DEBUG_VERBOSE(10, parsec_gpu_output_stream, "D2H[%d:%s] task %p:\tdata %d -> %p [%p] readers %d", | ||
| gpu_device->super.device_index, gpu_device->super.name, (void*)d2h_task, | ||
| nb_cleaned, gpu_copy, gpu_copy->original, gpu_copy->readers); | ||
| _selected += gpu_copy->original->nb_elts; | ||
| nb_cleaned++; | ||
| if (MAX_PARAM_COUNT == nb_cleaned) | ||
| if( MAX_PARAM_COUNT == nb_cleaned || _selected >= required_size ) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. required_size is in bytes, but if I recall correctly nb_elts is in number of datatypes. There is another similar check in the completion path. Please check both. |
||
| break; | ||
| } else { | ||
| parsec_atomic_unlock( &gpu_copy->original->lock ); | ||
| } | ||
| } | ||
|
|
||
| *selected_size = _selected; | ||
|
|
||
| if( 0 == nb_cleaned ) | ||
| return NULL; | ||
|
|
||
| gpu_device->mem_evict_in_flight += _selected; | ||
|
|
||
| d2h_task->priority = INT32_MAX; | ||
| d2h_task->task_class = &parsec_gpu_d2h_task_class; | ||
| d2h_task->taskpool = NULL; | ||
|
|
@@ -310,6 +329,7 @@ int parsec_gpu_complete_w2r_task(parsec_device_gpu_module_t *gpu_device, | |
| gpu_copy->readers--; | ||
| gpu_copy->data_transfer_status = PARSEC_DATA_STATUS_COMPLETE_TRANSFER; | ||
| gpu_device->super.data_out_to_host += gpu_copy->original->nb_elts; /* TODO: not hardcoded, use datatype size */ | ||
| gpu_device->mem_evict_in_flight -= gpu_copy->original->nb_elts; | ||
| assert(gpu_copy->readers >= 0); | ||
|
|
||
| original = gpu_copy->original; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.