| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -65,6 +65,8 @@ | |||
| 65 | 65 | # BQ managed functions (@udf) currently only support Python 3.11. | |
| 66 | 66 | _MANAGED_FUNC_PYTHON_VERSION = "python-3.11" | |
| 67 | 67 | ||
| 68 | + _DEFAULT_FUNCTION_MEMORY_MIB = 1024 | ||
| 69 | + | ||
| 68 | 70 | ||
| 69 | 71 | class FunctionClient: | |
| 70 | 72 | # Wait time (in seconds) for an IAM binding to take effect after creation. | |
@@ -402,8 +404,12 @@ def create_cloud_function( | |||
| 402 | 404 | is_row_processor=False, | |
| 403 | 405 | vpc_connector=None, | |
| 404 | 406 | vpc_connector_egress_settings="private-ranges-only", | |
| 405 | - memory_mib=1024, | ||
| 407 | + memory_mib=None, | ||
| 408 | + cpus=None, | ||
| 406 | 409 | ingress_settings="internal-only", | |
| 410 | + workers=None, | ||
| 411 | + threads=None, | ||
| 412 | + concurrency=None, | ||
| 407 | 413 | ): | |
| 408 | 414 | """Create a cloud function from the given user defined function.""" | |
| 409 | 415 | ||
@@ -486,6 +492,8 @@ def create_cloud_function( | |||
| 486 | 492 | function.service_config = functions_v2.ServiceConfig() | |
| 487 | 493 | if memory_mib is not None: | |
| 488 | 494 | function.service_config.available_memory = f"{memory_mib}Mi" | |
| 495 | + if cpus is not None: | ||
| 496 | + function.service_config.available_cpu = str(cpus) | ||
| 489 | 497 | if timeout_seconds is not None: | |
| 490 | 498 | if timeout_seconds > 1200: | |
| 491 | 499 | raise bf_formatting.create_exception_with_feedback_link( | |
@@ -517,6 +525,20 @@ def create_cloud_function( | |||
| 517 | 525 | function.service_config.service_account_email = ( | |
| 518 | 526 | self._cloud_function_service_account | |
| 519 | 527 | ) | |
| 528 | + if concurrency: | ||
| 529 | + function.service_config.max_instance_request_concurrency = concurrency | ||
| 530 | + | ||
| 531 | + # Functions framework use environment variables to pass config to gunicorn | ||
| 532 | + # See https://github.com/GoogleCloudPlatform/functions-framework-python/issues/241 | ||
| 533 | + # Code: https://github.com/GoogleCloudPlatform/functions-framework-python/blob/v3.10.1/src/functions_framework/_http/gunicorn.py#L37-L43 | ||
| 534 | + env_vars = {} | ||
| 535 | + if workers: | ||
| 536 | + env_vars["WORKERS"] = str(workers) | ||
| 537 | + if threads: | ||
| 538 | + env_vars["THREADS"] = str(threads) | ||
| 539 | + if env_vars: | ||
| 540 | + function.service_config.environment_variables = env_vars | ||
| 541 | + | ||
| 520 | 542 | if ingress_settings not in _INGRESS_SETTINGS_MAP: | |
| 521 | 543 | raise bf_formatting.create_exception_with_feedback_link( | |
| 522 | 544 | ValueError, | |
@@ -581,6 +603,7 @@ def provision_bq_remote_function( | |||
| 581 | 603 | cloud_function_vpc_connector, | |
| 582 | 604 | cloud_function_vpc_connector_egress_settings, | |
| 583 | 605 | cloud_function_memory_mib, | |
| 606 | + cloud_function_cpus, | ||
| 584 | 607 | cloud_function_ingress_settings, | |
| 585 | 608 | bq_metadata, | |
| 586 | 609 | ): | |
@@ -616,6 +639,21 @@ def provision_bq_remote_function( | |||
| 616 | 639 | ) | |
| 617 | 640 | cf_endpoint = self.get_cloud_function_endpoint(cloud_function_name) | |
| 618 | 641 | ||
| 642 | + if cloud_function_memory_mib is None: | ||
| 643 | + cloud_function_memory_mib = _DEFAULT_FUNCTION_MEMORY_MIB | ||
| 644 | + | ||
| 645 | + # assumption is most bigframes functions are cpu bound, single-threaded and many won't release GIL | ||
| 646 | + # therefore, want to allocate a worker for each cpu, and allow a concurrent request per worker | ||
| 647 | + expected_milli_cpus = ( | ||
| 648 | + int(cloud_function_cpus * 1000) | ||
| 649 | + if (cloud_function_cpus is not None) | ||
| 650 | + else _infer_milli_cpus_from_memory(cloud_function_memory_mib) | ||
| 651 | + ) | ||
| 652 | + workers = -(expected_milli_cpus // -1000) # ceil(cpus) without invoking floats | ||
| 653 | + threads = 4 # (per worker) | ||
| 654 | + # max concurrency==1 for vcpus < 1 hard limit from cloud run | ||
| 655 | + concurrency = (workers * threads) if (expected_milli_cpus >= 1000) else 1 | ||
| 656 | + | ||
| 619 | 657 | # Create the cloud function if it does not exist | |
| 620 | 658 | if not cf_endpoint: | |
| 621 | 659 | cf_endpoint = self.create_cloud_function( | |
@@ -630,7 +668,11 @@ def provision_bq_remote_function( | |||
| 630 | 668 | vpc_connector=cloud_function_vpc_connector, | |
| 631 | 669 | vpc_connector_egress_settings=cloud_function_vpc_connector_egress_settings, | |
| 632 | 670 | memory_mib=cloud_function_memory_mib, | |
| 671 | + cpus=cloud_function_cpus, | ||
| 633 | 672 | ingress_settings=cloud_function_ingress_settings, | |
| 673 | + workers=workers, | ||
| 674 | + threads=threads, | ||
| 675 | + concurrency=concurrency, | ||
| 634 | 676 | ) | |
| 635 | 677 | else: | |
| 636 | 678 | logger.info(f"Cloud function {cloud_function_name} already exists.") | |
@@ -696,3 +738,27 @@ def get_remote_function_specs(self, remote_function_name): | |||
| 696 | 738 | # Note: list_routines doesn't make an API request until we iterate on the response object. | |
| 697 | 739 | pass | |
| 698 | 740 | return (http_endpoint, bq_connection) | |
| 741 | + | ||
| 742 | + | ||
| 743 | + def _infer_milli_cpus_from_memory(memory_mib: int) -> int: | ||
| 744 | + # observed values, not formally documented by cloud run functions | ||
| 745 | + if memory_mib < 128: | ||
| 746 | + raise ValueError("Cloud run supports at minimum 128MiB per instance") | ||
| 747 | + elif memory_mib == 128: | ||
| 748 | + return 83 | ||
| 749 | + elif memory_mib <= 256: | ||
| 750 | + return 167 | ||
| 751 | + elif memory_mib <= 512: | ||
| 752 | + return 333 | ||
| 753 | + elif memory_mib <= 1024: | ||
| 754 | + return 583 | ||
| 755 | + elif memory_mib <= 2048: | ||
| 756 | + return 1000 | ||
| 757 | + elif memory_mib <= 8192: | ||
| 758 | + return 2000 | ||
| 759 | + elif memory_mib <= 16384: | ||
| 760 | + return 4000 | ||
| 761 | + elif memory_mib <= 32768: | ||
| 762 | + return 8000 | ||
| 763 | + else: | ||
| 764 | + raise ValueError("Cloud run supports at most 32768MiB per instance") | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -248,7 +248,8 @@ def remote_function( | |||
| 248 | 248 | cloud_function_vpc_connector_egress_settings: Optional[ | |
| 249 | 249 | Literal["all", "private-ranges-only", "unspecified"] | |
| 250 | 250 | ] = None, | |
| 251 | - cloud_function_memory_mib: Optional[int] = 1024, | ||
| 251 | + cloud_function_memory_mib: Optional[int] = None, | ||
| 252 | + cloud_function_cpus: Optional[float] = None, | ||
| 252 | 253 | cloud_function_ingress_settings: Literal[ | |
| 253 | 254 | "all", "internal-only", "internal-and-gclb" | |
| 254 | 255 | ] = "internal-only", | |
@@ -444,6 +445,10 @@ def remote_function( | |||
| 444 | 445 | default memory of cloud functions be allocated, pass `None`. See | |
| 445 | 446 | for more details | |
| 446 | 447 | https://cloud.google.com/functions/docs/configuring/memory. | |
| 448 | + cloud_function_cpus (float, Optional): | ||
| 449 | + The number of cpus to allocate for the cloud | ||
| 450 | + function (2nd gen) created. | ||
| 451 | + https://docs.cloud.google.com/run/docs/configuring/services/cpu. | ||
| 447 | 452 | cloud_function_ingress_settings (str, Optional): | |
| 448 | 453 | Ingress settings controls dictating what traffic can reach the | |
| 449 | 454 | function. Options are: `all`, `internal-only`, or `internal-and-gclb`. | |
@@ -638,6 +643,7 @@ def wrapper(func): | |||
| 638 | 643 | cloud_function_vpc_connector=cloud_function_vpc_connector, | |
| 639 | 644 | cloud_function_vpc_connector_egress_settings=cloud_function_vpc_connector_egress_settings, | |
| 640 | 645 | cloud_function_memory_mib=cloud_function_memory_mib, | |
| 646 | + cloud_function_cpus=cloud_function_cpus, | ||
| 641 | 647 | cloud_function_ingress_settings=cloud_function_ingress_settings, | |
| 642 | 648 | bq_metadata=bqrf_metadata, | |
| 643 | 649 | ) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -88,7 +88,8 @@ def remote_function( | |||
| 88 | 88 | cloud_function_vpc_connector_egress_settings: Optional[ | |
| 89 | 89 | Literal["all", "private-ranges-only", "unspecified"] | |
| 90 | 90 | ] = None, | |
| 91 | - cloud_function_memory_mib: Optional[int] = 1024, | ||
| 91 | + cloud_function_memory_mib: Optional[int] = None, | ||
| 92 | + cloud_function_cpus: Optional[float] = None, | ||
| 92 | 93 | cloud_function_ingress_settings: Literal[ | |
| 93 | 94 | "all", "internal-only", "internal-and-gclb" | |
| 94 | 95 | ] = "internal-only", | |
@@ -112,6 +113,7 @@ def remote_function( | |||
| 112 | 113 | cloud_function_vpc_connector=cloud_function_vpc_connector, | |
| 113 | 114 | cloud_function_vpc_connector_egress_settings=cloud_function_vpc_connector_egress_settings, | |
| 114 | 115 | cloud_function_memory_mib=cloud_function_memory_mib, | |
| 116 | + cloud_function_cpus=cloud_function_cpus, | ||
| 115 | 117 | cloud_function_ingress_settings=cloud_function_ingress_settings, | |
| 116 | 118 | cloud_build_service_account=cloud_build_service_account, | |
| 117 | 119 | ) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1536,7 +1536,8 @@ def remote_function( | |||
| 1536 | 1536 | cloud_function_vpc_connector_egress_settings: Optional[ | |
| 1537 | 1537 | Literal["all", "private-ranges-only", "unspecified"] | |
| 1538 | 1538 | ] = None, | |
| 1539 | - cloud_function_memory_mib: Optional[int] = 1024, | ||
| 1539 | + cloud_function_memory_mib: Optional[int] = None, | ||
| 1540 | + cloud_function_cpus: Optional[float] = None, | ||
| 1540 | 1541 | cloud_function_ingress_settings: Literal[ | |
| 1541 | 1542 | "all", "internal-only", "internal-and-gclb" | |
| 1542 | 1543 | ] = "internal-only", | |
@@ -1717,6 +1718,10 @@ def remote_function( | |||
| 1717 | 1718 | default memory of cloud functions be allocated, pass `None`. See | |
| 1718 | 1719 | for more details | |
| 1719 | 1720 | https://cloud.google.com/functions/docs/configuring/memory. | |
| 1721 | + cloud_function_cpus (float, Optional): | ||
| 1722 | + The number of cpus to allocate for the cloud | ||
| 1723 | + function (2nd gen) created. | ||
| 1724 | + https://docs.cloud.google.com/run/docs/configuring/services/cpu. | ||
| 1720 | 1725 | cloud_function_ingress_settings (str, Optional): | |
| 1721 | 1726 | Ingress settings controls dictating what traffic can reach the | |
| 1722 | 1727 | function. Options are: `all`, `internal-only`, or `internal-and-gclb`. | |
@@ -1767,6 +1772,7 @@ def remote_function( | |||
| 1767 | 1772 | cloud_function_vpc_connector=cloud_function_vpc_connector, | |
| 1768 | 1773 | cloud_function_vpc_connector_egress_settings=cloud_function_vpc_connector_egress_settings, | |
| 1769 | 1774 | cloud_function_memory_mib=cloud_function_memory_mib, | |
| 1775 | + cloud_function_cpus=cloud_function_cpus, | ||
| 1770 | 1776 | cloud_function_ingress_settings=cloud_function_ingress_settings, | |
| 1771 | 1777 | cloud_build_service_account=cloud_build_service_account, | |
| 1772 | 1778 | ) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2089,19 +2089,40 @@ def foo_list(x: pandas.Series, y0: float, y1, y2) -> list[str]: | |||
| 2089 | 2089 | ||
| 2090 | 2090 | ||
| 2091 | 2091 | @pytest.mark.parametrize( | |
| 2092 | - ("memory_mib_args", "expected_memory"), | ||
| 2092 | + ( | ||
| 2093 | + "memory_mib_args", | ||
| 2094 | + "expected_memory", | ||
| 2095 | + "expected_cpus", | ||
| 2096 | + ), | ||
| 2093 | 2097 | [ | |
| 2094 | - pytest.param({}, "1024Mi", id="no-set"), | ||
| 2095 | - pytest.param({"cloud_function_memory_mib": None}, "256M", id="set-None"), | ||
| 2096 | - pytest.param({"cloud_function_memory_mib": 128}, "128Mi", id="set-128"), | ||
| 2097 | - pytest.param({"cloud_function_memory_mib": 1024}, "1024Mi", id="set-1024"), | ||
| 2098 | - pytest.param({"cloud_function_memory_mib": 4096}, "4096Mi", id="set-4096"), | ||
| 2099 | - pytest.param({"cloud_function_memory_mib": 32768}, "32768Mi", id="set-32768"), | ||
| 2098 | + pytest.param({}, "1024Mi", None, id="no-set"), | ||
| 2099 | + pytest.param( | ||
| 2100 | + {"cloud_function_memory_mib": None}, "1024Mi", None, id="set-None" | ||
| 2101 | + ), | ||
| 2102 | + pytest.param({"cloud_function_memory_mib": 128}, "128Mi", None, id="set-128"), | ||
| 2103 | + pytest.param( | ||
| 2104 | + {"cloud_function_memory_mib": 512, "cloud_function_cpus": 0.6}, | ||
| 2105 | + "512Mi", | ||
| 2106 | + "0.6", | ||
| 2107 | + id="set-512", | ||
| 2108 | + ), | ||
| 2109 | + pytest.param( | ||
| 2110 | + {"cloud_function_memory_mib": 1024}, "1024Mi", None, id="set-1024" | ||
| 2111 | + ), | ||
| 2112 | + pytest.param( | ||
| 2113 | + {"cloud_function_memory_mib": 4096, "cloud_function_cpus": 4}, | ||
| 2114 | + "4096Mi", | ||
| 2115 | + "4", | ||
| 2116 | + id="set-4096", | ||
| 2117 | + ), | ||
| 2118 | + pytest.param( | ||
| 2119 | + {"cloud_function_memory_mib": 32768}, "32768Mi", None, id="set-32768" | ||
| 2120 | + ), | ||
| 2100 | 2121 | ], | |
| 2101 | 2122 | ) | |
| 2102 | 2123 | @pytest.mark.flaky(retries=2, delay=120) | |
| 2103 | 2124 | def test_remote_function_gcf_memory( | |
| 2104 | - session, scalars_dfs, memory_mib_args, expected_memory | ||
| 2125 | + session, scalars_dfs, memory_mib_args, expected_memory, expected_cpus | ||
| 2105 | 2126 | ): | |
| 2106 | 2127 | try: | |
| 2107 | 2128 | ||
@@ -2117,6 +2138,12 @@ def square(x: int) -> int: | |||
| 2117 | 2138 | name=square_remote.bigframes_cloud_function | |
| 2118 | 2139 | ) | |
| 2119 | 2140 | assert gcf.service_config.available_memory == expected_memory | |
| 2141 | + if expected_cpus is not None: | ||
| 2142 | + assert gcf.service_config.available_cpu == expected_cpus | ||
| 2143 | + if float(gcf.service_config.available_cpu) >= 1.0: | ||
| 2144 | + assert gcf.service_config.max_instance_request_concurrency >= float( | ||
| 2145 | + gcf.service_config.available_cpu | ||
| 2146 | + ) | ||
| 2120 | 2147 | ||
| 2121 | 2148 | scalars_df, scalars_pandas_df = scalars_dfs | |
| 2122 | 2149 | ||
@@ -2138,12 +2165,8 @@ def square(x: int) -> int: | |||
| 2138 | 2165 | pytest.param(32769, id="set-32769-too-high"), | |
| 2139 | 2166 | ], | |
| 2140 | 2167 | ) | |
| 2141 | - @pytest.mark.flaky(retries=2, delay=120) | ||
| 2142 | 2168 | def test_remote_function_gcf_memory_unsupported(session, memory_mib): | |
| 2143 | - with pytest.raises( | ||
| 2144 | - google.api_core.exceptions.InvalidArgument, | ||
| 2145 | - match="Invalid value specified for container memory", | ||
| 2146 | - ): | ||
| 2169 | + with pytest.raises(ValueError, match="Cloud run supports"): | ||
| 2147 | 2170 | ||
| 2148 | 2171 | @session.remote_function( | |
| 2149 | 2172 | reuse=False, | |
| Back | FazBrowse Home | New Git URL |
0 commit comments