[ 
https://issues.apache.org/jira/browse/ARROW-14987?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17560618#comment-17560618
 ] 

&res commented on ARROW-14987:
------------------------------

[~westonpace] first of all thanks for the above explanation it is very 
interesting and helpful

I'm having similar issues where the overall memory usage of my program doesn't 
match what I would expect from arrow.

If I compare the size of the arrow.Tables (using nbytes) with the real memory 
usage of my program there are very big discrepancies.

So taking a similar example, where I create a table, split it in many small 
tables and put them back together in one big table:
 
{code:python}
import numpy as np
import pandas as pd
import pyarrow as pa


def recombine(table: pa.Table) -> pa.Table:
    tables = []
    for i in range(table.num_rows):
        tables.append(table[i: i + 1])
    return pa.concat_tables(tables).combine_chunks()


def create_table() -> pa.Table:
    return pa.Table.from_pandas(
        pd.DataFrame(np.random.rand(1000000, 3), columns=['col1', 'col2', 
'col3'])
    )


def play_with_memory():
    for i in range(5):
        print(i)
        recombine(create_table())


if __name__ == "__main__":
    play_with_memory()
{code}
In this example, the table uses 24mb. When I look at the memory_pool stats I 
can see that bytes_allocated is around 24MB and max_memory is at 48MB. So it 
all makes sense.

But after the first pass, the memory usage of my process goes to 1.7gb, and 
this memory never gets released.

 

I suspect that when creating the array of tables, this create a lot of small 
objects. These allocations may get fragmented and increase the memory usage of 
the process.

Whilst I understand this pathologically bad, I can imagine long running 
services may accumulate a lot of these type of small object allocation. Or 
another example would be a batch job which loads fragmented parquet dataset and 
puts them back together.

 

I've written an instrumented version of this code:

 
{code:python}
import gc
import os
import sys

import guppy
import humanize
import numpy as np
import pandas as pd
import psutil
import pyarrow as pa


def recombine(table: pa.Table, position, process, h) -> pa.Table:
    tables = []
    for i in range(table.num_rows):
        tables.append(table[i: i + 1])
    print(
        {
            "position": position,
            "bytes_allocated": 
humanize.naturalsize(pa.default_memory_pool().bytes_allocated()),
            "max_memory": 
humanize.naturalsize(pa.default_memory_pool().max_memory()),
            "process_memory": humanize.naturalsize(process.memory_info().rss),
            "heap": humanize.naturalsize(h.heap().size)
        }
    )
    return pa.concat_tables(tables).combine_chunks()


def create_table() -> pa.Table:
    return pa.Table.from_pandas(
        pd.DataFrame(np.random.rand(1000000, 3), columns=['col1', 'col2', 
'col3'])
    )


def play_with_memory():
    process = psutil.Process(os.getpid())
    h = guppy.hpy()
    print({
        "python": sys.version_info,
        "pyarrow": pa.__version__,
        "memory_pool": pa.default_memory_pool().backend_name,
    })
    print(
        {
            "position": "before",
            "bytes_allocated": 
humanize.naturalsize(pa.default_memory_pool().bytes_allocated()),
            "max_memory": 
humanize.naturalsize(pa.default_memory_pool().max_memory()),
            "process_memory": humanize.naturalsize(process.memory_info().rss),
            "heap": humanize.naturalsize(h.heap().size)
        }
    )
    for i in range(5):
        recombine(create_table(), f"during_{i}", process, h)
        gc.collect()
        print(
            {
                "position": f"after_{i}",
                "bytes_allocated": 
humanize.naturalsize(pa.default_memory_pool().bytes_allocated()),
                "max_memory": 
humanize.naturalsize(pa.default_memory_pool().max_memory()),
                "process_memory": 
humanize.naturalsize(process.memory_info().rss),
                "heap": humanize.naturalsize(h.heap().size)
            }
        )
    print(
        {
            "position": "after",
            "bytes_allocated": 
humanize.naturalsize(pa.default_memory_pool().bytes_allocated()),
            "max_memory": 
humanize.naturalsize(pa.default_memory_pool().max_memory()),
            "process_memory": humanize.naturalsize(process.memory_info().rss),
            "heap": humanize.naturalsize(h.heap().size)
        }
    )


if __name__ == "__main__":
    play_with_memory()

{code}
When running it (using docker) I see these results:
{code:python}
{'python': sys.version_info(major=3, minor=8, micro=13, releaselevel='final', 
serial=0), 'pyarrow': '8.0.0', 'memory_pool': 'jemalloc'}
{'position': 'before', 'bytes_allocated': '0 Bytes', 'max_memory': '0 Bytes', 
'process_memory': '178.3 MB', 'heap': '31.0 MB'}
{'position': 'during_0', 'bytes_allocated': '24.0 MB', 'max_memory': '24.1 MB', 
'process_memory': '1.7 GB', 'heap': '144.1 MB'}
{'position': 'after_0', 'bytes_allocated': '0 Bytes', 'max_memory': '48.0 MB', 
'process_memory': '1.9 GB', 'heap': '31.4 MB'}
{'position': 'during_1', 'bytes_allocated': '24.0 MB', 'max_memory': '48.0 MB', 
'process_memory': '2.0 GB', 'heap': '144.1 MB'}
{'position': 'after_1', 'bytes_allocated': '0 Bytes', 'max_memory': '48.0 MB', 
'process_memory': '1.9 GB', 'heap': '31.4 MB'}
{'position': 'during_2', 'bytes_allocated': '24.0 MB', 'max_memory': '48.0 MB', 
'process_memory': '2.0 GB', 'heap': '144.1 MB'}
{'position': 'after_2', 'bytes_allocated': '0 Bytes', 'max_memory': '48.0 MB', 
'process_memory': '1.9 GB', 'heap': '31.4 MB'}
{'position': 'during_3', 'bytes_allocated': '24.0 MB', 'max_memory': '48.0 MB', 
'process_memory': '2.0 GB', 'heap': '144.1 MB'}
{'position': 'after_3', 'bytes_allocated': '0 Bytes', 'max_memory': '48.0 MB', 
'process_memory': '1.9 GB', 'heap': '31.4 MB'}
{'position': 'during_4', 'bytes_allocated': '24.0 MB', 'max_memory': '48.0 MB', 
'process_memory': '2.0 GB', 'heap': '144.1 MB'}
{'position': 'after_4', 'bytes_allocated': '0 Bytes', 'max_memory': '48.0 MB', 
'process_memory': '1.9 GB', 'heap': '31.4 MB'}
{'position': 'after', 'bytes_allocated': '0 Bytes', 'max_memory': '48.0 MB', 
'process_memory': '1.9 GB', 'heap': '31.4 MB'}

{code}
 

As you can see, the heap and the max_memory are within the bond of what one 
would expect, the the process memory isn't.

Here's the docker file I used to build:
{code:java}
FROM python:3.8-slim-bullseye

RUN --mount=type=cache,target=/root/.cache/pip pip install --upgrade pip
RUN --mount=type=cache,target=/root/.cache/pip pip install pyarrow humanize 
pandas numpy psutil guppy3

ENV PYTHONPATH=/opt/leak
ENV PYTHONUNBUFFERED=1

WORKDIR /opt/leak
ADD memory_leak.py ./

ENTRYPOINT ["python", "memory_leak.py"]

{code}
 

I'm not really sure how to solve this issue. I would like to be able to do this:
{quote}
"Since my test was already using jemalloc I can configure jemalloc the same way 
Arrow does by enabling the background thread and setting it to purge on a 1 
second interval. Now, running my test, after 5 seconds of inactivity I get the 
following from the global jemalloc"
{quote}
But I'm not sure how to do it in python.

> [C++]Memory leak while reading parquet file
> -------------------------------------------
>
>                 Key: ARROW-14987
>                 URL: https://issues.apache.org/jira/browse/ARROW-14987
>             Project: Apache Arrow
>          Issue Type: Bug
>          Components: C++
>    Affects Versions: 6.0.1
>            Reporter: Qingxiang Chen
>            Priority: Major
>
> When I used parquet to access data, I found that the memory usage was still 
> high after the function ended. I reproduced this problem in the example. code 
> show as below:
>  
> {code:c++}
> #include <arrow/api.h>
> #include <arrow/io/api.h>
> #include <parquet/arrow/reader.h>
> #include <parquet/arrow/writer.h>
> #include <parquet/exception.h>
> #include <unistd.h>
> #include <iostream>
> std::shared_ptr<arrow::Table> generate_table() {
>   arrow::Int64Builder i64builder;
>   for (int i=0;i<320000;i++){
>         i64builder.Append(i);
>   }
>   std::shared_ptr<arrow::Array> i64array;
>   PARQUET_THROW_NOT_OK(i64builder.Finish(&i64array));
>   std::shared_ptr<arrow::Schema> schema = arrow::schema(
>       {arrow::field("int", arrow::int64())});
>   return arrow::Table::Make(schema, {i64array});
> }
> void write_parquet_file(const arrow::Table& table) {
>   std::shared_ptr<arrow::io::FileOutputStream> outfile;
>   PARQUET_ASSIGN_OR_THROW(
>       outfile, 
> arrow::io::FileOutputStream::Open("parquet-arrow-example.parquet"));
>   PARQUET_THROW_NOT_OK(
>       parquet::arrow::WriteTable(table, arrow::default_memory_pool(), 
> outfile, 3));
> }
> void read_whole_file() {
>   std::cout << "Reading parquet-arrow-example.parquet at once" << std::endl;
>   std::shared_ptr<arrow::io::ReadableFile> infile;
>   PARQUET_ASSIGN_OR_THROW(infile,
>                           
> arrow::io::ReadableFile::Open("parquet-arrow-example.parquet",
>                                                         
> arrow::default_memory_pool()));
>   std::unique_ptr<parquet::arrow::FileReader> reader;
>   PARQUET_THROW_NOT_OK(
>       parquet::arrow::OpenFile(infile, arrow::default_memory_pool(), 
> &reader));
>   std::shared_ptr<arrow::Table> table;
>   PARQUET_THROW_NOT_OK(reader->ReadTable(&table));
>   std::cout << "Loaded " << table->num_rows() << " rows in " << 
> table->num_columns()
>             << " columns." << std::endl;
> }
> int main(int argc, char** argv) {
>   std::shared_ptr<arrow::Table> table = generate_table();
>   write_parquet_file(*table);
>   std::cout << "start " <<std::endl;
>   read_whole_file();
>   std::cout << "end " <<std::endl;
>   sleep(100);
> }
> {code}
> After the end, during sleep, the memory usage is still more than 100M and has 
> not dropped. When I increase the data volume by 5 times, the memory usage is 
> about 500M, and it will not drop.
> I want to know whether this part of the data is cached by the memory pool, or 
> whether it is a memory leak problem. If there is no memory leak, how to set 
> memory pool size or release memory?



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to