diff --git a/lib/python3.12/site-packages/ray/autoscaler/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8fa62e35a4e45bb40b5cf9500f3ea6ec2a13fc13 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/__init__.py @@ -0,0 +1,8 @@ +import os +from pathlib import Path + +from ray.autoscaler import sdk + +__all__ = ["sdk"] + +AUTOSCALER_DIR_PATH = Path(os.path.abspath(os.path.dirname(__file__))) diff --git a/lib/python3.12/site-packages/ray/autoscaler/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc3ce9d30954645dcb974e957ebe9b56d02a9a35 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/__pycache__/batching_node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/__pycache__/batching_node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24c2ff979ea8dc4101978e3573767851e8f6956e Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/__pycache__/batching_node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/__pycache__/command_runner.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/__pycache__/command_runner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a83311a221707175f2013adb658033ce70322b09 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/__pycache__/command_runner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/__pycache__/launch_and_verify_cluster.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/__pycache__/launch_and_verify_cluster.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ed6c85c315e6341b41e583577c971923405446f Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/__pycache__/launch_and_verify_cluster.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3dfd3a11aea17d1d0c9ed4ab992ff77e8ad9437 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81064dc3b313a9fcb89af82c6543e2e21ede2c79 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/autoscaler.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/autoscaler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abb2fd38be54dea6c165606ce42061f4ede1f694 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/autoscaler.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cli_logger.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cli_logger.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad52a2fe5ca660b114067ecc6ea4547fbda86bb8 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cli_logger.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cli_logger_demoall.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cli_logger_demoall.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad6c6143b16191e08564b3a1018a43472ffa4a8f Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cli_logger_demoall.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cluster_dump.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cluster_dump.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53aa2187cacf2d3ab494a915bd2d32676c8c15ee Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cluster_dump.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/command_runner.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/command_runner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da975b59cc13304a86967b9055331d1777d2712d Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/command_runner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/commands.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/commands.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39bd65ea7e833207481ea81d6ceafd1a6a4ddebc Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/commands.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/constants.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d70e23ceedab0312dfaa304d7bb02f2d8ae0e79e Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/constants.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/docker.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/docker.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b396b9451989724c7bd694b4eb3eba8fbba401af Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/docker.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/event_summarizer.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/event_summarizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63775667e4a03ef2bac67413fe8d2cac06eecac6 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/event_summarizer.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/event_system.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/event_system.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9969dcbf234a9c5ae3715a639880d8455dbcb60 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/event_system.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/legacy_info_string.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/legacy_info_string.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4231846f599184c730cc4acc9a67a25d3b7e437e Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/legacy_info_string.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/load_metrics.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/load_metrics.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d5b953f56a1b97da35ef64024deb91e02d21574 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/load_metrics.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/loader.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/loader.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..954bea5e2a20b5f697ac07b8d3786f1f1dd94e45 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/loader.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/log_timer.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/log_timer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b759d47810f6220b3c67349a8281a7223c0bdc3f Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/log_timer.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/monitor.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/monitor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fc4de8682e17398741c45baa8918c6a4a70fdef Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/monitor.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_launcher.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_launcher.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44f8d87a3e5d63bd5c66b23c8159d314f5261f5e Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_launcher.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_provider_availability_tracker.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_provider_availability_tracker.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0db7d5c281e69fa494e33f4656213b26639b56a Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_provider_availability_tracker.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_tracker.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_tracker.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0d4d0221b7b9b2fe9cf54072d7cd00eb7c09922 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_tracker.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/prom_metrics.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/prom_metrics.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90df409f14b8052f61c92695e24525edcb7cfff8 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/prom_metrics.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/providers.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/providers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd9a17b6c0beff29e475d7a8a78700375a0e1713 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/providers.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/resource_demand_scheduler.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/resource_demand_scheduler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..040db188f6bf0468e97ed5a290ec09459ec00cfd Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/resource_demand_scheduler.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/subprocess_output_util.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/subprocess_output_util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8cb63462a8fe63ae41174e64c3d66f3dedf89ae5 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/subprocess_output_util.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/updater.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/updater.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7e536b3810936717c67a9a381c8d0bafd02d835 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/updater.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/util.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b65d1c6920ea51dde8f0c1409b25580189e3e875 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/util.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aecc0d4014550e5a6323cd2296f643abc5581807 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/config.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..014d826757fcf91ac670f8829fc5fc928f21b198 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5c370b0c54fc017a710a940d675bd006582bb2b Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/azure-config-template.json b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/azure-config-template.json new file mode 100644 index 0000000000000000000000000000000000000000..5b18cbea6fd5305edce0d74a494bf9f4ef4377d7 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/azure-config-template.json @@ -0,0 +1,137 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "clusterId": { + "type": "string", + "metadata": { + "description": "Unique string appended to resource names to isolate resources from different ray clusters." + } + }, + "subnet": { + "type": "string", + "metadata": { + "description": "Subnet parameters." + } + }, + "msiName": { + "type": "string", + "metadata": { + "description": "Managed service identity." + } + }, + "msiResourceGroup": { + "type": "string", + "metadata": { + "description": "Managed service identity resource group." + } + }, + "createMsi": { + "type": "bool", + "defaultValue": "true" + }, + "roleAssignmentGuid": { + "type": "string", + "metadata": { + "description": "Deterministic resource name for the MSI role assignment." + } + } + }, + "variables": { + "contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "location": "[resourceGroup().location]", + "roleAssignmentName": "[concat('ray-', parameters('clusterId'), '-ra')]", + "nsgName": "[concat('ray-', parameters('clusterId'), '-nsg')]", + "nsg": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]", + "vnetName": "[concat('ray-', parameters('clusterId'), '-vnet')]", + "subnetName": "[concat('ray-', parameters('clusterId'), '-subnet')]" + }, + "resources": [ + { + "condition": "[parameters('createMsi')]", + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "location": "[variables('location')]", + "name": "[parameters('msiName')]" + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[parameters('roleAssignmentGuid')]", + "properties": { + "principalId": "[reference(resourceId(parameters('msiResourceGroup'), 'Microsoft.ManagedIdentity/userAssignedIdentities', parameters('msiName')), '2024-11-30').principalId]", + "roleDefinitionId": "[variables('contributor')]", + "scope": "[resourceGroup().id]", + "principalType": "ServicePrincipal", + "description": "[concat('Ray autoscaler cluster ', parameters('clusterId'))]" + }, + "dependsOn": [ + "[parameters('msiName')]" + ] + }, + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2024-10-01", + "name": "[variables('nsgName')]", + "location": "[variables('location')]", + "properties": { + "securityRules": [ + { + "name": "SSH", + "properties": { + "priority": 1000, + "protocol": "Tcp", + "access": "Allow", + "direction": "Inbound", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "22" + } + } + ] + } + }, + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2024-10-01", + "name": "[variables('vnetName')]", + "location": "[variables('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[parameters('subnet')]" + ] + }, + "subnets": [ + { + "name": "[variables('subnetName')]", + "properties": { + "addressPrefix": "[parameters('subnet')]", + "networkSecurityGroup": { + "id": "[variables('nsg')]" + } + } + } + ] + }, + "dependsOn": [ + "[variables('nsg')]" + ] + } + ], + "outputs": { + "subnet": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('subnetName'))]" + }, + "nsg": { + "type": "string", + "value": "[variables('nsg')]" + }, + "msi": { + "type": "string", + "value": "[resourceId(parameters('msiResourceGroup'), 'Microsoft.ManagedIdentity/userAssignedIdentities', parameters('msiName'))]" + } + } +} diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/azure-vm-template.json b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/azure-vm-template.json new file mode 100644 index 0000000000000000000000000000000000000000..7ad98c3dcd66b7a0cd0c6922502ecc6647b6ee82 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/azure-vm-template.json @@ -0,0 +1,318 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "vmName": { + "type": "string", + "metadata": { + "description": "The name of you Virtual Machine." + } + }, + "adminUsername": { + "type": "string", + "metadata": { + "description": "Username for the Virtual Machine." + } + }, + "publicKey": { + "type": "securestring", + "metadata": { + "description": "SSH Key for the Virtual Machine" + } + }, + "imagePublisher": { + "type": "string", + "metadata": { + "description": "The publisher of the VM image" + }, + "defaultValue": "" + }, + "imageOffer": { + "type": "string", + "metadata": { + "description": "The offer of the VM image" + }, + "defaultValue": "" + }, + "imageSku": { + "type": "string", + "metadata": { + "description": "The sku of the VM image" + }, + "defaultValue": "" + }, + "imageVersion": { + "type": "string", + "metadata": { + "description": "The version of the VM image" + }, + "defaultValue": "" + }, + "imageId": { + "type": "string", + "metadata": { + "description": "The resource id of the VM image. If provided, it will override the imagePublisher, imageOffer, imageSku and imageVersion parameters." + }, + "defaultValue": "" + }, + "vmSize": { + "type": "string", + "metadata": { + "description": "The size of the VM" + } + }, + "vmTags": { + "type": "object", + "metadata": { + "description": "Tags for the VM" + } + }, + "vmCount": { + "type": "int", + "metadata": { + "description": "Number of VMs to deploy" + } + }, + "osDiskSize": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Size of the OS disk in GB" + } + }, + "provisionPublicIp": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "If true creates a public ip" + } + }, + "priority": { + "type": "string", + "defaultValue": "Regular", + "metadata": { + "description": "Specifies the priority for the virtual machine." + } + }, + "evictionPolicy": { + "type": "string", + "defaultValue": "Delete", + "metadata": { + "description": "Specifies the eviction policy for the virtual machine." + } + }, + "billingProfile": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Specifies the maximum price to pay for Azure Spot VM." + } + }, + "msi": { + "type": "string", + "metadata": { + "description": "Managed service identity resource id." + } + }, + "nsg": { + "type": "string", + "metadata": { + "description": "Network security group resource id." + } + }, + "subnet": { + "type": "string", + "metadata": { + "descriptions": "Subnet resource id." + } + }, + "enableAcceleratedNetworking": { + "type": "bool", + "defaultValue": false, + "metadata": { + "descriptions": "Whether to enable accelerated networking." + } + }, + "zones": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Availability zones for the virtual machine. If empty, no zones will be specified." + } + } + }, + "variables": { + "location": "[resourceGroup().location]", + "useZones": "[greater(length(parameters('zones')), 0)]", + "networkInterfaceNamePrivate": "[concat(parameters('vmName'), '-nic')]", + "networkInterfaceNamePublic": "[concat(parameters('vmName'), '-nic-public')]", + "networkInterfaceName": "[if(parameters('provisionPublicIp'), variables('networkInterfaceNamePublic'), variables('networkInterfaceNamePrivate'))]", + "networkIpConfig": "[guid(resourceGroup().id, parameters('vmName'))]", + "osDiskType": "Standard_LRS", + "publicIpAddressName": "[concat(parameters('vmName'), '-ip')]" + }, + "resources": [ + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2024-07-01", + "name": "[concat(variables('networkInterfaceNamePublic'), copyIndex())]", + "location": "[variables('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIpAddresses/', concat(variables('publicIpAddressName'), copyIndex()))]" + ], + "copy": { + "name": "NICPublicCopy", + "count": "[parameters('vmCount')]" + }, + "properties": { + "ipConfigurations": [ + { + "name": "[variables('networkIpConfig')]", + "properties": { + "subnet": { + "id": "[parameters('subnet')]" + }, + "privateIPAllocationMethod": "Dynamic", + "publicIpAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('publicIPAddressName'), copyIndex()))]" + } + } + } + ], + "networkSecurityGroup": { + "id": "[parameters('nsg')]" + }, + "enableAcceleratedNetworking": "[parameters('enableAcceleratedNetworking')]" + }, + "condition": "[parameters('provisionPublicIp')]" + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2024-07-01", + "name": "[concat(variables('networkInterfaceNamePrivate'), copyIndex())]", + "location": "[variables('location')]", + "copy": { + "name": "NICPrivateCopy", + "count": "[parameters('vmCount')]" + }, + "properties": { + "ipConfigurations": [ + { + "name": "[variables('networkIpConfig')]", + "properties": { + "subnet": { + "id": "[parameters('subnet')]" + }, + "privateIPAllocationMethod": "Dynamic" + } + } + ], + "networkSecurityGroup": { + "id": "[parameters('nsg')]" + }, + "enableAcceleratedNetworking": "[parameters('enableAcceleratedNetworking')]" + }, + "condition": "[not(parameters('provisionPublicIp'))]" + }, + { + "type": "Microsoft.Network/publicIpAddresses", + "apiVersion": "2024-07-01", + "name": "[concat(variables('publicIpAddressName'), copyIndex())]", + "location": "[variables('location')]", + "properties": { + "publicIpAllocationMethod": "Static", + "publicIPAddressVersion": "IPv4" + }, + "copy": { + "name": "PublicIpCopy", + "count": "[parameters('vmCount')]" + }, + "zones": "[if(variables('useZones'), createArray(string(parameters('zones')[mod(copyIndex(), length(parameters('zones')))])), json('null'))]", + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "condition": "[parameters('provisionPublicIp')]" + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-07-01", + "name": "[concat(parameters('vmName'), copyIndex())]", + "location": "[variables('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces/', concat(variables('networkInterfaceName'), copyIndex()))]" + ], + "copy": { + "name": "VmCopy", + "count": "[parameters('vmCount')]" + }, + "tags": "[parameters('vmTags')]", + "zones": "[if(variables('useZones'), createArray(string(parameters('zones')[mod(copyIndex(), length(parameters('zones')))])), json('null'))]", + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "storageProfile": { + "osDisk": { + "createOption": "fromImage", + "diskSizeGB": "[if(equals(parameters('osDiskSize'), 0), json('null'), parameters('osDiskSize'))]", + "managedDisk": { + "storageAccountType": "[variables('osDiskType')]" + } + }, + "imageReference": "[if(equals(parameters('imageId'), ''), json(concat('{\"publisher\":\"', parameters('imagePublisher'), '\",\"offer\":\"', parameters('imageOffer'), '\",\"sku\":\"', parameters('imageSku'), '\",\"version\":\"', parameters('imageVersion'), '\"}')), json(concat('{\"id\":\"', parameters('imageId'), '\"}')))]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('networkInterfaceName'), copyIndex()))]" + } + ] + }, + "osProfile": { + "computerName": "[concat(parameters('vmName'), copyIndex())]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('publicKey')]", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "[concat('/home/', parameters('adminUsername'), '/.ssh/authorized_keys')]", + "keyData": "[parameters('publicKey')]" + } + ] + } + } + }, + "priority": "[parameters('priority')]", + "evictionPolicy": "[if(equals(parameters('priority'), 'Spot'), parameters('evictionPolicy'), '')]", + "billingProfile": "[parameters('billingProfile')]" + }, + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[parameters('msi')]": { + } + } + } + } + ], + "outputs": { + "publicIp": { + "type": "array", + "copy": { + "count": "[parameters('vmCount')]", + "input": "[reference(concat(variables('publicIpAddressName'), copyIndex())).ipAddress]" + }, + "condition": "[parameters('provisionPublicIp')]" + }, + "privateIp": { + "type": "array", + "copy": { + "count": "[parameters('vmCount')]", + "input": "[reference(concat(variables('networkInterfaceName'), copyIndex())).ipConfigurations[0].properties.privateIPAddress]" + } + } + } +} diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/config.py b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/config.py new file mode 100644 index 0000000000000000000000000000000000000000..faaef44aa2d12b993951bc70334e31008ccb2c95 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/config.py @@ -0,0 +1,580 @@ +import json +import logging +import os +import random +import time +from hashlib import md5, sha256 +from pathlib import Path +from typing import Any, Callable +from uuid import UUID + +from azure.common.credentials import get_cli_profile +from azure.core.exceptions import HttpResponseError, ResourceNotFoundError +from azure.identity import AzureCliCredential +from azure.mgmt.resource import ResourceManagementClient +from azure.mgmt.resource.resources.models import DeploymentMode + +from ray.autoscaler._private.util import ( + generate_rsa_key_pair, + generate_ssh_key_name, + generate_ssh_key_paths, +) + +# Built-in Azure Contributor role definition ID used for role assignments. +CONTRIBUTOR_ROLE_DEFINITION_ID = "b24988ac-6180-42a0-ab88-20f7382dd24c" + +UNIQUE_ID_LEN = 4 + +logger = logging.getLogger(__name__) + + +def get_azure_sdk_function(client: Any, function_name: str) -> Callable: + """Retrieve a callable function from Azure SDK client object. + + Newer versions of the various client SDKs renamed function names to + have a begin_ prefix. This function supports both the old and new + versions of the SDK by first trying the old name and falling back to + the prefixed new name. + """ + func = getattr( + client, function_name, getattr(client, f"begin_{function_name}", None) + ) + if func is None: + raise AttributeError( + "'{obj}' object has no {func} or begin_{func} attribute".format( + obj={client.__name__}, func=function_name + ) + ) + return func + + +def bootstrap_azure(config): + config = _configure_key_pair(config) + config = _configure_resource_group(config) + return config + + +def _configure_resource_group(config): + # TODO: look at availability sets + # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/tutorial-availability-sets + subscription_id = config["provider"].get("subscription_id") + if subscription_id is None: + subscription_id = get_cli_profile().get_subscription_id() + resource_client = ResourceManagementClient(AzureCliCredential(), subscription_id) + config["provider"]["subscription_id"] = subscription_id + logger.info("Using subscription id: %s", subscription_id) + + assert ( + "resource_group" in config["provider"] + ), "Provider config must include resource_group field" + resource_group = config["provider"]["resource_group"] + + assert ( + "location" in config["provider"] + ), "Provider config must include location field" + params = {"location": config["provider"]["location"]} + + if "tags" in config["provider"]: + params["tags"] = config["provider"]["tags"] + + logger.info("Creating/Updating resource group: %s", resource_group) + rg_create_or_update = get_azure_sdk_function( + client=resource_client.resource_groups, function_name="create_or_update" + ) + rg_create_or_update(resource_group_name=resource_group, parameters=params) + + # load the template file + current_path = Path(__file__).parent + template_path = current_path.joinpath("azure-config-template.json") + with open(template_path, "r") as template_fp: + template = json.load(template_fp) + + logger.info("Using cluster name: %s", config["cluster_name"]) + + # set unique id for resources in this cluster + unique_id = config["provider"].get("unique_id") + if unique_id is None: + hasher = sha256() + hasher.update(config["provider"]["resource_group"].encode("utf-8")) + unique_id = hasher.hexdigest()[:UNIQUE_ID_LEN] + else: + unique_id = str(unique_id) + config["provider"]["unique_id"] = unique_id + logger.info("Using unique id: %s", unique_id) + cluster_id = "{}-{}".format(config["cluster_name"], unique_id) + role_assignment_name = f"ray-{cluster_id}-ra" + role_assignment_guid = _generate_arm_guid(role_assignment_name) + role_assignment_resource_id = ( + f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers" + f"/Microsoft.Authorization/roleAssignments/{role_assignment_guid}" + ) + + subnet_mask = config["provider"].get("subnet_mask") + if subnet_mask is None: + # choose a random subnet, skipping most common value of 0 + random.seed(unique_id) + subnet_mask = "10.{}.0.0/16".format(random.randint(1, 254)) + logger.info("Using subnet mask: %s", subnet_mask) + + # Copy over properties from existing subnet. + # Addresses issue (https://github.com/Azure/azure-quickstart-templates/issues/2786) + # where existing subnet properties will get overwritten unless explicitly specified + # during multiple deployments even if vnet/subnet do not change. + # May eventually be fixed by passing empty subnet list if they already exist: + # https://techcommunity.microsoft.com/t5/azure-networking-blog/azure-virtual-network-now-supports-updates-without-subnet/ba-p/4067952 + list_by_rg = get_azure_sdk_function( + client=resource_client.resources, function_name="list_by_resource_group" + ) + existing_vnets = list( + list_by_rg( + resource_group, + f"substringof('{unique_id}', name) and " + "resourceType eq 'Microsoft.Network/virtualNetworks'", + ) + ) + if len(existing_vnets) > 0: + vnid = existing_vnets[0].id + get_by_id = get_azure_sdk_function( + client=resource_client.resources, function_name="get_by_id" + ) + + # Query for supported API versions for Microsoft.Network/virtualNetworks + # because resource_client.DEFAULT_API_VERSION is not always supported. + # (Example: "2024-11-01" is the default at the time of this writing) + # Use "2024-10-01" as a fallback if we can't determine the latest stable version. + vnet_api_version = "2024-10-01" + try: + # Get supported API versions for Microsoft.Network provider + providers = resource_client.providers.get("Microsoft.Network") + vnet_resource_type = next( + ( + rt + for rt in providers.resource_types + if rt.resource_type == "virtualNetworks" + ), + None, + ) + if vnet_resource_type and vnet_resource_type.api_versions: + stable_versions = [ + v for v in vnet_resource_type.api_versions if "preview" not in v + ] + versions_to_consider = ( + stable_versions or vnet_resource_type.api_versions + ) + vnet_api_version = sorted(versions_to_consider)[-1] + logger.info( + "Using API version: %s for virtualNetworks", vnet_api_version + ) + else: + logger.warning( + "Could not determine supported API versions for virtualNetworks, using fallback version %s", + vnet_api_version, + ) + except Exception as e: + logger.warning( + "Failed to query Microsoft.Network provider: %s. Using fallback API version 2024-10-01", + str(e), + ) + + subnet = get_by_id(vnid, vnet_api_version).properties["subnets"][0] + template_vnet = next( + ( + rs + for rs in template["resources"] + if rs["type"] == "Microsoft.Network/virtualNetworks" + ), + None, + ) + if template_vnet is not None: + template_subnets = template_vnet["properties"].get("subnets") + if template_subnets is not None: + template_subnets[0]["properties"].update(subnet["properties"]) + + # Get or create an MSI name and resource group. + # Defaults to current resource group if not provided. + use_existing_msi = ( + "msi_name" in config["provider"] and "msi_resource_group" in config["provider"] + ) + msi_resource_group = config["provider"].get("msi_resource_group", resource_group) + msi_name = config["provider"].get("msi_name", f"ray-{cluster_id}-msi") + logger.info( + "Using msi_name: %s from msi_resource_group: %s", msi_name, msi_resource_group + ) + + existing_principal_id = None + if not use_existing_msi: + # When creating a MSI for managing Azure resources, we first need to clean up + # any role assignments from the previous MSI's principal ID to avoid + # orphaned permissions when the MSI gets recreated with a new principal ID. + # Role assignments cannot be updated, only created/removed, so cleanup is required. + msi_resource_id = ( + f"/subscriptions/{subscription_id}/resourceGroups/{msi_resource_group}" + f"/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{msi_name}" + ) + try: + get_identity = get_azure_sdk_function( + client=resource_client.resources, function_name="get_by_id" + ) + existing_msi = get_identity(msi_resource_id, "2023-01-31") + existing_principal_id = getattr(existing_msi, "properties", {}).get( + "principalId" + ) + except ResourceNotFoundError: + existing_principal_id = None + except Exception as exc: + logger.warning( + "Failed to query MSI %s for existing principal: %s", + msi_name, + exc, + ) + + if existing_principal_id: + logger.info( + "Removing existing role assignments for MSI principal %s before recreation", + existing_principal_id, + ) + _delete_role_assignments_for_principal( + resource_client, + resource_group, + existing_principal_id, + ) + + delete_role_assignment = get_azure_sdk_function( + client=resource_client.resources, function_name="delete_by_id" + ) + get_role_assignment = get_azure_sdk_function( + client=resource_client.resources, function_name="get_by_id" + ) + + role_assignment_known_missing = False + initial_query_failed = False + try: + get_role_assignment( + role_assignment_resource_id, + "2022-04-01", + ) + except ResourceNotFoundError: + role_assignment_known_missing = True + logger.debug( + "Role assignment %s not found before MSI creation; skipping deletion", + role_assignment_guid, + ) + except Exception as exc: + logger.warning( + "Failed to query role assignment %s before deletion: %s", + role_assignment_guid, + exc, + ) + initial_query_failed = True + + if not role_assignment_known_missing: + try: + delete_lro = delete_role_assignment( + resource_id=role_assignment_resource_id, + api_version="2022-04-01", + ) + if hasattr(delete_lro, "wait"): + delete_lro.wait() + logger.info( + "Deleted existing role assignment %s before recreating MSI", + role_assignment_guid, + ) + + if initial_query_failed: + logger.debug( + "Retrying verification for role assignment %s", + role_assignment_guid, + ) + + if not _wait_for_role_assignment_deletion( + get_role_assignment, + role_assignment_resource_id, + role_assignment_guid, + ): + logger.warning( + "Role assignment %s not confirmed deleted", + role_assignment_guid, + ) + except ResourceNotFoundError: + logger.debug( + "Role assignment %s disappeared before deletion attempt; continuing", + role_assignment_guid, + ) + except Exception as e: + logger.warning( + "Failed to delete role assignment %s before MSI creation: %s", + role_assignment_guid, + e, + ) + + parameters = { + "properties": { + "mode": DeploymentMode.incremental, + "template": template, + "parameters": { + "subnet": {"value": subnet_mask}, + "clusterId": {"value": cluster_id}, + "msiName": {"value": msi_name}, + "msiResourceGroup": {"value": msi_resource_group}, + "createMsi": {"value": not use_existing_msi}, + "roleAssignmentGuid": {"value": role_assignment_guid}, + }, + } + } + + create_or_update = get_azure_sdk_function( + client=resource_client.deployments, function_name="create_or_update" + ) + outputs = ( + create_or_update( + resource_group_name=resource_group, + deployment_name="ray-config", + parameters=parameters, + ) + .result() + .properties.outputs + ) + + # append output resource ids to be used with vm creation + config["provider"]["msi"] = outputs["msi"]["value"] + config["provider"]["nsg"] = outputs["nsg"]["value"] + config["provider"]["subnet"] = outputs["subnet"]["value"] + + return config + + +def _configure_key_pair(config): + """ + Configure SSH keypair. Use user specified custom paths, otherwise, + generate Ray-specific keypair in this format: "ray-autoscaler_azure_{region}_{resource_group}_{ssh_user}_{index}" + """ + ssh_user = config["auth"]["ssh_user"] + public_key = None + + # Check if user specified custom SSH key paths + user_specified_private_key = "ssh_private_key" in config["auth"] + user_specified_public_key = "ssh_public_key" in config["auth"] + + # Validate that the user either specfied both keys or none, but not just one + if user_specified_private_key != user_specified_public_key: + if user_specified_private_key: + missing_key, specified_key = "ssh_public_key", "ssh_private_key" + else: + missing_key, specified_key = "ssh_private_key", "ssh_public_key" + raise ValueError( + f"{specified_key} is specified but {missing_key} is missing. " + "Both SSH key paths must be specified together, or omit both from " + "your config to use auto-generated keys." + ) + + if user_specified_private_key and user_specified_public_key: + # User specified custom paths + private_key_path = Path(config["auth"]["ssh_private_key"]).expanduser() + public_key_path = Path(config["auth"]["ssh_public_key"]).expanduser() + + # Validate that user-specified keys exist + missing_keys = [] + if not private_key_path.is_file(): + missing_keys.append(f"ssh_private_key: {private_key_path}") + if not public_key_path.is_file(): + missing_keys.append(f"ssh_public_key: {public_key_path}") + + if missing_keys: + raise ValueError( + "SSH key files from config do not exist: {}. " + "Please create the keys or remove the custom paths from your config " + "to use auto-generated keys.".format(", ".join(missing_keys)) + ) + logger.info( + "Using specified SSH keys from config: {} and {}".format( + private_key_path, public_key_path + ) + ) + + with open(public_key_path, "r") as f: + public_key = f.read() + else: + # Generate Ray-specific keys + region = config["provider"]["location"] + resource_group = config["provider"]["resource_group"] + + # Generate single deterministic key name for this configuration + key_name = generate_ssh_key_name( + "azure", None, region, resource_group, ssh_user + ) + public_key_path, private_key_path = generate_ssh_key_paths(key_name) + + # Check if this key pair already exists + if os.path.exists(private_key_path) and os.path.exists(public_key_path): + logger.info( + "Using existing Ray-specific SSH keys: {} and {}".format( + private_key_path, public_key_path + ) + ) + with open(public_key_path, "r") as f: + public_key = f.read() + else: + # Create a key pair since it doesn't exist locally + logger.info( + "Generating new Ray-specific SSH key pair at {} and {}".format( + private_key_path, public_key_path + ) + ) + os.makedirs(os.path.dirname(private_key_path), exist_ok=True) + public_key, private_key = generate_rsa_key_pair() + with open( + private_key_path, + "w", + opener=lambda path, flags: os.open(path, flags, 0o600), + ) as f: + f.write(private_key) + with open(public_key_path, "w") as f: + f.write(public_key) + + assert os.path.exists( + private_key_path + ), "Private key file {} not found for user {}".format( + private_key_path, ssh_user + ) + + config["auth"]["ssh_private_key"] = str(private_key_path) + # Remove public key path because bootstrap config must only contain paths that exist on head node + config["auth"].pop("ssh_public_key", None) + + for node_type in config["available_node_types"].values(): + azure_arm_parameters = node_type["node_config"].setdefault( + "azure_arm_parameters", {} + ) + azure_arm_parameters["adminUsername"] = ssh_user + azure_arm_parameters["publicKey"] = public_key + + return config + + +def _delete_role_assignments_for_principal( + resource_client: ResourceManagementClient, + resource_group: str, + principal_id: str, +) -> None: + """Delete all role assignments in the resource group for the given principal. + + Uses the generic ResourceManagementClient so we avoid depending on + azure-mgmt-authorization. All role assignments associated with the + provided principal ID are removed. + """ + + if not principal_id: + return + + list_by_rg = get_azure_sdk_function( + client=resource_client.resources, function_name="list_by_resource_group" + ) + delete_role_assignment = get_azure_sdk_function( + client=resource_client.resources, function_name="delete_by_id" + ) + + try: + assignments = list( + list_by_rg( + resource_group, + "resourceType eq 'Microsoft.Authorization/roleAssignments'", + ) + ) + logger.debug( + "Found %d role assignments in resource group %s while cleaning up principal %s", + len(assignments), + resource_group, + principal_id, + ) + except HttpResponseError as exc: + logger.warning( + "Failed to enumerate role assignments for resource group %s: %s", + resource_group, + exc, + ) + return + + for assignment in assignments: + properties = getattr(assignment, "properties", {}) or {} + logger.debug( + "Inspecting role assignment %s with principalId=%s roleDefinitionId=%s", + getattr(assignment, "name", ""), + properties.get("principalId"), + properties.get("roleDefinitionId"), + ) + if properties.get("principalId") != principal_id: + continue + + try: + delete_lro = delete_role_assignment( + resource_id=assignment.id, + api_version="2022-04-01", + ) + if hasattr(delete_lro, "wait"): + delete_lro.wait() + logger.info( + "Deleted existing role assignment %s for principal %s", + assignment.name, + principal_id, + ) + except ResourceNotFoundError: + logger.debug( + "Role assignment %s not found while processing principal %s", + assignment.name, + principal_id, + ) + except Exception as exc: + logger.warning( + "Failed to delete role assignment %s for principal %s: %s", + assignment.name, + principal_id, + exc, + ) + + +def _wait_for_role_assignment_deletion( + get_role_assignment: Callable[..., Any], + resource_id: str, + role_assignment_guid: str, + *, + max_attempts: int = 10, + delay_seconds: int = 2, +) -> bool: + """Poll until a role assignment disappears after deletion. + + Returns True if the assignment is confirmed deleted, False otherwise. + Logs detailed progress to aid troubleshooting when transient errors occur. + """ + + for attempt in range(1, max_attempts + 1): + try: + get_role_assignment(resource_id, "2022-04-01") + except ResourceNotFoundError: + return True + except Exception as exc: # noqa: BLE001 + logger.debug( + "Attempt %d/%d to verify removal of role assignment %s failed: %s", + attempt, + max_attempts, + role_assignment_guid, + exc, + ) + else: + logger.debug( + "Role assignment %s still present after deletion (attempt %d/%d)", + role_assignment_guid, + attempt, + max_attempts, + ) + + if attempt < max_attempts: + time.sleep(delay_seconds) + + return False + + +def _generate_arm_guid(*values: Any) -> str: + """Replicates ARM template guid() function for creating deterministic IDs.""" + + concatenated = "".join(str(v) for v in values) + return str(UUID(md5(concatenated.encode("utf-8")).hexdigest())) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..ba9a58c0aae1bb6e4988c47ef1165b8a72741e6f --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/_azure/node_provider.py @@ -0,0 +1,942 @@ +import json +import logging +import os +import time +from concurrent.futures import Future, ThreadPoolExecutor +from pathlib import Path +from threading import RLock +from typing import List, Optional +from uuid import uuid4 + +from azure.common.credentials import get_cli_profile +from azure.core.exceptions import ResourceNotFoundError +from azure.identity import DefaultAzureCredential +from azure.mgmt.compute import ComputeManagementClient +from azure.mgmt.network import NetworkManagementClient +from azure.mgmt.resource import ResourceManagementClient +from azure.mgmt.resource.resources.models import DeploymentMode + +from ray._common.usage.usage_lib import get_cloud_from_metadata_requests +from ray.autoscaler._private._azure.config import ( + _delete_role_assignments_for_principal, + _generate_arm_guid, + bootstrap_azure, + get_azure_sdk_function, +) +from ray.autoscaler._private.constants import ( + AUTOSCALER_NODE_START_WAIT_S, + AUTOSCALER_NODE_TERMINATE_WAIT_S, + MAX_PARALLEL_SHUTDOWN_WORKERS, +) +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + TAG_RAY_CLUSTER_NAME, + TAG_RAY_LAUNCH_CONFIG, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_NAME, + TAG_RAY_USER_NODE_TYPE, +) + +VM_NAME_MAX_LEN = 64 +UNIQUE_ID_LEN = 4 + +logger = logging.getLogger(__name__) +azure_logger = logging.getLogger("azure.core.pipeline.policies.http_logging_policy") +azure_logger.setLevel(logging.WARNING) + + +def synchronized(f): + def wrapper(self, *args, **kwargs): + self.lock.acquire() + try: + return f(self, *args, **kwargs) + finally: + self.lock.release() + + return wrapper + + +class AzureNodeProvider(NodeProvider): + """Node Provider for Azure + + This provider assumes Azure credentials are set by running ``az login`` + and the default subscription is configured through ``az account`` + or set in the ``provider`` field of the autoscaler configuration. + + Nodes may be in one of three states: {pending, running, terminated}. Nodes + appear immediately once started by ``create_node``, and transition + immediately to terminated when ``terminate_node`` is called. + """ + + def __init__(self, provider_config, cluster_name): + NodeProvider.__init__(self, provider_config, cluster_name) + subscription_id = provider_config.get("subscription_id") + if subscription_id is None: + # Get subscription from logged in azure profile + # if it isn't provided in the provider_config + # so operations like `get-head-ip` will work + subscription_id = get_cli_profile().get_subscription_id() + logger.info( + "subscription_id not found in provider config, falling back " + f"to subscription_id from the logged in azure profile: {subscription_id}" + ) + self.cache_stopped_nodes = provider_config.get("cache_stopped_nodes", True) + + # Detect cloud environment to optimize Azure credential chain. + # On non-Azure clouds (AWS, GCP), skip Azure-specific auth methods + # (managed identity, workload identity) to avoid IMDS timeout delays / failures. + detected_cloud = get_cloud_from_metadata_requests() + on_azure = detected_cloud == "azure" + + if on_azure: + logger.info( + "Initializing Azure node provider for Azure infrastructure " + "running on Azure cloud environment" + ) + else: + logger.info( + f"Initializing Azure node provider for Azure infrastructure " + f"but detected this is running on a '{detected_cloud}' environment. " + f"Skipping Azure-specific authentication methods to avoid timeouts." + ) + + credential = DefaultAzureCredential( + exclude_shared_token_cache_credential=True, + exclude_managed_identity_credential=not on_azure, + exclude_workload_identity_credential=not on_azure, + ) + self.compute_client = ComputeManagementClient(credential, subscription_id) + self.network_client = NetworkManagementClient(credential, subscription_id) + self.resource_client = ResourceManagementClient(credential, subscription_id) + + self.lock = RLock() + + # cache node objects + self.cached_nodes = {} + + # Cache terminating node operations + self.terminating_nodes: dict[str, Future] = {} + self.termination_executor = ThreadPoolExecutor( + max_workers=MAX_PARALLEL_SHUTDOWN_WORKERS + ) + + @synchronized + def _get_filtered_nodes(self, tag_filters): + # add cluster name filter to only get nodes from this cluster + cluster_tag_filters = {**tag_filters, TAG_RAY_CLUSTER_NAME: self.cluster_name} + + def match_tags(tags): + for k, v in cluster_tag_filters.items(): + if tags.get(k) != v: + return False + return True + + vms = self.compute_client.virtual_machines.list( + resource_group_name=self.provider_config["resource_group"] + ) + + nodes = [self._extract_metadata(vm) for vm in vms] + self.cached_nodes = {node["name"]: node for node in nodes} + + # Update terminating nodes list by removing nodes that + # have finished termination. + self.terminating_nodes = { + k: v for k, v in self.terminating_nodes.items() if not v.done() + } + + return { + k: v + for k, v in self.cached_nodes.items() + if v["tags"] is not None and match_tags(v["tags"]) + } + + def _extract_metadata(self, vm): + # get tags + metadata = {"name": vm.name, "tags": vm.tags, "status": ""} + + # get status + resource_group = self.provider_config["resource_group"] + try: + instance = self.compute_client.virtual_machines.instance_view( + resource_group_name=resource_group, vm_name=vm.name + ).as_dict() + except ResourceNotFoundError: + return metadata + + for status in instance["statuses"]: + # If ProvisioningState is "failed" (e.g., + # ProvisioningState/failed/RetryableError), we can get a third + # string here, so we need to limit to the first two outputs. + code, state = status["code"].split("/")[:2] + # skip provisioning status + if code == "PowerState": + metadata["status"] = state + break + + # get ip data + nic_id = vm.network_profile.network_interfaces[0].id + metadata["nic_name"] = nic_id.split("/")[-1] + nic = self.network_client.network_interfaces.get( + resource_group_name=resource_group, + network_interface_name=metadata["nic_name"], + ) + ip_config = nic.ip_configurations[0] + + # Get public IP if not using internal IPs or if this is the + # head node and use_external_head_ip is True + if not self.provider_config.get("use_internal_ips", False) or ( + self.provider_config.get("use_external_head_ip", False) + and metadata["tags"][TAG_RAY_NODE_KIND] == NODE_KIND_HEAD + ): + public_ip_id = ip_config.public_ip_address.id + metadata["public_ip_name"] = public_ip_id.split("/")[-1] + public_ip = self.network_client.public_ip_addresses.get( + resource_group_name=resource_group, + public_ip_address_name=metadata["public_ip_name"], + ) + metadata["external_ip"] = public_ip.ip_address + + metadata["internal_ip"] = ip_config.private_ip_address + + return metadata + + def _get_zones_for_vm_size(self, vm_size, location): + """Get usable availability zones for a given VM size in a specific location.""" + try: + # Note: Azure ResourceSKUs API filters don't work reliably(?), so we query all SKUs + # and filter in code. Each SKU object represents one location for the VM size. + skus = self.compute_client.resource_skus.list() + + for sku in skus: + if sku.name == vm_size and sku.location_info: + # Each SKU object represents one location, check if it matches our target + for location_info in sku.location_info: + if location_info.location.lower() == location.lower(): + zones = location_info.zones if location_info.zones else [] + logger.debug( + f"Found {vm_size} in {location} with zones: {zones}" + ) + return sorted(zones) + + logger.warning(f"No zones found for {vm_size} in {location}") + return [] # No zones available for this VM size + except Exception as e: + logger.warning( + f"Failed to get zones for VM size {vm_size} in location {location}: {str(e)}" + ) + return [] + + def _parse_availability_zones( + self, availability_zone_config: Optional[str] + ) -> Optional[List[str]]: + """Parse availability_zone configuration from comma-separated string format. + + Args: + availability_zone_config: Can be: + - String: comma-separated zones like "1,2,3" + - "none": explicitly disable zones + - "auto": let Azure automatically pick zones + - None: no zones specified (defaults to letting Azure pick) + + Returns: + List of zone strings, or None if zones explicitly disabled, or [] if auto/unspecified + """ + if availability_zone_config is None: + return [] # Auto - let Azure pick + + # Handle string format (AWS-style comma-separated) + if isinstance(availability_zone_config, str): + # Strip whitespace and split by comma + zones = [zone.strip() for zone in availability_zone_config.split(",")] + + # Handle special cases (case-insensitive) + if len(zones) == 1: + zone_lower = zones[0].lower() + if zone_lower in ["none", "null"]: + return None # Explicitly disabled + elif zone_lower == "auto": + return [] # Auto - let Azure pick + + # Handle empty string or whitespace-only + if not zones or all(not zone for zone in zones): + return [] # Auto - let Azure pick + return zones + + # Unsupported format + raise ValueError( + f"availability_zone must be a string, got {type(availability_zone_config).__name__}: {availability_zone_config!r}" + ) + + def _validate_zones_for_node_pool(self, zones, location, vm_size): + """Validate that the specified zones are available for the given VM size in the location.""" + # Special case: zones=None means explicitly disabled availability zones + if zones is None: + logger.info( + "Zones explicitly disabled with 'none' - will create VM without an availability zone" + ) + return None # Special return value to indicate "no zones by choice" + + vm_zones = self._get_zones_for_vm_size(vm_size, location) + + available_zones = set(vm_zones) + if not available_zones: + logger.warning("No zones available for this VM size and location") + return [] + + if zones: + requested_zones = {str(z) for z in zones} + intersection = sorted(available_zones.intersection(requested_zones)) + return intersection + + return sorted(available_zones) + + def stopped_nodes(self, tag_filters): + """Return a list of stopped node ids filtered by the specified tags dict.""" + nodes = self._get_filtered_nodes(tag_filters=tag_filters) + return [k for k, v in nodes.items() if v["status"].startswith("deallocat")] + + def non_terminated_nodes(self, tag_filters): + """Return a list of node ids filtered by the specified tags dict. + + This list must not include terminated nodes. For performance reasons, + providers are allowed to cache the result of a call to nodes() to + serve single-node queries (e.g. is_running(node_id)). This means that + nodes() must be called again to refresh results. + + Examples: + >>> from ray.autoscaler.tags import TAG_RAY_NODE_KIND + >>> provider = ... # doctest: +SKIP + >>> provider.non_terminated_nodes( # doctest: +SKIP + ... {TAG_RAY_NODE_KIND: "worker"}) + ["node-1", "node-2"] + """ + nodes = self._get_filtered_nodes(tag_filters=tag_filters) + return [ + k + for k, v in nodes.items() + if not v["status"].startswith("deallocat") or k in self.terminating_nodes + ] + + def is_running(self, node_id): + """Return whether the specified node is running.""" + # always get current status + node = self._get_node(node_id=node_id) + return node["status"] == "running" + + def is_terminated(self, node_id): + """Return whether the specified node is terminated.""" + # always get current status + node = self._get_node(node_id=node_id) + return node["status"].startswith("deallocat") + + def node_tags(self, node_id): + """Returns the tags of the given node (string dict).""" + return self._get_cached_node(node_id=node_id)["tags"] + + def external_ip(self, node_id): + """Returns the external ip of the given node.""" + ip = ( + self._get_cached_node(node_id=node_id)["external_ip"] + or self._get_node(node_id=node_id)["external_ip"] + ) + return ip + + def internal_ip(self, node_id): + """Returns the internal ip (Ray ip) of the given node.""" + ip = ( + self._get_cached_node(node_id=node_id)["internal_ip"] + or self._get_node(node_id=node_id)["internal_ip"] + ) + return ip + + def create_node(self, node_config, tags, count): + resource_group = self.provider_config["resource_group"] + + if self.cache_stopped_nodes: + VALIDITY_TAGS = [ + TAG_RAY_CLUSTER_NAME, + TAG_RAY_NODE_KIND, + TAG_RAY_LAUNCH_CONFIG, + TAG_RAY_USER_NODE_TYPE, + ] + filters = {tag: tags[tag] for tag in VALIDITY_TAGS if tag in tags} + reuse_nodes = self.stopped_nodes(filters)[:count] + logger.info( + f"Reusing nodes {list(reuse_nodes)}. " + "To disable reuse, set `cache_stopped_nodes: False` " + "under `provider` in the cluster configuration.", + ) + start = get_azure_sdk_function( + client=self.compute_client.virtual_machines, function_name="start" + ) + for node_id in reuse_nodes: + start(resource_group_name=resource_group, vm_name=node_id).wait() + self.set_node_tags(node_id, tags) + count -= len(reuse_nodes) + + if count: + self._create_node(node_config, tags, count) + + def _create_node(self, node_config, tags, count): + """Creates a number of nodes within the namespace.""" + resource_group = self.provider_config["resource_group"] + location = self.provider_config["location"] + vm_size = node_config["azure_arm_parameters"]["vmSize"] + + # Determine availability zones with precedence: node-level > provider-level + # Check for "availability_zone" field in node config first + node_availability_zone = node_config.get("azure_arm_parameters", {}).get( + "availability_zone" + ) + # Then check provider-level "availability_zone" + provider_availability_zone = self.provider_config.get("availability_zone") + + requested_zones = [] + zone_source = "default" + + # Precedence: node availability_zone > provider availability_zone + if node_availability_zone is not None: + requested_zones = self._parse_availability_zones(node_availability_zone) + zone_source = "node config availability_zone" + elif provider_availability_zone is not None: + requested_zones = self._parse_availability_zones(provider_availability_zone) + zone_source = "provider availability_zone" + + logger.info(f"Requested zones from {zone_source}: {requested_zones}") + + # Get actually available zones for this VM size + available_zones = self._validate_zones_for_node_pool( + requested_zones, location, vm_size + ) + + # Handle explicit zone disabling + zones_explicitly_disabled = available_zones is None + + if requested_zones and not zones_explicitly_disabled and not available_zones: + raise ValueError( + f"No available zones for VM size {vm_size} in {location}. " + f"Requested: {requested_zones}, but none are available for this VM size." + ) + + # load the template file + current_path = Path(__file__).parent + template_path = current_path.joinpath("azure-vm-template.json") + with open(template_path, "r") as template_fp: + template = json.load(template_fp) + + # get the tags + config_tags = node_config.get("tags", {}).copy() + config_tags.update(tags) + config_tags[TAG_RAY_CLUSTER_NAME] = self.cluster_name + + deployment_name = "{node}-{unique_id}-{vm_id}".format( + node=config_tags.get(TAG_RAY_NODE_NAME, "node"), + unique_id=self.provider_config["unique_id"], + vm_id=uuid4().hex[:UNIQUE_ID_LEN], + )[:VM_NAME_MAX_LEN] + + template_params = node_config["azure_arm_parameters"].copy() + # Remove availability_zone from template params since ARM template expects "zones" + template_params.pop("availability_zone", None) + # Use deployment_name for the vmName template parameter since + # the template will append copyIndex() for each VM that gets created + # to guarantee uniqueness. + template_params["vmName"] = deployment_name + # Provision public IP if not using internal IPs or if this is the + # head node and use_external_head_ip is True + template_params["provisionPublicIp"] = not self.provider_config.get( + "use_internal_ips", False + ) or ( + self.provider_config.get("use_external_head_ip", False) + and config_tags[TAG_RAY_NODE_KIND] == NODE_KIND_HEAD + ) + template_params["vmTags"] = config_tags + template_params["vmCount"] = count + template_params["msi"] = self.provider_config["msi"] + template_params["nsg"] = self.provider_config["nsg"] + template_params["subnet"] = self.provider_config["subnet"] + + # Add zone information based on availability and requested zones + if zones_explicitly_disabled: + # User explicitly disabled zones with ["None"] + template_params["zones"] = [] + logger.info( + f"Creating {count} VMs with zones explicitly disabled (no availability zone)" + ) + elif available_zones: + # Pass the list of available zones to the template + template_params["zones"] = available_zones + logger.info( + f"Creating {count} VMs, distributed across availability zones: {available_zones}" + ) + else: + # For non-zonal deployments (no zones available), use empty array + template_params["zones"] = [] + logger.info(f"Creating {count} VMs without specific availability zone") + + parameters = { + "properties": { + "mode": DeploymentMode.incremental, + "template": template, + "parameters": { + key: {"value": value} for key, value in template_params.items() + }, + } + } + + # TODO: we could get the private/public ips back directly + create_or_update = get_azure_sdk_function( + client=self.resource_client.deployments, + function_name="create_or_update", + ) + create_or_update( + resource_group_name=resource_group, + deployment_name=deployment_name, + parameters=parameters, + ).wait(timeout=AUTOSCALER_NODE_START_WAIT_S) + + @synchronized + def set_node_tags(self, node_id, tags): + """Sets the tag values (string dict) for the specified node.""" + node_tags = self._get_cached_node(node_id)["tags"] + node_tags.update(tags) + update = get_azure_sdk_function( + client=self.compute_client.virtual_machines, function_name="update" + ) + update( + resource_group_name=self.provider_config["resource_group"], + vm_name=node_id, + parameters={"tags": node_tags}, + ) + self.cached_nodes[node_id]["tags"] = node_tags + + def terminate_node(self, node_id): + """Terminates the specified node. This will delete the VM and + associated resources (NIC, IP, Storage) for the specified node.""" + + resource_group = self.provider_config["resource_group"] + + if self.cache_stopped_nodes: + try: + # stop machine and leave all resources + logger.info( + f"Stopping instance {node_id}" + "(to fully terminate instead, " + "set `cache_stopped_nodes: False` " + "under `provider` in the cluster configuration)" + ) + stop = get_azure_sdk_function( + client=self.compute_client.virtual_machines, + function_name="deallocate", + ) + stop(resource_group_name=resource_group, vm_name=node_id) + except Exception as e: + logger.warning("Failed to stop VM: {}".format(e)) + + # If node_id is in terminating nodes dict, it's already terminating + # Otherwise, kick off termination and add it to the dict + elif node_id not in self.terminating_nodes: + self.terminating_nodes[node_id] = self.termination_executor.submit( + self._delete_node_and_resources, resource_group, node_id + ) + + def _delete_node_and_resources(self, resource_group, node_id): + try: + vm = self.compute_client.virtual_machines.get( + resource_group_name=resource_group, vm_name=node_id + ) + except ResourceNotFoundError as e: + # Node no longer exists + logger.warning("Failed to delete VM: {}".format(e)) + return + + # Gather dependent disks + disks = set() + if vm.storage_profile is not None and vm.storage_profile.data_disks is not None: + for d in vm.storage_profile.data_disks: + if d.name is not None: + disks.add(d.name) + if ( + vm.storage_profile is not None + and vm.storage_profile.os_disk is not None + and vm.storage_profile.os_disk.name is not None + ): + disks.add(vm.storage_profile.os_disk.name) + + # Gather dependent NICs and public IPs + nics = set() + ips = set() + if ( + vm.network_profile is not None + and vm.network_profile.network_interfaces is not None + ): + for nint in vm.network_profile.network_interfaces: + if nint.id is not None: + nic_name = nint.id.split("/")[-1] + nics.add(nic_name) + # Get public IP if not using internal IPs or if this is the + # head node and use_external_head_ip is True + if not self.provider_config.get("use_internal_ips", False) or ( + self.provider_config.get("use_external_head_ip", False) + and vm.tags[TAG_RAY_NODE_KIND] == NODE_KIND_HEAD + ): + nic = self.network_client.network_interfaces.get( + resource_group_name=resource_group, + network_interface_name=nic_name, + ) + if nic.ip_configurations is not None: + for ipc in nic.ip_configurations: + if ipc.public_ip_address.id is not None: + ips.add(ipc.public_ip_address.id.split("/")[-1]) + + # Delete VM + st = time.monotonic() + delete = get_azure_sdk_function( + client=self.compute_client.virtual_machines, + function_name="delete", + ) + try: + delete(resource_group_name=resource_group, vm_name=node_id).wait( + timeout=AUTOSCALER_NODE_TERMINATE_WAIT_S + ) + except Exception as e: + logger.warning("Failed to delete VM: {}".format(e)) + + # Delete disks (no need to wait for these, but gather the LROs for end) + disk_lros = [] + delete = get_azure_sdk_function( + client=self.compute_client.disks, function_name="delete" + ) + for d in disks: + try: + disk_lros.append( + delete( + resource_group_name=resource_group, + disk_name=d, + ) + ) + except Exception as e: + logger.warning("Failed to delete disk: {}".format(e)) + + # Delete NICs + nic_lros = [] + delete = get_azure_sdk_function( + client=self.network_client.network_interfaces, function_name="delete" + ) + for n in nics: + try: + nic_lros.append( + delete( + resource_group_name=resource_group, + network_interface_name=n, + ) + ) + except Exception as e: + logger.warning("Failed to delete NIC: {}".format(e)) + + while ( + not all(nlro.done() for nlro in nic_lros) + and (time.monotonic() - st) < AUTOSCALER_NODE_TERMINATE_WAIT_S + ): + time.sleep(0.1) + + # Delete Public IPs + delete = get_azure_sdk_function( + client=self.network_client.public_ip_addresses, + function_name="delete", + ) + ip_lros = [] + for ip in ips: + try: + ip_lros.append( + delete( + resource_group_name=resource_group, + public_ip_address_name=ip, + ) + ) + except Exception as e: + logger.warning("Failed to delete public IP: {}".format(e)) + + while ( + not all(dlro.done() for dlro in disk_lros) + and (time.monotonic() - st) < AUTOSCALER_NODE_TERMINATE_WAIT_S + ): + time.sleep(0.1) + while ( + not all(iplro.done() for iplro in ip_lros) + and (time.monotonic() - st) < AUTOSCALER_NODE_TERMINATE_WAIT_S + ): + time.sleep(0.1) + + def cleanup_cluster_resources(self): + """Delete shared cluster infrastructure (MSI, NSG, Subnet, VNet).""" + + resource_group = self.provider_config["resource_group"] + + msi_principal_id = self._cleanup_managed_identity( + resource_group, self.provider_config.get("msi") + ) + + subnet_id = self.provider_config.get("subnet") + vnet_name = self._cleanup_subnet(resource_group, subnet_id) + + nsg_id = self.provider_config.get("nsg") + self._cleanup_nsg(resource_group, nsg_id) + + self._cleanup_vnet(resource_group, subnet_id, vnet_name) + + self._cleanup_role_assignments(resource_group, msi_principal_id) + + self._prune_provider_config_entries() + self._cleanup_config_cache() + + @staticmethod + def _get_resource_name_from_id(resource_id: Optional[str]) -> Optional[str]: + if resource_id: + return resource_id.split("/")[-1] + return None + + @staticmethod + def _retry_delete(delete_fn, max_retries: int = 5, initial_delay: int = 2): + """Retry a delete operation with exponential backoff.""" + + delay = initial_delay + for attempt in range(max_retries): + try: + return delete_fn() + except Exception as exc: # noqa: BLE001 + error_msg = str(exc) + if "InUse" in error_msg and attempt < max_retries - 1: + logger.info( + "Resource still in use, retrying in %ss (attempt %s/%s)...", + delay, + attempt + 1, + max_retries, + ) + time.sleep(delay) + delay *= 2 + else: + raise + + def _cleanup_managed_identity( + self, resource_group: str, msi_id: Optional[str] + ) -> Optional[str]: + if not msi_id: + return None + + msi_name = self._get_resource_name_from_id(msi_id) + if not msi_name: + return None + + msi_principal_id: Optional[str] = None + try: + get_identity = get_azure_sdk_function( + client=self.resource_client.resources, + function_name="get_by_id", + ) + existing_msi = get_identity(msi_id, "2023-01-31") + msi_principal_id = getattr(existing_msi, "properties", {}).get( + "principalId" + ) + except ResourceNotFoundError: + msi_principal_id = None + except Exception as exc: # noqa: BLE001 + logger.warning( + "Failed to query MSI %s for principal ID prior to deletion: %s", + msi_name, + exc, + ) + + try: + logger.info("Deleting Managed Service Identity: %s", msi_name) + delete = get_azure_sdk_function( + client=self.resource_client.resources, + function_name="delete_by_id", + ) + delete(resource_id=msi_id, api_version="2023-01-31").wait() + logger.info("Successfully deleted MSI: %s", msi_name) + except ResourceNotFoundError: + logger.info("MSI %s not found, may have been already deleted", msi_name) + except Exception as exc: # noqa: BLE001 + logger.warning("Failed to delete MSI %s: %s", msi_name, exc) + + return msi_principal_id + + def _cleanup_subnet( + self, resource_group: str, subnet_id: Optional[str] + ) -> Optional[str]: + if not subnet_id: + return None + + subnet_name = self._get_resource_name_from_id(subnet_id) + vnet_name: Optional[str] = None + + if subnet_id and "/virtualNetworks/" in subnet_id: + parts = subnet_id.split("/") + vnet_idx = parts.index("virtualNetworks") + if vnet_idx + 1 < len(parts): + vnet_name = parts[vnet_idx + 1] + + if not subnet_name or not vnet_name: + return None + + try: + logger.info("Deleting Subnet: %s in VNet: %s", subnet_name, vnet_name) + + def delete_subnet(): + delete = get_azure_sdk_function( + client=self.network_client.subnets, function_name="delete" + ) + delete( + resource_group_name=resource_group, + virtual_network_name=vnet_name, + subnet_name=subnet_name, + ).wait() + + self._retry_delete(delete_subnet) + logger.info("Successfully deleted Subnet: %s", subnet_name) + except ResourceNotFoundError: + logger.info( + "Subnet %s not found, may have been already deleted", subnet_name + ) + except Exception as exc: # noqa: BLE001 + logger.warning("Failed to delete Subnet %s: %s", subnet_name, exc) + + return vnet_name + + def _cleanup_nsg(self, resource_group: str, nsg_id: Optional[str]) -> None: + if not nsg_id: + return + + nsg_name = self._get_resource_name_from_id(nsg_id) + if not nsg_name: + return + + try: + logger.info("Deleting Network Security Group: %s", nsg_name) + + def delete_nsg(): + delete = get_azure_sdk_function( + client=self.network_client.network_security_groups, + function_name="delete", + ) + delete( + resource_group_name=resource_group, + network_security_group_name=nsg_name, + ).wait() + + self._retry_delete(delete_nsg) + logger.info("Successfully deleted NSG: %s", nsg_name) + except ResourceNotFoundError: + logger.info("NSG %s not found, may have been already deleted", nsg_name) + except Exception as exc: # noqa: BLE001 + logger.warning("Failed to delete NSG %s: %s", nsg_name, exc) + + def _cleanup_vnet( + self, + resource_group: str, + subnet_id: Optional[str], + vnet_name: Optional[str], + ) -> None: + if not subnet_id or not vnet_name: + return + + try: + logger.info("Deleting Virtual Network: %s", vnet_name) + + def delete_vnet(): + delete = get_azure_sdk_function( + client=self.network_client.virtual_networks, + function_name="delete", + ) + delete( + resource_group_name=resource_group, + virtual_network_name=vnet_name, + ).wait() + + self._retry_delete(delete_vnet) + logger.info("Successfully deleted VNet: %s", vnet_name) + except ResourceNotFoundError: + logger.info("VNet %s not found, may have been already deleted", vnet_name) + except Exception as exc: # noqa: BLE001 + logger.warning("Failed to delete VNet %s: %s", vnet_name, exc) + + def _cleanup_role_assignments( + self, resource_group: str, msi_principal_id: Optional[str] + ) -> None: + subscription_id = self.provider_config.get("subscription_id") + unique_id = self.provider_config.get("unique_id") + if not subscription_id or not unique_id: + return + + cluster_id = f"{self.cluster_name}-{unique_id}" + role_assignment_name = f"ray-{cluster_id}-ra" + role_assignment_guid = _generate_arm_guid(role_assignment_name) + role_assignment_id = ( + f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers" + f"/Microsoft.Authorization/roleAssignments/{role_assignment_guid}" + ) + + if msi_principal_id: + _delete_role_assignments_for_principal( + self.resource_client, resource_group, msi_principal_id + ) + + delete_role_assignment = get_azure_sdk_function( + client=self.resource_client.resources, function_name="delete_by_id" + ) + try: + delete_lro = delete_role_assignment( + resource_id=role_assignment_id, + api_version="2022-04-01", + ) + if hasattr(delete_lro, "wait"): + delete_lro.wait() + logger.info( + "Deleted role assignment %s for cluster %s", + role_assignment_guid, + self.cluster_name, + ) + except ResourceNotFoundError: + logger.debug( + "Role assignment %s not found during cleanup", role_assignment_guid + ) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Failed to delete role assignment %s: %s", + role_assignment_guid, + exc, + ) + + def _prune_provider_config_entries(self) -> None: + for key in ("msi", "nsg", "subnet"): + self.provider_config.pop(key, None) + + def _cleanup_config_cache(self) -> None: + cache_path = self.provider_config.get("_config_cache_path") + if not cache_path: + return + + try: + if os.path.exists(cache_path): + os.remove(cache_path) + logger.info( + "Deleted cached Ray config at %s after resource cleanup", + cache_path, + ) + except Exception as exc: # noqa: BLE001 + logger.warning("Failed to delete cached Ray config %s: %s", cache_path, exc) + finally: + self.provider_config.pop("_config_cache_path", None) + + def _get_node(self, node_id): + self._get_filtered_nodes({}) # Side effect: updates cache + return self.cached_nodes[node_id] + + def _get_cached_node(self, node_id): + return self.cached_nodes.get(node_id) or self._get_node(node_id=node_id) + + @staticmethod + def bootstrap_config(cluster_config): + return bootstrap_azure(cluster_config) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0eae85845244aef60739ccef0dde7ad5a5964f0f Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/config.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea6336121e3409a7c89448e57eefa0f2b4747f76 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e55b3de825317311dc1c2ec387221eab8c034846 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a78dd4b4b26f5c2f1ee2c2fb7335797ac5783ad Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/config.py b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/config.py new file mode 100644 index 0000000000000000000000000000000000000000..464b170ddb646089f84a2bd5b3a053ea11e4d601 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/config.py @@ -0,0 +1,116 @@ +import logging +import os +import stat + +from ray.autoscaler._private.aliyun.utils import AcsClient + +# instance status +PENDING = "Pending" +RUNNING = "Running" +STARTING = "Starting" +STOPPING = "Stopping" +STOPPED = "Stopped" + +logger = logging.getLogger(__name__) + + +def bootstrap_aliyun(config): + # print(config["provider"]) + # create vpc + _get_or_create_vpc(config) + # create security group id + _get_or_create_security_group(config) + # create vswitch + _get_or_create_vswitch(config) + # create key pair + _get_or_import_key_pair(config) + # print(config["provider"]) + return config + + +def _client(config): + return AcsClient( + access_key=config["provider"].get("access_key"), + access_key_secret=config["provider"].get("access_key_secret"), + region_id=config["provider"]["region"], + max_retries=1, + ) + + +def _get_or_create_security_group(config): + cli = _client(config) + security_groups = cli.describe_security_groups(vpc_id=config["provider"]["vpc_id"]) + if security_groups is not None and len(security_groups) > 0: + config["provider"]["security_group_id"] = security_groups[0]["SecurityGroupId"] + return config + + security_group_id = cli.create_security_group(vpc_id=config["provider"]["vpc_id"]) + + for rule in config["provider"].get("security_group_rule", {}): + cli.authorize_security_group( + security_group_id=security_group_id, + port_range=rule["port_range"], + source_cidr_ip=rule["source_cidr_ip"], + ip_protocol=rule["ip_protocol"], + ) + config["provider"]["security_group_id"] = security_group_id + return + + +def _get_or_create_vpc(config): + cli = _client(config) + vpcs = cli.describe_vpcs() + if vpcs is not None and len(vpcs) > 0: + config["provider"]["vpc_id"] = vpcs[0].get("VpcId") + return + + vpc_id = cli.create_vpc() + if vpc_id is not None: + config["provider"]["vpc_id"] = vpc_id + + +def _get_or_create_vswitch(config): + cli = _client(config) + vswitches = cli.describe_v_switches(vpc_id=config["provider"]["vpc_id"]) + if vswitches is not None and len(vswitches) > 0: + config["provider"]["v_switch_id"] = vswitches[0].get("VSwitchId") + return + + v_switch_id = cli.create_v_switch( + vpc_id=config["provider"]["vpc_id"], + zone_id=config["provider"]["zone_id"], + cidr_block=config["provider"]["cidr_block"], + ) + + if v_switch_id is not None: + config["provider"]["v_switch_id"] = v_switch_id + + +def _get_or_import_key_pair(config): + cli = _client(config) + key_name = config["provider"].get("key_name", "ray") + key_path = os.path.expanduser("~/.ssh/{}".format(key_name)) + keypairs = cli.describe_key_pairs(key_pair_name=key_name) + + if keypairs is not None and len(keypairs) > 0: + if "ssh_private_key" not in config["auth"]: + logger.info( + "{} keypair exists, use {} as local ssh key".format(key_name, key_path) + ) + config["auth"]["ssh_private_key"] = key_path + else: + if "ssh_private_key" not in config["auth"]: + # create new keypair + resp = cli.create_key_pair(key_pair_name=key_name) + if resp is not None: + with open(key_path, "w+") as f: + f.write(resp.get("PrivateKeyBody")) + os.chmod(key_path, stat.S_IRUSR) + config["auth"]["ssh_private_key"] = key_path + else: + public_key_file = config["auth"]["ssh_private_key"] + ".pub" + # create new keypair, from local file + with open(public_key_file) as f: + public_key = f.readline().strip("\n") + cli.import_key_pair(key_pair_name=key_name, public_key_body=public_key) + return diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..e9b65a07936f9a1aaf18a9763e2263d542738809 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/node_provider.py @@ -0,0 +1,324 @@ +import logging +import random +import threading +import time +from collections import defaultdict +from typing import Any, Dict, List, Optional + +from ray.autoscaler._private.aliyun.config import ( + PENDING, + RUNNING, + STOPPED, + STOPPING, + bootstrap_aliyun, +) +from ray.autoscaler._private.aliyun.utils import AcsClient +from ray.autoscaler._private.cli_logger import cli_logger +from ray.autoscaler._private.constants import BOTO_MAX_RETRIES +from ray.autoscaler._private.log_timer import LogTimer +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + TAG_RAY_CLUSTER_NAME, + TAG_RAY_LAUNCH_CONFIG, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_NAME, + TAG_RAY_NODE_STATUS, + TAG_RAY_USER_NODE_TYPE, +) + +logger = logging.getLogger(__name__) + +TAG_BATCH_DELAY = 1 +STOPPING_NODE_DELAY = 1 + + +class AliyunNodeProvider(NodeProvider): + def __init__(self, provider_config, cluster_name): + NodeProvider.__init__(self, provider_config, cluster_name) + self.cache_stopped_nodes = provider_config.get("cache_stopped_nodes", True) + self.acs = AcsClient( + access_key=provider_config["access_key"], + access_key_secret=provider_config["access_key_secret"], + region_id=provider_config["region"], + max_retries=BOTO_MAX_RETRIES, + ) + + # Try availability zones round-robin, starting from random offset + self.subnet_idx = random.randint(0, 100) + + # Tags that we believe to actually be on the node. + self.tag_cache = {} + # Tags that we will soon upload. + self.tag_cache_pending = defaultdict(dict) + # Number of threads waiting for a batched tag update. + self.batch_thread_count = 0 + self.batch_update_done = threading.Event() + self.batch_update_done.set() + self.ready_for_new_batch = threading.Event() + self.ready_for_new_batch.set() + self.tag_cache_lock = threading.Lock() + self.count_lock = threading.Lock() + + # Cache of node objects from the last nodes() call. This avoids + # excessive DescribeInstances requests. + self.cached_nodes = {} + + def non_terminated_nodes(self, tag_filters: Dict[str, str]) -> List[str]: + tags = [ + { + "Key": TAG_RAY_CLUSTER_NAME, + "Value": self.cluster_name, + }, + ] + for k, v in tag_filters.items(): + tags.append( + { + "Key": k, + "Value": v, + } + ) + + instances = self.acs.describe_instances(tags=tags) + non_terminated_instance = [] + for instance in instances: + if instance.get("Status") == RUNNING or instance.get("Status") == PENDING: + non_terminated_instance.append(instance.get("InstanceId")) + self.cached_nodes[instance.get("InstanceId")] = instance + return non_terminated_instance + + def is_running(self, node_id: str) -> bool: + instances = self.acs.describe_instances(instance_ids=[node_id]) + if instances is not None: + instance = instances[0] + return instance.get("Status") == "Running" + cli_logger.error("Invalid node id: %s", node_id) + return False + + def is_terminated(self, node_id: str) -> bool: + instances = self.acs.describe_instances(instance_ids=[node_id]) + if instances is not None: + assert len(instances) == 1 + instance = instances[0] + return instance.get("Status") == "Stopped" + cli_logger.error("Invalid node id: %s", node_id) + return False + + def node_tags(self, node_id: str) -> Dict[str, str]: + instances = self.acs.describe_instances(instance_ids=[node_id]) + if instances is not None: + assert len(instances) == 1 + instance = instances[0] + if instance.get("Tags") is not None: + node_tags = dict() + for tag in instance.get("Tags").get("Tag"): + node_tags[tag.get("TagKey")] = tag.get("TagValue") + return node_tags + return dict() + + def external_ip(self, node_id: str) -> str: + while True: + instances = self.acs.describe_instances(instance_ids=[node_id]) + if instances is not None: + assert len(instances) + instance = instances[0] + if ( + instance.get("PublicIpAddress") is not None + and instance.get("PublicIpAddress").get("IpAddress") is not None + ): + if len(instance.get("PublicIpAddress").get("IpAddress")) > 0: + return instance.get("PublicIpAddress").get("IpAddress")[0] + cli_logger.error("PublicIpAddress attribute is not exist. %s" % instance) + time.sleep(STOPPING_NODE_DELAY) + + def internal_ip(self, node_id: str) -> str: + while True: + instances = self.acs.describe_instances(instance_ids=[node_id]) + if instances is not None: + assert len(instances) == 1 + instance = instances[0] + if ( + instance.get("VpcAttributes") is not None + and instance.get("VpcAttributes").get("PrivateIpAddress") + is not None + and len( + instance.get("VpcAttributes") + .get("PrivateIpAddress") + .get("IpAddress") + ) + > 0 + ): + return ( + instance.get("VpcAttributes") + .get("PrivateIpAddress") + .get("IpAddress")[0] + ) + cli_logger.error("InnerIpAddress attribute is not exist. %s" % instance) + time.sleep(STOPPING_NODE_DELAY) + + def set_node_tags(self, node_id: str, tags: Dict[str, str]) -> None: + is_batching_thread = False + with self.tag_cache_lock: + if not self.tag_cache_pending: + is_batching_thread = True + # Wait for threads in the last batch to exit + self.ready_for_new_batch.wait() + self.ready_for_new_batch.clear() + self.batch_update_done.clear() + self.tag_cache_pending[node_id].update(tags) + + if is_batching_thread: + time.sleep(TAG_BATCH_DELAY) + with self.tag_cache_lock: + self._update_node_tags() + self.batch_update_done.set() + + with self.count_lock: + self.batch_thread_count += 1 + self.batch_update_done.wait() + + with self.count_lock: + self.batch_thread_count -= 1 + if self.batch_thread_count == 0: + self.ready_for_new_batch.set() + + def _update_node_tags(self): + batch_updates = defaultdict(list) + + for node_id, tags in self.tag_cache_pending.items(): + for x in tags.items(): + batch_updates[x].append(node_id) + self.tag_cache[node_id] = tags + + self.tag_cache_pending = defaultdict(dict) + + self._create_tags(batch_updates) + + def _create_tags(self, batch_updates): + + for (k, v), node_ids in batch_updates.items(): + m = "Set tag {}={} on {}".format(k, v, node_ids) + with LogTimer("AliyunNodeProvider: {}".format(m)): + if k == TAG_RAY_NODE_NAME: + k = "Name" + + self.acs.tag_resource(node_ids, [{"Key": k, "Value": v}]) + + def create_node( + self, node_config: Dict[str, Any], tags: Dict[str, str], count: int + ) -> Optional[Dict[str, Any]]: + filter_tags = [ + { + "Key": TAG_RAY_CLUSTER_NAME, + "Value": self.cluster_name, + }, + {"Key": TAG_RAY_NODE_KIND, "Value": tags[TAG_RAY_NODE_KIND]}, + {"Key": TAG_RAY_USER_NODE_TYPE, "Value": tags[TAG_RAY_USER_NODE_TYPE]}, + {"Key": TAG_RAY_LAUNCH_CONFIG, "Value": tags[TAG_RAY_LAUNCH_CONFIG]}, + {"Key": TAG_RAY_NODE_NAME, "Value": tags[TAG_RAY_NODE_NAME]}, + ] + + reused_nodes_dict = {} + if self.cache_stopped_nodes: + reuse_nodes_candidate = self.acs.describe_instances(tags=filter_tags) + if reuse_nodes_candidate: + with cli_logger.group("Stopping instances to reuse"): + reuse_node_ids = [] + for node in reuse_nodes_candidate: + node_id = node.get("InstanceId") + status = node.get("Status") + if status != STOPPING and status != STOPPED: + continue + if status == STOPPING: + # wait for node stopped + while ( + self.acs.describe_instances(instance_ids=[node_id])[ + 0 + ].get("Status") + == STOPPING + ): + logging.info("wait for %s stop" % node_id) + time.sleep(STOPPING_NODE_DELAY) + # logger.info("reuse %s" % node_id) + reuse_node_ids.append(node_id) + reused_nodes_dict[node.get("InstanceId")] = node + self.acs.start_instance(node_id) + self.tag_cache[node_id] = node.get("Tags") + self.set_node_tags(node_id, tags) + if len(reuse_node_ids) == count: + break + count -= len(reuse_node_ids) + + created_nodes_dict = {} + if count > 0: + filter_tags.append( + {"Key": TAG_RAY_NODE_STATUS, "Value": tags[TAG_RAY_NODE_STATUS]} + ) + instance_id_sets = self.acs.run_instances( + instance_type=node_config["InstanceType"], + image_id=node_config["ImageId"], + tags=filter_tags, + amount=count, + vswitch_id=self.provider_config["v_switch_id"], + security_group_id=self.provider_config["security_group_id"], + key_pair_name=self.provider_config["key_name"], + ) + instances = self.acs.describe_instances(instance_ids=instance_id_sets) + + if instances is not None: + for instance in instances: + created_nodes_dict[instance.get("InstanceId")] = instance + + all_created_nodes = reused_nodes_dict + all_created_nodes.update(created_nodes_dict) + return all_created_nodes + + def terminate_node(self, node_id: str) -> None: + logger.info("terminate node: %s" % node_id) + if self.cache_stopped_nodes: + logger.info( + "Stopping instance {} (to terminate instead, " + "set `cache_stopped_nodes: False` " + "under `provider` in the cluster configuration)" + ).format(node_id) + self.acs.stop_instance(node_id) + else: + self.acs.delete_instance(node_id) + + def terminate_nodes(self, node_ids: List[str]) -> None: + if not node_ids: + return + if self.cache_stopped_nodes: + logger.info( + "Stopping instances {} (to terminate instead, " + "set `cache_stopped_nodes: False` " + "under `provider` in the cluster configuration)".format(node_ids) + ) + + self.acs.stop_instances(node_ids) + else: + self.acs.delete_instances(node_ids) + + def _get_node(self, node_id): + """Refresh and get info for this node, updating the cache.""" + self.non_terminated_nodes({}) # Side effect: updates cache + + if node_id in self.cached_nodes: + return self.cached_nodes[node_id] + + # Node not in {pending, running} -- retry with a point query. This + # usually means the node was recently preempted or terminated. + matches = self.acs.describe_instances(instance_ids=[node_id]) + + assert len(matches) == 1, "Invalid instance id {}".format(node_id) + return matches[0] + + def _get_cached_node(self, node_id): + """Return node info from cache if possible, otherwise fetches it.""" + if node_id in self.cached_nodes: + return self.cached_nodes[node_id] + + return self._get_node(node_id) + + @staticmethod + def bootstrap_config(cluster_config): + return bootstrap_aliyun(cluster_config) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/utils.py b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a1bb3c2710a7b73cd965f917d903c785a83bb1a3 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/utils.py @@ -0,0 +1,461 @@ +import json +import logging + +from aliyunsdkcore import client +from aliyunsdkcore.acs_exception.exceptions import ClientException, ServerException +from aliyunsdkecs.request.v20140526.AllocatePublicIpAddressRequest import ( + AllocatePublicIpAddressRequest, +) +from aliyunsdkecs.request.v20140526.AuthorizeSecurityGroupRequest import ( + AuthorizeSecurityGroupRequest, +) +from aliyunsdkecs.request.v20140526.CreateInstanceRequest import CreateInstanceRequest +from aliyunsdkecs.request.v20140526.CreateKeyPairRequest import CreateKeyPairRequest +from aliyunsdkecs.request.v20140526.CreateSecurityGroupRequest import ( + CreateSecurityGroupRequest, +) +from aliyunsdkecs.request.v20140526.CreateVpcRequest import CreateVpcRequest +from aliyunsdkecs.request.v20140526.CreateVSwitchRequest import CreateVSwitchRequest +from aliyunsdkecs.request.v20140526.DeleteInstanceRequest import DeleteInstanceRequest +from aliyunsdkecs.request.v20140526.DeleteInstancesRequest import DeleteInstancesRequest +from aliyunsdkecs.request.v20140526.DeleteKeyPairsRequest import DeleteKeyPairsRequest +from aliyunsdkecs.request.v20140526.DescribeInstancesRequest import ( + DescribeInstancesRequest, +) +from aliyunsdkecs.request.v20140526.DescribeKeyPairsRequest import ( + DescribeKeyPairsRequest, +) +from aliyunsdkecs.request.v20140526.DescribeSecurityGroupsRequest import ( + DescribeSecurityGroupsRequest, +) +from aliyunsdkecs.request.v20140526.DescribeVpcsRequest import DescribeVpcsRequest +from aliyunsdkecs.request.v20140526.DescribeVSwitchesRequest import ( + DescribeVSwitchesRequest, +) +from aliyunsdkecs.request.v20140526.ImportKeyPairRequest import ImportKeyPairRequest +from aliyunsdkecs.request.v20140526.RunInstancesRequest import RunInstancesRequest +from aliyunsdkecs.request.v20140526.StartInstanceRequest import StartInstanceRequest +from aliyunsdkecs.request.v20140526.StopInstanceRequest import StopInstanceRequest +from aliyunsdkecs.request.v20140526.StopInstancesRequest import StopInstancesRequest +from aliyunsdkecs.request.v20140526.TagResourcesRequest import TagResourcesRequest + + +class AcsClient: + """ + A wrapper around Aliyun SDK. We use this wrapper in aliyun node provider. + + Parameters: + access_key: The AccessKey ID of your aliyun account. + access_key_secret: The AccessKey secret of your aliyun account. + region_id: A region is a geographic area where a data center resides. + Region_id is the ID of region (e.g., cn-hangzhou, + us-west-1, etc.) + max_retries: The maximum number of retries each connection. + """ + + def __init__(self, access_key, access_key_secret, region_id, max_retries): + self.cli = client.AcsClient( + ak=access_key, + secret=access_key_secret, + max_retry_time=max_retries, + region_id=region_id, + ) + + def describe_instances(self, tags=None, instance_ids=None): + """Query the details of one or more Elastic Compute Service (ECS) instances. + + :param tags: The tags of the instance. + :param instance_ids: The IDs of ECS instances + :return: ECS instance list + """ + request = DescribeInstancesRequest() + if tags is not None: + request.set_Tags(tags) + if instance_ids is not None: + request.set_InstanceIds(instance_ids) + response = self._send_request(request) + if response is not None: + instance_list = response.get("Instances").get("Instance") + return instance_list + return None + + def create_instance( + self, + instance_type, + image_id, + tags, + key_pair_name, + optimized="optimized", + instance_charge_type="PostPaid", + spot_strategy="SpotWithPriceLimit", + internet_charge_type="PayByTraffic", + internet_max_bandwidth_out=5, + ): + """Create a subscription or pay-as-you-go ECS instance. + + :param instance_type: The instance type of the ECS. + :param image_id: The ID of the image used to create the instance. + :param tags: The tags of the instance. + :param key_pair_name: The name of the key pair to be bound to + the instance. + :param optimized: Specifies whether the instance is I/O optimized + :param instance_charge_type: The billing method of the instance. + Default value: PostPaid. + :param spot_strategy: The preemption policy for the pay-as-you-go + instance. + :param internet_charge_type: The billing method for network usage. + Default value: PayByTraffic. + :param internet_max_bandwidth_out: The maximum inbound public + bandwidth. Unit: Mbit/s. + :return: The created instance ID. + """ + request = CreateInstanceRequest() + request.set_InstanceType(instance_type) + request.set_ImageId(image_id) + request.set_IoOptimized(optimized) + request.set_InstanceChargeType(instance_charge_type) + request.set_SpotStrategy(spot_strategy) + request.set_InternetChargeType(internet_charge_type) + request.set_InternetMaxBandwidthOut(internet_max_bandwidth_out) + request.set_KeyPairName(key_pair_name) + request.set_Tags(tags) + + response = self._send_request(request) + if response is not None: + instance_id = response.get("InstanceId") + logging.info("instance %s created task submit successfully.", instance_id) + return instance_id + logging.error("instance created failed.") + return None + + def run_instances( + self, + instance_type, + image_id, + tags, + security_group_id, + vswitch_id, + key_pair_name, + amount=1, + optimized="optimized", + instance_charge_type="PostPaid", + spot_strategy="SpotWithPriceLimit", + internet_charge_type="PayByTraffic", + internet_max_bandwidth_out=1, + ): + """Create one or more pay-as-you-go or subscription + Elastic Compute Service (ECS) instances + + :param instance_type: The instance type of the ECS. + :param image_id: The ID of the image used to create the instance. + :param tags: The tags of the instance. + :param security_group_id: The ID of the security group to which to + assign the instance. Instances in the same + security group can communicate with + each other. + :param vswitch_id: The ID of the vSwitch to which to connect + the instance. + :param key_pair_name: The name of the key pair to be bound to + the instance. + :param amount: The number of instances that you want to create. + :param optimized: Specifies whether the instance is I/O optimized + :param instance_charge_type: The billing method of the instance. + Default value: PostPaid. + :param spot_strategy: The preemption policy for the pay-as-you-go + instance. + :param internet_charge_type: The billing method for network usage. + Default value: PayByTraffic. + :param internet_max_bandwidth_out: The maximum inbound public + bandwidth. Unit: Mbit/s. + :return: The created instance IDs. + """ + request = RunInstancesRequest() + request.set_InstanceType(instance_type) + request.set_ImageId(image_id) + request.set_IoOptimized(optimized) + request.set_InstanceChargeType(instance_charge_type) + request.set_SpotStrategy(spot_strategy) + request.set_InternetChargeType(internet_charge_type) + request.set_InternetMaxBandwidthOut(internet_max_bandwidth_out) + request.set_Tags(tags) + request.set_Amount(amount) + request.set_SecurityGroupId(security_group_id) + request.set_VSwitchId(vswitch_id) + request.set_KeyPairName(key_pair_name) + + response = self._send_request(request) + if response is not None: + instance_ids = response.get("InstanceIdSets").get("InstanceIdSet") + return instance_ids + logging.error("instance created failed.") + return None + + def create_security_group(self, vpc_id): + """Create a security group + + :param vpc_id: The ID of the VPC in which to create + the security group. + :return: The created security group ID. + """ + request = CreateSecurityGroupRequest() + request.set_VpcId(vpc_id) + response = self._send_request(request) + if response is not None: + security_group_id = response.get("SecurityGroupId") + return security_group_id + return None + + def describe_security_groups(self, vpc_id=None, tags=None): + """Query basic information of security groups. + + :param vpc_id: The ID of the VPC to which the security group belongs. + :param tags: The tags of the security group. + :return: Security group list. + """ + request = DescribeSecurityGroupsRequest() + if vpc_id is not None: + request.set_VpcId(vpc_id) + if tags is not None: + request.set_Tags(tags) + response = self._send_request(request) + if response is not None: + security_groups = response.get("SecurityGroups").get("SecurityGroup") + return security_groups + logging.error("describe security group failed.") + return None + + def authorize_security_group( + self, ip_protocol, port_range, security_group_id, source_cidr_ip + ): + """Create an inbound security group rule. + + :param ip_protocol: The transport layer protocol. + :param port_range: The range of destination ports relevant to + the transport layer protocol. + :param security_group_id: The ID of the destination security group. + :param source_cidr_ip: The range of source IPv4 addresses. + CIDR blocks and IPv4 addresses are supported. + """ + request = AuthorizeSecurityGroupRequest() + request.set_IpProtocol(ip_protocol) + request.set_PortRange(port_range) + request.set_SecurityGroupId(security_group_id) + request.set_SourceCidrIp(source_cidr_ip) + self._send_request(request) + + def create_v_switch(self, vpc_id, zone_id, cidr_block): + """Create vSwitches to divide the VPC into one or more subnets + + :param vpc_id: The ID of the VPC to which the VSwitch belongs. + :param zone_id: The ID of the zone to which + the target VSwitch belongs. + :param cidr_block: The CIDR block of the VSwitch. + :return: + """ + request = CreateVSwitchRequest() + request.set_ZoneId(zone_id) + request.set_VpcId(vpc_id) + request.set_CidrBlock(cidr_block) + response = self._send_request(request) + if response is not None: + return response.get("VSwitchId") + else: + logging.error("create_v_switch vpc_id %s failed.", vpc_id) + return None + + def create_vpc(self): + """Creates a virtual private cloud (VPC). + + :return: The created VPC ID. + """ + request = CreateVpcRequest() + response = self._send_request(request) + if response is not None: + return response.get("VpcId") + return None + + def describe_vpcs(self): + """Queries one or more VPCs in a region. + + :return: VPC list. + """ + request = DescribeVpcsRequest() + response = self._send_request(request) + if response is not None: + return response.get("Vpcs").get("Vpc") + return None + + def tag_resource(self, resource_ids, tags, resource_type="instance"): + """Create and bind tags to specified ECS resources. + + :param resource_ids: The IDs of N resources. + :param tags: The tags of the resource. + :param resource_type: The type of the resource. + """ + request = TagResourcesRequest() + request.set_Tags(tags) + request.set_ResourceType(resource_type) + request.set_ResourceIds(resource_ids) + response = self._send_request(request) + if response is not None: + logging.info("instance %s create tag successfully.", resource_ids) + else: + logging.error("instance %s create tag failed.", resource_ids) + + def start_instance(self, instance_id): + """Start an ECS instance. + + :param instance_id: The Ecs instance ID. + """ + request = StartInstanceRequest() + request.set_InstanceId(instance_id) + response = self._send_request(request) + + if response is not None: + logging.info("instance %s start successfully.", instance_id) + else: + logging.error("instance %s start failed.", instance_id) + + def stop_instance(self, instance_id, force_stop=False): + """Stop an ECS instance that is in the Running state. + + :param instance_id: The Ecs instance ID. + :param force_stop: Specifies whether to forcibly stop the instance. + :return: + """ + request = StopInstanceRequest() + request.set_InstanceId(instance_id) + request.set_ForceStop(force_stop) + logging.info("Stop %s command submit successfully.", instance_id) + self._send_request(request) + + def stop_instances(self, instance_ids, stopped_mode="StopCharging"): + """Stop one or more ECS instances that are in the Running state. + + :param instance_ids: The IDs of instances. + :param stopped_mode: Specifies whether billing for the instance + continues after the instance is stopped. + """ + request = StopInstancesRequest() + request.set_InstanceIds(instance_ids) + request.set_StoppedMode(stopped_mode) + response = self._send_request(request) + if response is None: + logging.error("stop_instances failed") + + def delete_instance(self, instance_id): + """Release a pay-as-you-go instance or + an expired subscription instance. + + :param instance_id: The ID of the instance that you want to release. + """ + request = DeleteInstanceRequest() + request.set_InstanceId(instance_id) + request.set_Force(True) + logging.info("Delete %s command submit successfully", instance_id) + self._send_request(request) + + def delete_instances(self, instance_ids): + """Release one or more pay-as-you-go instances or + expired subscription instances. + + :param instance_ids: The IDs of instances that you want to release. + """ + request = DeleteInstancesRequest() + request.set_Force(True) + request.set_InstanceIds(instance_ids) + self._send_request(request) + + def allocate_public_address(self, instance_id): + """Assign a public IP address to an ECS instance. + + :param instance_id: The ID of the instance to which you want to + assign a public IP address. + :return: The assigned ip. + """ + request = AllocatePublicIpAddressRequest() + request.set_InstanceId(instance_id) + response = self._send_request(request) + if response is not None: + return response.get("IpAddress") + + def create_key_pair(self, key_pair_name): + """Create an SSH key pair. + + :param key_pair_name: The name of the key pair. + :return: The created keypair data. + """ + request = CreateKeyPairRequest() + request.set_KeyPairName(key_pair_name) + response = self._send_request(request) + if response is not None: + logging.info("Create Key Pair %s Successfully", response.get("KeyPairId")) + return response + else: + logging.error("Create Key Pair Failed") + return None + + def import_key_pair(self, key_pair_name, public_key_body): + """Import the public key of an RSA-encrypted key pair + that is generated by a third-party tool. + + :param key_pair_name: The name of the key pair. + :param public_key_body: The public key of the key pair. + """ + request = ImportKeyPairRequest() + request.set_KeyPairName(key_pair_name) + request.set_PublicKeyBody(public_key_body) + self._send_request(request) + + def delete_key_pairs(self, key_pair_names): + """Delete one or more SSH key pairs. + + :param key_pair_names: The name of the key pair. + :return: + """ + request = DeleteKeyPairsRequest() + request.set_KeyPairNames(key_pair_names) + self._send_request(request) + + def describe_key_pairs(self, key_pair_name=None): + """Query one or more key pairs. + + :param key_pair_name: The name of the key pair. + :return: + """ + request = DescribeKeyPairsRequest() + if key_pair_name is not None: + request.set_KeyPairName(key_pair_name) + response = self._send_request(request) + if response is not None: + return response.get("KeyPairs").get("KeyPair") + else: + return None + + def describe_v_switches(self, vpc_id=None): + """Queries one or more VSwitches. + + :param vpc_id: The ID of the VPC to which the VSwitch belongs. + :return: VSwitch list. + """ + request = DescribeVSwitchesRequest() + if vpc_id is not None: + request.set_VpcId(vpc_id) + response = self._send_request(request) + if response is not None: + return response.get("VSwitches").get("VSwitch") + else: + logging.error("Describe VSwitches Failed.") + return None + + def _send_request(self, request): + """send open api request""" + request.set_accept_format("json") + try: + response_str = self.cli.do_action_with_exception(request) + response_detail = json.loads(response_str) + return response_detail + except (ClientException, ServerException) as e: + logging.error(request.get_action_name()) + logging.error(e) + return None diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/autoscaler.py b/lib/python3.12/site-packages/ray/autoscaler/_private/autoscaler.py new file mode 100644 index 0000000000000000000000000000000000000000..2051977bf655838d025c00bc602333f409bc1474 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/autoscaler.py @@ -0,0 +1,1522 @@ +import copy +import logging +import math +import operator +import os +import queue +import subprocess +import threading +import time +from collections import Counter, defaultdict, namedtuple +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Callable, Dict, FrozenSet, List, Optional, Set, Tuple, Union + +import yaml + +import ray +from ray._common.utils import PLACEMENT_GROUP_BUNDLE_RESOURCE_NAME +from ray.autoscaler._private.constants import ( + AUTOSCALER_HEARTBEAT_TIMEOUT_S, + AUTOSCALER_MAX_CONCURRENT_LAUNCHES, + AUTOSCALER_MAX_LAUNCH_BATCH, + AUTOSCALER_MAX_NUM_FAILURES, + AUTOSCALER_STATUS_LOG, + AUTOSCALER_UPDATE_INTERVAL_S, + DISABLE_LAUNCH_CONFIG_CHECK_KEY, + DISABLE_NODE_UPDATERS_KEY, + FOREGROUND_NODE_LAUNCH_KEY, + WORKER_LIVENESS_CHECK_KEY, +) +from ray.autoscaler._private.event_summarizer import EventSummarizer +from ray.autoscaler._private.legacy_info_string import legacy_log_info_string +from ray.autoscaler._private.load_metrics import LoadMetrics +from ray.autoscaler._private.local.node_provider import ( + LocalNodeProvider, + record_local_head_state_if_needed, +) +from ray.autoscaler._private.node_launcher import BaseNodeLauncher, NodeLauncher +from ray.autoscaler._private.node_provider_availability_tracker import ( + NodeAvailabilitySummary, + NodeProviderAvailabilityTracker, +) +from ray.autoscaler._private.node_tracker import NodeTracker +from ray.autoscaler._private.prom_metrics import AutoscalerPrometheusMetrics +from ray.autoscaler._private.providers import _get_node_provider +from ray.autoscaler._private.resource_demand_scheduler import ( + ResourceDemandScheduler, + ResourceDict, + get_bin_pack_residual, + placement_groups_to_resource_demands, +) +from ray.autoscaler._private.updater import NodeUpdaterThread +from ray.autoscaler._private.util import ( + ConcurrentCounter, + NodeCount, + NodeID, + NodeIP, + NodeType, + NodeTypeConfigDict, + format_info_string, + hash_launch_conf, + hash_runtime_conf, + validate_config, + with_head_node_ip, +) +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + NODE_KIND_UNMANAGED, + NODE_KIND_WORKER, + STATUS_UP_TO_DATE, + STATUS_UPDATE_FAILED, + TAG_RAY_FILE_MOUNTS_CONTENTS, + TAG_RAY_LAUNCH_CONFIG, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_STATUS, + TAG_RAY_RUNTIME_CONFIG, + TAG_RAY_USER_NODE_TYPE, +) +from ray.exceptions import RpcError + +logger = logging.getLogger(__name__) + +# Status of a node e.g. "up-to-date", see ray/autoscaler/tags.py +NodeStatus = str + +# Tuple of modified fields for the given node_id returned by should_update +# that will be passed into a NodeUpdaterThread. +UpdateInstructions = namedtuple( + "UpdateInstructions", + ["node_id", "setup_commands", "ray_start_commands", "docker_config"], +) + +NodeLaunchData = Tuple[NodeTypeConfigDict, NodeCount, Optional[NodeType]] + + +@dataclass +class AutoscalerSummary: + active_nodes: Dict[NodeType, int] + idle_nodes: Optional[Dict[NodeType, int]] + pending_nodes: List[Tuple[NodeIP, NodeType, NodeStatus]] + pending_launches: Dict[NodeType, int] + failed_nodes: List[Tuple[NodeIP, NodeType]] + node_availability_summary: NodeAvailabilitySummary = field( + default_factory=lambda: NodeAvailabilitySummary({}) + ) + # A dictionary of node IP to a list of reasons the node is not idle. + node_activities: Optional[Dict[str, Tuple[NodeIP, List[str]]]] = None + pending_resources: Dict[str, int] = field(default_factory=lambda: {}) + # A mapping from node name (the same key as `usage_by_node`) to node type. + # Optional for deployment modes which have the concept of node types and + # backwards compatibility. + node_type_mapping: Optional[Dict[str, str]] = None + # Whether the autoscaler summary is v1 or v2. + legacy: bool = False + + +class NonTerminatedNodes: + """Class to extract and organize information on non-terminated nodes.""" + + def __init__(self, provider: NodeProvider): + start_time = time.time() + # All non-terminated nodes + self.all_node_ids = provider.non_terminated_nodes({}) + + # Managed worker nodes (node kind "worker"): + self.worker_ids: List[NodeID] = [] + # The head node (node kind "head") + self.head_id: Optional[NodeID] = None + + for node in self.all_node_ids: + node_kind = provider.node_tags(node)[TAG_RAY_NODE_KIND] + if node_kind == NODE_KIND_WORKER: + self.worker_ids.append(node) + elif node_kind == NODE_KIND_HEAD: + self.head_id = node + + # Note: For typical use-cases, self.all_node_ids == self.worker_ids + + # [self.head_id]. The difference being in the case of unmanaged nodes. + + # Record the time of the non_terminated nodes call. This typically + # translates to a "describe" or "list" call on most cluster managers + # which can be quite expensive. Note that we include the processing + # time because on some clients, there may be pagination and the + # underlying api calls may be done lazily. + self.non_terminated_nodes_time = time.time() - start_time + logger.info( + f"The autoscaler took {round(self.non_terminated_nodes_time, 3)}" + " seconds to fetch the list of non-terminated nodes." + ) + + def remove_terminating_nodes(self, terminating_nodes: List[NodeID]) -> None: + """Remove nodes we're in the process of terminating from internal + state.""" + + def not_terminating(node): + return node not in terminating_nodes + + self.worker_ids = list(filter(not_terminating, self.worker_ids)) + self.all_node_ids = list(filter(not_terminating, self.all_node_ids)) + + +# Whether a worker should be kept based on the min_workers and +# max_workers constraints. +# +# keep: should keep the worker +# terminate: should terminate the worker +# decide_later: the worker can be terminated if needed +KeepOrTerminate = Enum("KeepOrTerminate", "keep terminate decide_later") + + +class StandardAutoscaler: + """The autoscaling control loop for a Ray cluster. + + There are two ways to start an autoscaling cluster: manually by running + `ray start --head --autoscaling-config=/path/to/config.yaml` on a instance + that has permission to launch other instances, or you can also use `ray up + /path/to/config.yaml` from your laptop, which will configure the right + AWS/Cloud roles automatically. See the Ray documentation + (https://docs.ray.io/en/latest/) for a full definition of autoscaling behavior. + StandardAutoscaler's `update` method is periodically called in + `monitor.py`'s monitoring loop. + + StandardAutoscaler is also used to bootstrap clusters (by adding workers + until the cluster size that can handle the resource demand is met). + """ + + def __init__( + self, + # TODO(ekl): require config reader to be a callable always. + config_reader: Union[str, Callable[[], dict]], + load_metrics: LoadMetrics, + gcs_client: "ray._raylet.GcsClient", + session_name: Optional[str] = None, + max_launch_batch: int = AUTOSCALER_MAX_LAUNCH_BATCH, + max_concurrent_launches: int = AUTOSCALER_MAX_CONCURRENT_LAUNCHES, + max_failures: int = AUTOSCALER_MAX_NUM_FAILURES, + process_runner: Any = subprocess, + update_interval_s: int = AUTOSCALER_UPDATE_INTERVAL_S, + prefix_cluster_info: bool = False, + event_summarizer: Optional[EventSummarizer] = None, + prom_metrics: Optional[AutoscalerPrometheusMetrics] = None, + ): + """Create a StandardAutoscaler. + + Args: + config_reader: Path to a Ray Autoscaler YAML, or a function to read + and return the latest config. + load_metrics: Provides metrics for the Ray cluster. + session_name: The current Ray session name when this autoscaler + is deployed. + max_launch_batch: Max number of nodes to launch in one request. + max_concurrent_launches: Max number of nodes that can be + concurrently launched. This value and `max_launch_batch` + determine the number of batches that are used to launch nodes. + max_failures: Number of failures that the autoscaler will tolerate + before exiting. + process_runner: Subproc-like interface used by the CommandRunner. + update_interval_s: Seconds between running the autoscaling loop. + prefix_cluster_info: Whether to add the cluster name to info strs. + event_summarizer: Utility to consolidate duplicated messages. + prom_metrics: Prometheus metrics for autoscaler-related operations. + gcs_client: client for interactions with the GCS. Used to drain nodes + before termination. + """ + + if isinstance(config_reader, str): + # Auto wrap with file reader. + def read_fn(): + with open(config_reader) as f: + new_config = yaml.safe_load(f.read()) + return new_config + + self.config_reader = read_fn + else: + self.config_reader = config_reader + + self.node_provider_availability_tracker = NodeProviderAvailabilityTracker() + # Prefix each line of info string with cluster name if True + self.prefix_cluster_info = prefix_cluster_info + # Keep this before self.reset (self.provider needs to be created + # exactly once). + self.provider = None + # Keep this before self.reset (if an exception occurs in reset + # then prom_metrics must be instantitiated to increment the + # exception counter) + self.prom_metrics = prom_metrics or AutoscalerPrometheusMetrics( + session_name=session_name + ) # noqa + self.resource_demand_scheduler = None + self.reset(errors_fatal=True) + self.load_metrics = load_metrics + + self.max_failures = max_failures + self.max_launch_batch = max_launch_batch + self.max_concurrent_launches = max_concurrent_launches + self.process_runner = process_runner + self.event_summarizer = event_summarizer or EventSummarizer() + + # Map from node_id to NodeUpdater threads + self.updaters: Dict[NodeID, NodeUpdaterThread] = {} + self.num_failed_updates: Dict[NodeID, int] = defaultdict(int) + self.num_successful_updates: Dict[NodeID, int] = defaultdict(int) + self.num_failures = 0 + self.last_update_time = 0.0 + self.update_interval_s = update_interval_s + + # Keeps track of pending and running nodes + self.non_terminated_nodes: Optional[NonTerminatedNodes] = None + + # Tracks nodes scheduled for termination + self.nodes_to_terminate: List[NodeID] = [] + + # Disable NodeUpdater threads if true. + # Should be set to true in situations where another component, such as + # a Kubernetes operator, is responsible for Ray setup on nodes. + self.disable_node_updaters = self.config["provider"].get( + DISABLE_NODE_UPDATERS_KEY, False + ) + logger.info(f"{DISABLE_NODE_UPDATERS_KEY}:{self.disable_node_updaters}") + + # Disable launch configuration checking if set to true. + # This setting is used in scenarios where there is no meaningful node type + # to enforce, such as in fake multinode situations. When this option is enabled, + # outdated nodes will not be terminated. + self.disable_launch_config_check = self.config["provider"].get( + DISABLE_LAUNCH_CONFIG_CHECK_KEY, False + ) + logger.info( + f"{DISABLE_LAUNCH_CONFIG_CHECK_KEY}:{self.disable_launch_config_check}" + ) + + # By default, the autoscaler launches nodes in batches asynchronously in + # background threads. + # When the following flag is set, that behavior is disabled, so that nodes + # are launched in the main thread, all in one batch, blocking until all + # NodeProvider.create_node calls have returned. + self.foreground_node_launch = self.config["provider"].get( + FOREGROUND_NODE_LAUNCH_KEY, False + ) + logger.info(f"{FOREGROUND_NODE_LAUNCH_KEY}:{self.foreground_node_launch}") + + # By default, the autoscaler kills and/or tries to recover + # a worker node if it hasn't produced a resource heartbeat in the last 30 + # seconds. The worker_liveness_check flag allows disabling this behavior in + # settings where another component, such as a Kubernetes operator, is + # responsible for healthchecks. + self.worker_liveness_check = self.config["provider"].get( + WORKER_LIVENESS_CHECK_KEY, True + ) + logger.info(f"{WORKER_LIVENESS_CHECK_KEY}:{self.worker_liveness_check}") + + # Node launchers + self.foreground_node_launcher: Optional[BaseNodeLauncher] = None + self.launch_queue: Optional[queue.Queue[NodeLaunchData]] = None + self.pending_launches = ConcurrentCounter() + if self.foreground_node_launch: + self.foreground_node_launcher = BaseNodeLauncher( + provider=self.provider, + pending=self.pending_launches, + event_summarizer=self.event_summarizer, + node_provider_availability_tracker=self.node_provider_availability_tracker, # noqa: E501 Flake and black disagree how to format this. + session_name=session_name, + node_types=self.available_node_types, + prom_metrics=self.prom_metrics, + ) + else: + self.launch_queue = queue.Queue() + max_batches = math.ceil(max_concurrent_launches / float(max_launch_batch)) + for i in range(int(max_batches)): + node_launcher = NodeLauncher( + provider=self.provider, + queue=self.launch_queue, + index=i, + pending=self.pending_launches, + event_summarizer=self.event_summarizer, + node_provider_availability_tracker=self.node_provider_availability_tracker, # noqa: E501 Flake and black disagreee how to format this. + session_name=session_name, + node_types=self.available_node_types, + prom_metrics=self.prom_metrics, + ) + node_launcher.daemon = True + node_launcher.start() + + # NodeTracker maintains soft state to track the number of recently + # failed nodes. It is best effort only. + self.node_tracker = NodeTracker() + + # Expand local file_mounts to allow ~ in the paths. This can't be done + # earlier when the config is written since we might be on different + # platform and the expansion would result in wrong path. + self.config["file_mounts"] = { + remote: os.path.expanduser(local) + for remote, local in self.config["file_mounts"].items() + } + + self.gcs_client = gcs_client + + for local_path in self.config["file_mounts"].values(): + assert os.path.exists(local_path) + logger.info("StandardAutoscaler: {}".format(self.config)) + + @property + def all_node_types(self) -> Set[str]: + return self.config["available_node_types"].keys() + + def update(self): + try: + self.reset(errors_fatal=False) + self._update() + except Exception as e: + self.prom_metrics.update_loop_exceptions.inc() + logger.exception("StandardAutoscaler: Error during autoscaling.") + self.num_failures += 1 + if self.num_failures > self.max_failures: + logger.critical("StandardAutoscaler: Too many errors, abort.") + raise e + + def _update(self): + # For type checking, assert that these objects have been instantitiated. + assert self.provider + assert self.resource_demand_scheduler + + now = time.time() + # Throttle autoscaling updates to this interval to avoid exceeding + # rate limits on API calls. + if now - self.last_update_time < self.update_interval_s: + return + + self.last_update_time = now + + # Query the provider to update the list of non-terminated nodes + self.non_terminated_nodes = NonTerminatedNodes(self.provider) + + # Back off the update if the provider says it's not safe to proceed. + if not self.provider.safe_to_scale(): + logger.info( + "Backing off of autoscaler update." + f" Will try again in {self.update_interval_s} seconds." + ) + return + + # This will accumulate the nodes we need to terminate. + self.nodes_to_terminate = [] + + # Update status strings + if AUTOSCALER_STATUS_LOG: + logger.info(self.info_string()) + legacy_log_info_string(self, self.non_terminated_nodes.worker_ids) + + if not self.provider.is_readonly(): + self.terminate_nodes_to_enforce_config_constraints(now) + + if self.disable_node_updaters: + # Don't handle unhealthy nodes if the liveness check is disabled. + # self.worker_liveness_check is True by default. + if self.worker_liveness_check: + self.terminate_unhealthy_nodes(now) + else: + self.process_completed_updates() + self.update_nodes() + # Don't handle unhealthy nodes if the liveness check is disabled. + # self.worker_liveness_check is True by default. + if self.worker_liveness_check: + self.attempt_to_recover_unhealthy_nodes(now) + self.set_prometheus_updater_data() + + # Update running nodes gauge + num_workers = len(self.non_terminated_nodes.worker_ids) + self.prom_metrics.running_workers.set(num_workers) + + # Remove IPs from LoadMetrics that are not known to the NodeProvider. + self.load_metrics.prune_active_ips( + active_ips=[ + self.provider.internal_ip(node_id) + for node_id in self.non_terminated_nodes.all_node_ids + ] + ) + + # Dict[NodeType, int], List[ResourceDict] + to_launch, unfulfilled = self.resource_demand_scheduler.get_nodes_to_launch( + self.non_terminated_nodes.all_node_ids, + self.pending_launches.breakdown(), + self.load_metrics.get_resource_demand_vector(), + self.load_metrics.get_resource_utilization(), + self.load_metrics.get_pending_placement_groups(), + self.load_metrics.get_static_node_resources_by_ip(), + ensure_min_cluster_size=self.load_metrics.get_resource_requests(), + node_availability_summary=self.node_provider_availability_tracker.summary(), + ) + self._report_pending_infeasible(unfulfilled) + + if not self.provider.is_readonly(): + self.launch_required_nodes(to_launch) + + # Execute optional end-of-update logic. + # Keep this method call at the end of autoscaler._update(). + self.provider.post_process() + + # Record the amount of time the autoscaler took for + # this _update() iteration. + update_time = time.time() - self.last_update_time + logger.info( + f"The autoscaler took {round(update_time, 3)}" + " seconds to complete the update iteration." + ) + self.prom_metrics.update_time.observe(update_time) + + def terminate_nodes_to_enforce_config_constraints(self, now: float): + """Terminates nodes to enforce constraints defined by the autoscaling + config. + + (1) Terminates nodes in excess of `max_workers`. + (2) Terminates nodes idle for longer than `idle_timeout_minutes`. + (3) Terminates outdated nodes, + namely nodes whose configs don't match `node_config` for the + relevant node type. + + Avoids terminating non-outdated nodes required by + autoscaler.sdk.request_resources(). + """ + # For type checking, assert that these objects have been instantitiated. + assert self.non_terminated_nodes + assert self.provider + + last_used = self.load_metrics.ray_nodes_last_used_time_by_ip + + idle_timeout_s = 60 * self.config["idle_timeout_minutes"] + + last_used_cutoff = now - idle_timeout_s + + # Sort based on last used to make sure to keep min_workers that + # were most recently used. Otherwise, _keep_min_workers_of_node_type + # might keep a node that should be terminated. + sorted_node_ids = self._sort_based_on_last_used( + self.non_terminated_nodes.worker_ids, last_used + ) + + # Don't terminate nodes needed by request_resources() + nodes_not_allowed_to_terminate = self._get_nodes_needed_for_request_resources( + sorted_node_ids + ) + + # Tracks counts of nodes we intend to keep for each node type. + node_type_counts = defaultdict(int) + + def keep_node(node_id: NodeID) -> None: + assert self.provider + # Update per-type counts. + tags = self.provider.node_tags(node_id) + if TAG_RAY_USER_NODE_TYPE in tags: + node_type = tags[TAG_RAY_USER_NODE_TYPE] + node_type_counts[node_type] += 1 + + # Nodes that we could terminate, if needed. + nodes_we_could_terminate: List[NodeID] = [] + + for node_id in sorted_node_ids: + # Make sure to not kill idle node types if the number of workers + # of that type is lower/equal to the min_workers of that type + # or it is needed for request_resources(). + should_keep_or_terminate, reason = self._keep_worker_of_node_type( + node_id, node_type_counts + ) + if should_keep_or_terminate == KeepOrTerminate.terminate: + self.schedule_node_termination(node_id, reason, logger.info) + continue + if ( + should_keep_or_terminate == KeepOrTerminate.keep + or node_id in nodes_not_allowed_to_terminate + ) and self.launch_config_ok(node_id): + keep_node(node_id) + continue + + node_ip = self.provider.internal_ip(node_id) + + if node_ip in last_used and last_used[node_ip] < last_used_cutoff: + self.schedule_node_termination(node_id, "idle", logger.info) + # Get the local time of the node's last use as a string. + formatted_last_used_time = time.asctime( + time.localtime(last_used[node_ip]) + ) + logger.info(f"Node last used: {formatted_last_used_time}.") + # Note that the current time will appear in the log prefix. + elif not self.launch_config_ok(node_id): + self.schedule_node_termination(node_id, "outdated", logger.info) + else: + keep_node(node_id) + nodes_we_could_terminate.append(node_id) + + # Terminate nodes if there are too many + num_workers = len(self.non_terminated_nodes.worker_ids) + num_extra_nodes_to_terminate = ( + num_workers - len(self.nodes_to_terminate) - self.config["max_workers"] + ) + + if num_extra_nodes_to_terminate > len(nodes_we_could_terminate): + logger.warning( + "StandardAutoscaler: trying to terminate " + f"{num_extra_nodes_to_terminate} nodes, while only " + f"{len(nodes_we_could_terminate)} are safe to terminate." + " Inconsistent config is likely." + ) + num_extra_nodes_to_terminate = len(nodes_we_could_terminate) + + # If num_extra_nodes_to_terminate is negative or zero, + # we would have less than max_workers nodes after terminating + # nodes_to_terminate and we do not need to terminate anything else. + if num_extra_nodes_to_terminate > 0: + extra_nodes_to_terminate = nodes_we_could_terminate[ + -num_extra_nodes_to_terminate: + ] + for node_id in extra_nodes_to_terminate: + self.schedule_node_termination(node_id, "max workers", logger.info) + + self.terminate_scheduled_nodes() + + def schedule_node_termination( + self, node_id: NodeID, reason_opt: Optional[str], logger_method: Callable + ) -> None: + # For type checking, assert that this object has been instantitiated. + assert self.provider + + if reason_opt is None: + raise Exception("reason should be not None.") + reason: str = reason_opt + node_ip = self.provider.internal_ip(node_id) + # Log, record an event, and add node_id to nodes_to_terminate. + logger_method( + "StandardAutoscaler: " + f"Terminating the node with id {node_id}" + f" and ip {node_ip}." + f" ({reason})" + ) + self.event_summarizer.add( + "Removing {} nodes of type " + + self._get_node_type(node_id) + + " ({}).".format(reason), + quantity=1, + aggregate=operator.add, + ) + self.nodes_to_terminate.append(node_id) + + def terminate_scheduled_nodes(self): + """Terminate scheduled nodes and clean associated autoscaler state.""" + # For type checking, assert that these objects have been instantitiated. + assert self.provider + assert self.non_terminated_nodes + + if not self.nodes_to_terminate: + return + + # Drain the nodes + self.drain_nodes_via_gcs(self.nodes_to_terminate) + # Terminate the nodes + self.provider.terminate_nodes(self.nodes_to_terminate) + for node in self.nodes_to_terminate: + self.node_tracker.untrack(node) + self.prom_metrics.stopped_nodes.inc() + + # Update internal node lists + self.non_terminated_nodes.remove_terminating_nodes(self.nodes_to_terminate) + + self.nodes_to_terminate = [] + + def drain_nodes_via_gcs(self, provider_node_ids_to_drain: List[NodeID]): + """Send an RPC request to the GCS to drain (prepare for termination) + the nodes with the given node provider ids. + + note: The current implementation of DrainNode on the GCS side is to + de-register and gracefully shut down the Raylets. In the future, + the behavior may change to better reflect the name "Drain." + See https://github.com/ray-project/ray/pull/19350. + """ + # For type checking, assert that this object has been instantitiated. + assert self.provider + + # The GCS expects Node ids in the request, rather than NodeProvider + # ids. To get the Node ids of the nodes to we're draining, we make + # the following translations of identifiers: + # node provider node id -> ip -> node id + + # Convert node provider node ids to ips. + node_ips = set() + failed_ip_fetch = False + for provider_node_id in provider_node_ids_to_drain: + # If the provider's call to fetch ip fails, the exception is not + # fatal. Log the exception and proceed. + try: + ip = self.provider.internal_ip(provider_node_id) + node_ips.add(ip) + except Exception: + logger.exception( + "Failed to get ip of node with id" + f" {provider_node_id} during scale-down." + ) + failed_ip_fetch = True + if failed_ip_fetch: + self.prom_metrics.drain_node_exceptions.inc() + + # Only attempt to drain connected nodes, i.e. nodes with ips in + # LoadMetrics. + connected_node_ips = node_ips & self.load_metrics.node_id_by_ip.keys() + + # Convert ips to Node ids. + # (The assignment ip->node_id is well-defined under current + # assumptions. See "use_node_id_as_ip" in monitor.py) + node_ids_to_drain = { + self.load_metrics.node_id_by_ip[ip] for ip in connected_node_ips + } + + if not node_ids_to_drain: + return + + logger.info(f"Draining {len(node_ids_to_drain)} raylet(s).") + try: + # A successful response indicates that the GCS has marked the + # desired nodes as "drained." The cloud provider can then terminate + # the nodes without the GCS printing an error. + # Check if we succeeded in draining all of the intended nodes by + # looking at the RPC response. + drained_node_ids = set( + self.gcs_client.drain_nodes(node_ids_to_drain, timeout=5) + ) + failed_to_drain = node_ids_to_drain - drained_node_ids + if failed_to_drain: + self.prom_metrics.drain_node_exceptions.inc() + logger.error(f"Failed to drain {len(failed_to_drain)} raylet(s).") + # If we get a gRPC error with an UNIMPLEMENTED code, fail silently. + # This error indicates that the GCS is using Ray version < 1.8.0, + # for which DrainNode is not implemented. + except RpcError as e: + # If the code is UNIMPLEMENTED, pass. + if e.rpc_code == ray._raylet.GRPC_STATUS_CODE_UNIMPLEMENTED: + pass + # Otherwise, it's a plain old gRPC error and we should log it. + else: + self.prom_metrics.drain_node_exceptions.inc() + logger.exception("Failed to drain Ray nodes. Traceback follows.") + except Exception: + # We don't need to interrupt the autoscaler update with an + # exception, but we should log what went wrong and record the + # failure in Prometheus. + self.prom_metrics.drain_node_exceptions.inc() + logger.exception("Failed to drain Ray nodes. Traceback follows.") + + def launch_required_nodes(self, to_launch: Dict[NodeType, int]) -> None: + if to_launch: + for node_type, count in to_launch.items(): + self.launch_new_node(count, node_type=node_type) + + def update_nodes(self): + """Run NodeUpdaterThreads to run setup commands, sync files, + and/or start Ray. + """ + # Update nodes with out-of-date files. + # TODO(edoakes): Spawning these threads directly seems to cause + # problems. They should at a minimum be spawned as daemon threads. + # See https://github.com/ray-project/ray/pull/5903 for more info. + T = [] + for node_id, setup_commands, ray_start_commands, docker_config in ( + self.should_update(node_id) + for node_id in self.non_terminated_nodes.worker_ids + ): + if node_id is not None: + resources = self._node_resources(node_id) + labels = self._node_labels(node_id) + logger.debug(f"{node_id}: Starting new thread runner.") + T.append( + threading.Thread( + target=self.spawn_updater, + args=( + node_id, + setup_commands, + ray_start_commands, + resources, + labels, + docker_config, + ), + ) + ) + for t in T: + t.start() + for t in T: + t.join() + + def process_completed_updates(self): + """Clean up completed NodeUpdaterThreads.""" + completed_nodes = [] + for node_id, updater in self.updaters.items(): + if not updater.is_alive(): + completed_nodes.append(node_id) + if completed_nodes: + failed_nodes = [] + for node_id in completed_nodes: + updater = self.updaters[node_id] + if updater.exitcode == 0: + self.num_successful_updates[node_id] += 1 + self.prom_metrics.successful_updates.inc() + if updater.for_recovery: + self.prom_metrics.successful_recoveries.inc() + if updater.update_time: + self.prom_metrics.worker_update_time.observe( + updater.update_time + ) + # Mark the node as active to prevent the node recovery + # logic immediately trying to restart Ray on the new node. + self.load_metrics.mark_active(self.provider.internal_ip(node_id)) + else: + failed_nodes.append(node_id) + self.num_failed_updates[node_id] += 1 + self.prom_metrics.failed_updates.inc() + if updater.for_recovery: + self.prom_metrics.failed_recoveries.inc() + self.node_tracker.untrack(node_id) + del self.updaters[node_id] + + if failed_nodes: + # Some nodes in failed_nodes may already have been terminated + # during an update (for being idle after missing a heartbeat). + + # Update the list of non-terminated workers. + for node_id in failed_nodes: + # Check if the node has already been terminated. + if node_id in self.non_terminated_nodes.worker_ids: + self.schedule_node_termination( + node_id, "launch failed", logger.error + ) + else: + logger.warning( + f"StandardAutoscaler: {node_id}:" + " Failed to update node." + " Node has already been terminated." + ) + self.terminate_scheduled_nodes() + + def set_prometheus_updater_data(self): + """Record total number of active NodeUpdaterThreads and how many of + these are being run to recover nodes. + """ + self.prom_metrics.updating_nodes.set(len(self.updaters)) + num_recovering = 0 + for updater in self.updaters.values(): + if updater.for_recovery: + num_recovering += 1 + self.prom_metrics.recovering_nodes.set(num_recovering) + + def _report_pending_infeasible(self, unfulfilled: List[ResourceDict]): + """Emit event messages for infeasible or unschedulable tasks. + + This adds messages to the event summarizer for warning on infeasible + or "cluster full" resource requests. + + Args: + unfulfilled: List of resource demands that would be unfulfilled + even after full scale-up. + """ + # For type checking, assert that this object has been instantitiated. + assert self.resource_demand_scheduler + pending = [] + infeasible = [] + for bundle in unfulfilled: + placement_group = any( + "_group_" in k or k == PLACEMENT_GROUP_BUNDLE_RESOURCE_NAME + for k in bundle + ) + if placement_group: + continue + if self.resource_demand_scheduler.is_feasible(bundle): + pending.append(bundle) + else: + infeasible.append(bundle) + if pending: + if self.load_metrics.cluster_full_of_actors_detected: + for request in pending: + self.event_summarizer.add_once_per_interval( + "Warning: The following resource request cannot be " + "scheduled right now: {}. This is likely due to all " + "cluster resources being claimed by actors. Consider " + "creating fewer actors or adding more nodes " + "to this Ray cluster.".format(request), + key="pending_{}".format(sorted(request.items())), + interval_s=30, + ) + if infeasible: + for request in infeasible: + self.event_summarizer.add_once_per_interval( + "Error: No available node types can fulfill resource " + "request {}. Add suitable node types to this cluster to " + "resolve this issue.".format(request), + key="infeasible_{}".format(sorted(request.items())), + interval_s=30, + ) + + def _sort_based_on_last_used( + self, nodes: List[NodeID], last_used: Dict[str, float] + ) -> List[NodeID]: + """Sort the nodes based on the last time they were used. + + The first item in the return list is the most recently used. + """ + last_used_copy = copy.deepcopy(last_used) + # Add the unconnected nodes as the least recently used (the end of + # list). This prioritizes connected nodes. + least_recently_used = -1 + + def last_time_used(node_id: NodeID): + assert self.provider + node_ip = self.provider.internal_ip(node_id) + if node_ip not in last_used_copy: + return least_recently_used + else: + return last_used_copy[node_ip] + + return sorted(nodes, key=last_time_used, reverse=True) + + def _get_nodes_needed_for_request_resources( + self, sorted_node_ids: List[NodeID] + ) -> FrozenSet[NodeID]: + # TODO(ameer): try merging this with resource_demand_scheduler + # code responsible for adding nodes for get_resource_requests() and get_pending_placement_groups(). + """Returns the nodes NOT allowed to terminate due to get_resource_requests() and get_pending_placement_groups(). + + Args: + sorted_node_ids: the node ids sorted based on last used (LRU last). + + Returns: + FrozenSet[NodeID]: a set of nodes (node ids) that + we should NOT terminate. + """ + # For type checking, assert that this object has been instantitiated. + assert self.provider + + nodes_not_allowed_to_terminate: Set[NodeID] = set() + + resource_demands, strict_spreads = placement_groups_to_resource_demands( + self.load_metrics.get_pending_placement_groups() + ) + resource_demands.extend(self.load_metrics.get_resource_requests()) + if not resource_demands and not strict_spreads: + return frozenset(nodes_not_allowed_to_terminate) + + static_node_resources: Dict[ + NodeIP, ResourceDict + ] = self.load_metrics.get_static_node_resources_by_ip() + + head_node_resources: ResourceDict = copy.deepcopy( + self.available_node_types[self.config["head_node_type"]]["resources"] + ) + # TODO(ameer): this is somewhat duplicated in + # resource_demand_scheduler.py. + if not head_node_resources: + # Legacy yaml might include {} in the resources field. + head_node_ip = self.provider.internal_ip(self.non_terminated_nodes.head_id) + head_node_resources = static_node_resources.get(head_node_ip, {}) + + node_total_resources: List[ResourceDict] = [head_node_resources] + resource_demand_vector_worker_node_ids = [] + # Get max resources on all the non terminated nodes. + for node_id in sorted_node_ids: + tags = self.provider.node_tags(node_id) + if TAG_RAY_USER_NODE_TYPE in tags: + node_type = tags[TAG_RAY_USER_NODE_TYPE] + node_resources: ResourceDict = copy.deepcopy( + self.available_node_types[node_type]["resources"] + ) + if not node_resources: + # Legacy yaml might include {} in the resources field. + node_ip = self.provider.internal_ip(node_id) + node_resources = static_node_resources.get(node_ip, {}) + node_total_resources.append(node_resources) + resource_demand_vector_worker_node_ids.append(node_id) + # Since it is sorted based on last used, we "keep" nodes that are + # most recently used when we binpack. We assume get_bin_pack_residual + # is following the given order here. + node_remaining_resources = copy.deepcopy(node_total_resources) + + for strict_spread in strict_spreads: + unfulfilled, updated_node_remaining_resources = get_bin_pack_residual( + node_remaining_resources, strict_spread, strict_spread=True + ) + if unfulfilled: + continue + node_remaining_resources = updated_node_remaining_resources + + _, node_remaining_resources = get_bin_pack_residual( + node_remaining_resources, resource_demands + ) + # Remove the first entry (the head node). + node_total_resources.pop(0) + # Remove the first entry (the head node). + node_remaining_resources.pop(0) + for i, node_id in enumerate(resource_demand_vector_worker_node_ids): + if ( + node_remaining_resources[i] == node_total_resources[i] + and node_total_resources[i] + ): + # No resources of the node were needed for request_resources(). + # node_total_resources[i] is an empty dict for legacy yamls + # before the node is connected. + pass + else: + nodes_not_allowed_to_terminate.add(node_id) + return frozenset(nodes_not_allowed_to_terminate) + + def _keep_worker_of_node_type( + self, node_id: NodeID, node_type_counts: Dict[NodeType, int] + ) -> Tuple[KeepOrTerminate, Optional[str]]: + """Determines if a worker should be kept based on the min_workers + and max_workers constraint of the worker's node_type. + + Returns KeepOrTerminate.keep when both of the following hold: + (a) The worker's node_type is present among the keys of the current + config's available_node_types dict. + (b) Deleting the node would violate the min_workers constraint for that + worker's node_type. + + Returns KeepOrTerminate.terminate when both the following hold: + (a) The worker's node_type is not present among the keys of the current + config's available_node_types dict. + (b) Keeping the node would violate the max_workers constraint for that + worker's node_type. + + Return KeepOrTerminate.decide_later otherwise. + + Args: + node_type_counts(Dict[NodeType, int]): The non_terminated node + types counted so far. + Returns: + KeepOrTerminate: keep if the node should be kept, terminate if the + node should be terminated, decide_later if we are allowed + to terminate it, but do not have to. + Optional[str]: reason for termination. Not None on + KeepOrTerminate.terminate, None otherwise. + """ + # For type checking, assert that this object has been instantitiated. + assert self.provider + + tags = self.provider.node_tags(node_id) + if TAG_RAY_USER_NODE_TYPE in tags: + node_type = tags[TAG_RAY_USER_NODE_TYPE] + + min_workers = self.available_node_types.get(node_type, {}).get( + "min_workers", 0 + ) + max_workers = self.available_node_types.get(node_type, {}).get( + "max_workers", 0 + ) + if node_type not in self.available_node_types: + # The node type has been deleted from the cluster config. + # Allow terminating it if needed. + available_node_types = list(self.available_node_types.keys()) + return ( + KeepOrTerminate.terminate, + f"not in available_node_types: {available_node_types}", + ) + new_count = node_type_counts[node_type] + 1 + if new_count <= min(min_workers, max_workers): + return KeepOrTerminate.keep, None + if new_count > max_workers: + return KeepOrTerminate.terminate, "max_workers_per_type" + + return KeepOrTerminate.decide_later, None + + def _node_resources(self, node_id): + node_type = self.provider.node_tags(node_id).get(TAG_RAY_USER_NODE_TYPE) + if self.available_node_types: + return self.available_node_types.get(node_type, {}).get("resources", {}) + else: + return {} + + def _node_labels(self, node_id): + node_type = self.provider.node_tags(node_id).get(TAG_RAY_USER_NODE_TYPE) + if self.available_node_types: + return self.available_node_types.get(node_type, {}).get("labels", {}) + else: + return {} + + def reset(self, errors_fatal=False): + sync_continuously = False + if hasattr(self, "config"): + sync_continuously = self.config.get("file_mounts_sync_continuously", False) + try: + new_config = self.config_reader() + if new_config != getattr(self, "config", None): + try: + validate_config(new_config) + except Exception as e: + self.prom_metrics.config_validation_exceptions.inc() + logger.debug( + "Cluster config validation failed. The version of " + "the ray CLI you launched this cluster with may " + "be higher than the version of ray being run on " + "the cluster. Some new features may not be " + "available until you upgrade ray on your cluster.", + exc_info=e, + ) + logger.debug( + f"New config after validation: {new_config}," + f" of type: {type(new_config)}" + ) + (new_runtime_hash, new_file_mounts_contents_hash) = hash_runtime_conf( + new_config["file_mounts"], + new_config["cluster_synced_files"], + [ + new_config["worker_setup_commands"], + new_config["worker_start_ray_commands"], + ], + generate_file_mounts_contents_hash=sync_continuously, + ) + self.config = new_config + self.runtime_hash = new_runtime_hash + self.file_mounts_contents_hash = new_file_mounts_contents_hash + if not self.provider: + self.provider = _get_node_provider( + self.config["provider"], self.config["cluster_name"] + ) + + # If using the LocalNodeProvider, make sure the head node is marked + # non-terminated. + if isinstance(self.provider, LocalNodeProvider): + record_local_head_state_if_needed(self.provider) + + self.available_node_types = self.config["available_node_types"] + upscaling_speed = self.config.get("upscaling_speed") + aggressive = self.config.get("autoscaling_mode") == "aggressive" + target_utilization_fraction = self.config.get("target_utilization_fraction") + if upscaling_speed: + upscaling_speed = float(upscaling_speed) + # TODO(ameer): consider adding (if users ask) an option of + # initial_upscaling_num_workers. + elif aggressive: + upscaling_speed = 99999 + logger.warning( + "Legacy aggressive autoscaling mode " + "detected. Replacing it by setting upscaling_speed to " + "99999." + ) + elif target_utilization_fraction: + upscaling_speed = 1 / max(target_utilization_fraction, 0.001) - 1 + logger.warning( + "Legacy target_utilization_fraction config " + "detected. Replacing it by setting upscaling_speed to " + + "1 / target_utilization_fraction - 1." + ) + else: + upscaling_speed = 1.0 + if self.resource_demand_scheduler: + # The node types are autofilled internally for legacy yamls, + # overwriting the class will remove the inferred node resources + # for legacy yamls. + self.resource_demand_scheduler.reset_config( + self.provider, + self.available_node_types, + self.config["max_workers"], + self.config["head_node_type"], + upscaling_speed, + ) + else: + self.resource_demand_scheduler = ResourceDemandScheduler( + self.provider, + self.available_node_types, + self.config["max_workers"], + self.config["head_node_type"], + upscaling_speed, + ) + + except Exception as e: + self.prom_metrics.reset_exceptions.inc() + if errors_fatal: + raise e + else: + logger.exception("StandardAutoscaler: Error parsing config.") + + def launch_config_ok(self, node_id): + if self.disable_launch_config_check: + return True + node_tags = self.provider.node_tags(node_id) + tag_launch_conf = node_tags.get(TAG_RAY_LAUNCH_CONFIG) + node_type = node_tags.get(TAG_RAY_USER_NODE_TYPE) + if node_type not in self.available_node_types: + # The node type has been deleted from the cluster config. + # Don't keep the node. + return False + + # The `worker_nodes` field is deprecated in favor of per-node-type + # node_configs. We allow it for backwards-compatibility. + launch_config = copy.deepcopy(self.config.get("worker_nodes", {})) + if node_type: + launch_config.update( + self.config["available_node_types"][node_type]["node_config"] + ) + calculated_launch_hash = hash_launch_conf(launch_config, self.config["auth"]) + + if calculated_launch_hash != tag_launch_conf: + return False + return True + + def files_up_to_date(self, node_id): + node_tags = self.provider.node_tags(node_id) + applied_config_hash = node_tags.get(TAG_RAY_RUNTIME_CONFIG) + applied_file_mounts_contents_hash = node_tags.get(TAG_RAY_FILE_MOUNTS_CONTENTS) + if applied_config_hash != self.runtime_hash or ( + self.file_mounts_contents_hash is not None + and self.file_mounts_contents_hash != applied_file_mounts_contents_hash + ): + logger.info( + "StandardAutoscaler: " + "{}: Runtime state is ({},{}), want ({},{})".format( + node_id, + applied_config_hash, + applied_file_mounts_contents_hash, + self.runtime_hash, + self.file_mounts_contents_hash, + ) + ) + return False + return True + + def heartbeat_on_time(self, node_id: NodeID, now: float) -> bool: + """Determine whether we've received a heartbeat from a node within the + last AUTOSCALER_HEARTBEAT_TIMEOUT_S seconds. + """ + # For type checking, assert that this object has been instantitiated. + assert self.provider + + key = self.provider.internal_ip(node_id) + + if key in self.load_metrics.last_heartbeat_time_by_ip: + last_heartbeat_time = self.load_metrics.last_heartbeat_time_by_ip[key] + delta = now - last_heartbeat_time + if delta < AUTOSCALER_HEARTBEAT_TIMEOUT_S: + return True + return False + + def terminate_unhealthy_nodes(self, now: float): + """Terminated nodes for which we haven't received a heartbeat on time. + These nodes are subsequently terminated. + """ + # For type checking, assert that these objects have been instantitiated. + assert self.provider + assert self.non_terminated_nodes + + for node_id in self.non_terminated_nodes.worker_ids: + node_status = self.provider.node_tags(node_id)[TAG_RAY_NODE_STATUS] + # We're not responsible for taking down + # nodes with pending or failed status: + if not node_status == STATUS_UP_TO_DATE: + continue + # This node is up-to-date. If it hasn't had the chance to produce + # a heartbeat, fake the heartbeat now (see logic for completed node + # updaters). + ip = self.provider.internal_ip(node_id) + if ip not in self.load_metrics.last_heartbeat_time_by_ip: + self.load_metrics.mark_active(ip) + # Heartbeat indicates node is healthy: + if self.heartbeat_on_time(node_id, now): + continue + self.schedule_node_termination( + node_id, "lost contact with raylet", logger.warning + ) + self.terminate_scheduled_nodes() + + def attempt_to_recover_unhealthy_nodes(self, now): + for node_id in self.non_terminated_nodes.worker_ids: + self.recover_if_needed(node_id, now) + + def recover_if_needed(self, node_id, now): + if not self.can_update(node_id): + return + if self.heartbeat_on_time(node_id, now): + return + + logger.warning( + "StandardAutoscaler: " + "{}: No recent heartbeat, " + "restarting Ray to recover...".format(node_id) + ) + self.event_summarizer.add( + "Restarting {} nodes of type " + + self._get_node_type(node_id) + + " (lost contact with raylet).", + quantity=1, + aggregate=operator.add, + ) + head_node_ip = self.provider.internal_ip(self.non_terminated_nodes.head_id) + updater = NodeUpdaterThread( + node_id=node_id, + provider_config=self.config["provider"], + provider=self.provider, + auth_config=self.config["auth"], + cluster_name=self.config["cluster_name"], + file_mounts={}, + initialization_commands=[], + setup_commands=[], + ray_start_commands=with_head_node_ip( + self.config["worker_start_ray_commands"], head_node_ip + ), + runtime_hash=self.runtime_hash, + file_mounts_contents_hash=self.file_mounts_contents_hash, + process_runner=self.process_runner, + use_internal_ip=True, + is_head_node=False, + docker_config=self._get_node_specific_docker_config(node_id), + node_resources=self._node_resources(node_id), + node_labels=self._node_labels(node_id), + for_recovery=True, + ) + updater.start() + self.updaters[node_id] = updater + + def _get_node_type(self, node_id: str) -> str: + # For type checking, assert that this object has been instantitiated. + assert self.provider + + node_tags = self.provider.node_tags(node_id) + if TAG_RAY_USER_NODE_TYPE in node_tags: + return node_tags[TAG_RAY_USER_NODE_TYPE] + else: + return "unknown_node_type" + + def _get_node_type_specific_fields(self, node_id: str, fields_key: str) -> Any: + # For type checking, assert that this object has been instantitiated. + assert self.provider + + fields = self.config[fields_key] + node_tags = self.provider.node_tags(node_id) + if TAG_RAY_USER_NODE_TYPE in node_tags: + node_type = node_tags[TAG_RAY_USER_NODE_TYPE] + if node_type not in self.available_node_types: + raise ValueError(f"Unknown node type tag: {node_type}.") + node_specific_config = self.available_node_types[node_type] + if fields_key in node_specific_config: + fields = node_specific_config[fields_key] + return fields + + def _get_node_specific_docker_config(self, node_id): + if "docker" not in self.config: + return {} + docker_config = copy.deepcopy(self.config.get("docker", {})) + node_specific_docker = self._get_node_type_specific_fields(node_id, "docker") + docker_config.update(node_specific_docker) + return docker_config + + def should_update(self, node_id): + if not self.can_update(node_id): + return UpdateInstructions(None, None, None, None) # no update + + status = self.provider.node_tags(node_id).get(TAG_RAY_NODE_STATUS) + if status == STATUS_UP_TO_DATE and self.files_up_to_date(node_id): + return UpdateInstructions(None, None, None, None) # no update + + successful_updated = self.num_successful_updates.get(node_id, 0) > 0 + if successful_updated and self.config.get("restart_only", False): + setup_commands = [] + ray_start_commands = self.config["worker_start_ray_commands"] + elif successful_updated and self.config.get("no_restart", False): + setup_commands = self._get_node_type_specific_fields( + node_id, "worker_setup_commands" + ) + ray_start_commands = [] + else: + setup_commands = self._get_node_type_specific_fields( + node_id, "worker_setup_commands" + ) + ray_start_commands = self.config["worker_start_ray_commands"] + + docker_config = self._get_node_specific_docker_config(node_id) + return UpdateInstructions( + node_id=node_id, + setup_commands=setup_commands, + ray_start_commands=ray_start_commands, + docker_config=docker_config, + ) + + def spawn_updater( + self, + node_id, + setup_commands, + ray_start_commands, + node_resources, + node_labels, + docker_config, + ): + logger.info( + f"Creating new (spawn_updater) updater thread for node" f" {node_id}." + ) + ip = self.provider.internal_ip(node_id) + node_type = self._get_node_type(node_id) + self.node_tracker.track(node_id, ip, node_type) + head_node_ip = self.provider.internal_ip(self.non_terminated_nodes.head_id) + updater = NodeUpdaterThread( + node_id=node_id, + provider_config=self.config["provider"], + provider=self.provider, + auth_config=self.config["auth"], + cluster_name=self.config["cluster_name"], + file_mounts=self.config["file_mounts"], + initialization_commands=with_head_node_ip( + self._get_node_type_specific_fields(node_id, "initialization_commands"), + head_node_ip, + ), + setup_commands=with_head_node_ip(setup_commands, head_node_ip), + ray_start_commands=with_head_node_ip(ray_start_commands, head_node_ip), + runtime_hash=self.runtime_hash, + file_mounts_contents_hash=self.file_mounts_contents_hash, + is_head_node=False, + cluster_synced_files=self.config["cluster_synced_files"], + rsync_options={ + "rsync_exclude": self.config.get("rsync_exclude"), + "rsync_filter": self.config.get("rsync_filter"), + }, + process_runner=self.process_runner, + use_internal_ip=True, + docker_config=docker_config, + node_resources=node_resources, + node_labels=node_labels, + ) + updater.start() + self.updaters[node_id] = updater + + def can_update(self, node_id): + if self.disable_node_updaters: + return False + if node_id in self.updaters: + return False + if not self.launch_config_ok(node_id): + return False + if self.num_failed_updates.get(node_id, 0) > 0: # TODO(ekl) retry? + return False + logger.debug( + f"{node_id} is not being updated and " + "passes config check (can_update=True)." + ) + return True + + def launch_new_node(self, count: int, node_type: str) -> None: + logger.info("StandardAutoscaler: Queue {} new nodes for launch".format(count)) + self.pending_launches.inc(node_type, count) + config = copy.deepcopy(self.config) + if self.foreground_node_launch: + assert self.foreground_node_launcher is not None + # Launch in the main thread and block. + self.foreground_node_launcher.launch_node(config, count, node_type) + else: + assert self.launch_queue is not None + # Split into individual launch requests of the max batch size. + while count > 0: + # Enqueue launch data for the background NodeUpdater threads. + self.launch_queue.put( + (config, min(count, self.max_launch_batch), node_type) + ) + count -= self.max_launch_batch + + def kill_workers(self): + logger.error("StandardAutoscaler: kill_workers triggered") + nodes = self.workers() + if nodes: + self.provider.terminate_nodes(nodes) + for node in nodes: + self.node_tracker.untrack(node) + self.prom_metrics.stopped_nodes.inc() + logger.error("StandardAutoscaler: terminated {} node(s)".format(len(nodes))) + + def summary(self) -> Optional[AutoscalerSummary]: + """Summarizes the active, pending, and failed node launches. + + An active node is a node whose raylet is actively reporting heartbeats. + A pending node is non-active node whose node tag is uninitialized, + waiting for ssh, syncing files, or setting up. + If a node is not pending or active, it is failed. + + Returns: + AutoscalerSummary: The summary. + """ + # For type checking, assert that this object has been instantitiated. + assert self.provider + + if not self.non_terminated_nodes: + return None + active_nodes: Dict[NodeType, int] = Counter() + pending_nodes = [] + failed_nodes = [] + non_failed = set() + + node_type_mapping = {} + now = time.time() + for node_id in self.non_terminated_nodes.all_node_ids: + ip = self.provider.internal_ip(node_id) + node_tags = self.provider.node_tags(node_id) + + if not all( + tag in node_tags + for tag in ( + TAG_RAY_NODE_KIND, + TAG_RAY_USER_NODE_TYPE, + TAG_RAY_NODE_STATUS, + ) + ): + # In some node providers, creation of a node and tags is not + # atomic, so just skip it. + continue + + if node_tags[TAG_RAY_NODE_KIND] == NODE_KIND_UNMANAGED: + continue + node_type = node_tags[TAG_RAY_USER_NODE_TYPE] + + node_type_mapping[ip] = node_type + + is_active = self.heartbeat_on_time(node_id, now) + if is_active: + active_nodes[node_type] += 1 + non_failed.add(node_id) + else: + status = node_tags[TAG_RAY_NODE_STATUS] + completed_states = [STATUS_UP_TO_DATE, STATUS_UPDATE_FAILED] + is_pending = status not in completed_states + if is_pending: + pending_nodes.append((node_id, ip, node_type, status)) + non_failed.add(node_id) + + failed_nodes = self.node_tracker.get_all_failed_node_info(non_failed) + + # The concurrent counter leaves some 0 counts in, so we need to + # manually filter those out. + pending_launches = {} + for node_type, count in self.pending_launches.breakdown().items(): + if count: + pending_launches[node_type] = count + + pending_resources = {} + for node_resources in self.resource_demand_scheduler.calculate_node_resources( + nodes=[node_id for node_id, _, _, _ in pending_nodes], + pending_nodes=pending_launches, + # We don't fill this field out because we're intentionally only + # passing pending nodes (which aren't tracked by load metrics + # anyways). + unused_resources_by_ip={}, + )[0]: + for key, value in node_resources.items(): + pending_resources[key] = value + pending_resources.get(key, 0) + + return AutoscalerSummary( + # Convert active_nodes from counter to dict for later serialization + active_nodes=dict(active_nodes), + idle_nodes=None, + pending_nodes=[ + (ip, node_type, status) for _, ip, node_type, status in pending_nodes + ], + pending_launches=pending_launches, + failed_nodes=failed_nodes, + node_availability_summary=self.node_provider_availability_tracker.summary(), + pending_resources=pending_resources, + node_type_mapping=node_type_mapping, + legacy=True, + ) + + def info_string(self): + lm_summary = self.load_metrics.summary() + autoscaler_summary = self.summary() + assert autoscaler_summary + return "\n" + format_info_string(lm_summary, autoscaler_summary) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7dd10c92cd144cd6d426ef9fc993f045e37aa74 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/config.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a87d440fd5974b963c3c2a29095b89cf97002fee Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a843750a0cbec946ba0f490677a5b425d09febe Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94b89c25afd8deb970de9389b78c3f6b8fe0b336 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d6d749cb3dc08cfc7288c78302f6f1b67d0bda2 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/__pycache__/cloudwatch_helper.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/__pycache__/cloudwatch_helper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd17f5617c01f601946d990d09f5b723b230120e Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/__pycache__/cloudwatch_helper.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/cloudwatch_helper.py b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/cloudwatch_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..939d11620eaae694d1d9be0dd88e967cc149c621 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/cloudwatch/cloudwatch_helper.py @@ -0,0 +1,821 @@ +import copy +import hashlib +import json +import logging +import os +import time +from enum import Enum +from typing import Any, Callable, Dict, List, Union + +import botocore + +from ray.autoscaler._private.aws.utils import client_cache, resource_cache +from ray.autoscaler.tags import NODE_KIND_HEAD, TAG_RAY_CLUSTER_NAME, TAG_RAY_NODE_KIND + +logger = logging.getLogger(__name__) + +RAY = "ray-autoscaler" +CLOUDWATCH_RAY_INSTANCE_PROFILE = RAY + "-cloudwatch-v1" +CLOUDWATCH_RAY_IAM_ROLE = RAY + "-cloudwatch-v1" +CLOUDWATCH_AGENT_INSTALLED_AMI_TAG = "T6Iq2faj" +CLOUDWATCH_AGENT_INSTALLED_TAG = "cloudwatch-agent-installed" +CLOUDWATCH_CONFIG_HASH_TAG_BASE = "cloudwatch-config-hash" + + +class CloudwatchConfigType(str, Enum): + AGENT = "agent" + DASHBOARD = "dashboard" + ALARM = "alarm" + + +class CloudwatchHelper: + def __init__( + self, provider_config: Dict[str, Any], node_id: str, cluster_name: str + ) -> None: + self.node_id = node_id + self.cluster_name = cluster_name + self.provider_config = provider_config + region = provider_config["region"] + self.ec2_resource = resource_cache("ec2", region) + self.ec2_client = self.ec2_resource.meta.client + self.ssm_client = client_cache("ssm", region) + cloudwatch_resource = resource_cache("cloudwatch", region) + self.cloudwatch_client = cloudwatch_resource.meta.client + self.CLOUDWATCH_CONFIG_TYPE_TO_CONFIG_VARIABLE_REPLACE_FUNC: Dict[ + str, Callable + ] = { + CloudwatchConfigType.AGENT.value: self._replace_cwa_config_vars, + CloudwatchConfigType.DASHBOARD.value: self._replace_dashboard_config_vars, + CloudwatchConfigType.ALARM.value: self._load_config_file, + } + self.CLOUDWATCH_CONFIG_TYPE_TO_UPDATE_FUNC_HEAD_NODE: Dict[str, Callable] = { + CloudwatchConfigType.AGENT.value: self._restart_cloudwatch_agent, + CloudwatchConfigType.DASHBOARD.value: self._put_cloudwatch_dashboard, + CloudwatchConfigType.ALARM.value: self._put_cloudwatch_alarm, + } + self.CLOUDWATCH_CONFIG_TYPE_TO_UPDATE_FUNC_WORKER_NODE: Dict[str, Callable] = { + CloudwatchConfigType.AGENT.value: self._restart_cloudwatch_agent, + CloudwatchConfigType.ALARM.value: self._put_cloudwatch_alarm, + } + + def update_from_config(self, is_head_node: bool) -> None: + """Discovers and applies CloudWatch config updates as required. + + Args: + is_head_node: whether this node is the head node. + """ + for config_type in CloudwatchConfigType: + if CloudwatchHelper.cloudwatch_config_exists( + self.provider_config, config_type.value + ): + self._update_cloudwatch_config(config_type.value, is_head_node) + + def _ec2_health_check_waiter(self, node_id: str) -> None: + # wait for all EC2 instance checks to complete + try: + logger.info( + "Waiting for EC2 instance health checks to complete before " + "configuring Unified Cloudwatch Agent. This may take a few " + "minutes..." + ) + waiter = self.ec2_client.get_waiter("instance_status_ok") + waiter.wait(InstanceIds=[node_id]) + except botocore.exceptions.WaiterError as e: + logger.error( + "Failed while waiting for EC2 instance checks to complete: {}".format( + e.message + ) + ) + raise e + + def _update_cloudwatch_config(self, config_type: str, is_head_node: bool) -> None: + """ + check whether update operations are needed in + cloudwatch related configs + """ + cwa_installed = self._setup_cwa() + param_name = self._get_ssm_param_name(config_type) + if cwa_installed: + if is_head_node: + cw_config_ssm = self._set_cloudwatch_ssm_config_param( + param_name, config_type + ) + cur_cw_config_hash = self._sha1_hash_file(config_type) + ssm_cw_config_hash = self._sha1_hash_json(cw_config_ssm) + # check if user updated cloudwatch related config files. + # if so, perform corresponding actions. + if cur_cw_config_hash != ssm_cw_config_hash: + logger.info( + "Cloudwatch {} config file has changed.".format(config_type) + ) + self._upload_config_to_ssm_and_set_hash_tag(config_type) + self.CLOUDWATCH_CONFIG_TYPE_TO_UPDATE_FUNC_HEAD_NODE.get( + config_type + )() + else: + head_node_hash = self._get_head_node_config_hash(config_type) + cur_node_hash = self._get_cur_node_config_hash(config_type) + if head_node_hash != cur_node_hash: + logger.info( + "Cloudwatch {} config file has changed.".format(config_type) + ) + update_func = ( + self.CLOUDWATCH_CONFIG_TYPE_TO_UPDATE_FUNC_WORKER_NODE.get( + config_type + ) + ) + if update_func: + update_func() + self._update_cloudwatch_hash_tag_value( + self.node_id, head_node_hash, config_type + ) + + def _put_cloudwatch_dashboard(self) -> Dict[str, Any]: + """put dashboard to cloudwatch console""" + + cloudwatch_config = self.provider_config["cloudwatch"] + dashboard_config = cloudwatch_config.get("dashboard", {}) + dashboard_name_cluster = dashboard_config.get("name", self.cluster_name) + dashboard_name = self.cluster_name + "-" + dashboard_name_cluster + + widgets = self._replace_dashboard_config_vars( + CloudwatchConfigType.DASHBOARD.value + ) + + response = self.cloudwatch_client.put_dashboard( + DashboardName=dashboard_name, DashboardBody=json.dumps({"widgets": widgets}) + ) + issue_count = len(response.get("DashboardValidationMessages", [])) + if issue_count > 0: + for issue in response.get("DashboardValidationMessages"): + logging.error( + "Error in dashboard config: {} - {}".format( + issue["Message"], issue["DataPath"] + ) + ) + raise Exception( + "Errors in dashboard configuration: {} issues raised".format( + issue_count + ) + ) + else: + logger.info("Successfully put dashboard to CloudWatch console") + return response + + def _put_cloudwatch_alarm(self) -> None: + """put CloudWatch metric alarms read from config""" + param_name = self._get_ssm_param_name(CloudwatchConfigType.ALARM.value) + data = json.loads(self._get_ssm_param(param_name)) + for item in data: + item_out = copy.deepcopy(item) + self._replace_all_config_variables( + item_out, + self.node_id, + self.cluster_name, + self.provider_config["region"], + ) + self.cloudwatch_client.put_metric_alarm(**item_out) + logger.info("Successfully put alarms to CloudWatch console") + + def _send_command_to_node( + self, document_name: str, parameters: Dict[str, List[str]], node_id: str + ) -> Dict[str, Any]: + """send SSM command to the given nodes""" + logger.debug( + "Sending SSM command to {} node(s). Document name: {}. " + "Parameters: {}.".format(node_id, document_name, parameters) + ) + response = self.ssm_client.send_command( + InstanceIds=[node_id], + DocumentName=document_name, + Parameters=parameters, + MaxConcurrency="1", + MaxErrors="0", + ) + return response + + def _ssm_command_waiter( + self, + document_name: str, + parameters: Dict[str, List[str]], + node_id: str, + retry_failed: bool = True, + ) -> Dict[str, Any]: + """wait for SSM command to complete on all cluster nodes""" + + # This waiter differs from the built-in SSM.Waiter by + # optimistically waiting for the command invocation to + # exist instead of failing immediately, and by resubmitting + # any failed command until all retry attempts are exhausted + # by default. + response = self._send_command_to_node(document_name, parameters, node_id) + command_id = response["Command"]["CommandId"] + + cloudwatch_config = self.provider_config["cloudwatch"] + agent_retryer_config = cloudwatch_config.get( + CloudwatchConfigType.AGENT.value + ).get("retryer", {}) + max_attempts = agent_retryer_config.get("max_attempts", 120) + delay_seconds = agent_retryer_config.get("delay_seconds", 30) + num_attempts = 0 + cmd_invocation_res = {} + while True: + num_attempts += 1 + logger.debug( + "Listing SSM command ID {} invocations on node {}".format( + command_id, node_id + ) + ) + response = self.ssm_client.list_command_invocations( + CommandId=command_id, + InstanceId=node_id, + ) + cmd_invocations = response["CommandInvocations"] + if not cmd_invocations: + logger.debug( + "SSM Command ID {} invocation does not exist. If " + "the command was just started, it may take a " + "few seconds to register.".format(command_id) + ) + else: + if len(cmd_invocations) > 1: + logger.warning( + "Expected to find 1 SSM command invocation with " + "ID {} on node {} but found {}: {}".format( + command_id, + node_id, + len(cmd_invocations), + cmd_invocations, + ) + ) + cmd_invocation = cmd_invocations[0] + if cmd_invocation["Status"] == "Success": + logger.debug( + "SSM Command ID {} completed successfully.".format(command_id) + ) + cmd_invocation_res[node_id] = True + break + if num_attempts >= max_attempts: + logger.error( + "Max attempts for command {} exceeded on node {}".format( + command_id, node_id + ) + ) + raise botocore.exceptions.WaiterError( + name="ssm_waiter", + reason="Max attempts exceeded", + last_response=cmd_invocation, + ) + if cmd_invocation["Status"] == "Failed": + logger.debug(f"SSM Command ID {command_id} failed.") + if retry_failed: + logger.debug(f"Retrying in {delay_seconds} seconds.") + response = self._send_command_to_node( + document_name, parameters, node_id + ) + command_id = response["Command"]["CommandId"] + logger.debug( + "Sent SSM command ID {} to node {}".format( + command_id, node_id + ) + ) + else: + logger.debug(f"Ignoring Command ID {command_id} failure.") + cmd_invocation_res[node_id] = False + break + time.sleep(delay_seconds) + + return cmd_invocation_res + + def _replace_config_variables( + self, string: str, node_id: str, cluster_name: str, region: str + ) -> str: + """ + replace known config variable occurrences in the input string + does not replace variables with undefined or empty strings + """ + + if node_id: + string = string.replace("{instance_id}", node_id) + if cluster_name: + string = string.replace("{cluster_name}", cluster_name) + if region: + string = string.replace("{region}", region) + return string + + def _replace_all_config_variables( + self, + collection: Union[Dict[str, Any], str], + node_id: str, + cluster_name: str, + region: str, + ) -> Union[str, Dict[str, Any]]: + """ + Replace known config variable occurrences in the input collection. + The input collection must be either a dict or list. + Returns a tuple consisting of the output collection and the number of + modified strings in the collection (which is not necessarily equal to + the number of variables replaced). + """ + + for key in collection: + if type(collection) is dict: + value = collection.get(key) + index_key = key + elif type(collection) is list: + value = key + index_key = collection.index(key) + else: + raise ValueError( + f"Can't replace CloudWatch config variables " + f"in unsupported collection type: {type(collection)}." + f"Please check your CloudWatch JSON config files." + ) + if type(value) is str: + collection[index_key] = self._replace_config_variables( + value, node_id, cluster_name, region + ) + elif type(value) is dict or type(value) is list: + collection[index_key] = self._replace_all_config_variables( + value, node_id, cluster_name, region + ) + return collection + + def _load_config_file(self, config_type: str) -> Dict[str, Any]: + """load JSON config file""" + cloudwatch_config = self.provider_config["cloudwatch"] + json_config_file_section = cloudwatch_config.get(config_type, {}) + json_config_file_path = json_config_file_section.get("config", {}) + json_config_path = os.path.abspath(json_config_file_path) + with open(json_config_path) as f: + data = json.load(f) + return data + + def _set_cloudwatch_ssm_config_param( + self, parameter_name: str, config_type: str + ) -> str: + """ + get cloudwatch config for the given param and config type from SSM + if it exists, put it in the SSM param store if not + """ + try: + parameter_value = self._get_ssm_param(parameter_name) + except botocore.exceptions.ClientError as e: + if e.response["Error"]["Code"] == "ParameterNotFound": + logger.info( + "Cloudwatch {} config file is not found " + "at SSM parameter store. " + "Checking for Unified CloudWatch Agent installation".format( + config_type + ) + ) + return self._get_default_empty_config_file_hash() + else: + logger.info( + "Failed to fetch Unified CloudWatch Agent config from SSM " + "parameter store." + ) + logger.error(e) + raise e + return parameter_value + + def _get_default_empty_config_file_hash(self): + default_cw_config = "{}" + parameter_value = self._sha1_hash_json(default_cw_config) + return parameter_value + + def _get_ssm_param(self, parameter_name: str) -> str: + """ + get the SSM parameter value associated with the given parameter name + """ + response = self.ssm_client.get_parameter(Name=parameter_name) + logger.info("Successfully fetch ssm parameter: {}".format(parameter_name)) + res = response.get("Parameter", {}) + cwa_parameter = res.get("Value", {}) + return cwa_parameter + + def _sha1_hash_json(self, value: str) -> str: + """calculate the json string sha1 hash""" + sha1_hash = hashlib.new("sha1") + binary_value = value.encode("ascii") + sha1_hash.update(binary_value) + sha1_res = sha1_hash.hexdigest() + return sha1_res + + def _sha1_hash_file(self, config_type: str) -> str: + """calculate the config file sha1 hash""" + config = self.CLOUDWATCH_CONFIG_TYPE_TO_CONFIG_VARIABLE_REPLACE_FUNC.get( + config_type + )(config_type) + value = json.dumps(config) + sha1_res = self._sha1_hash_json(value) + return sha1_res + + def _upload_config_to_ssm_and_set_hash_tag(self, config_type: str): + data = self.CLOUDWATCH_CONFIG_TYPE_TO_CONFIG_VARIABLE_REPLACE_FUNC.get( + config_type + )(config_type) + sha1_hash_value = self._sha1_hash_file(config_type) + self._upload_config_to_ssm(data, config_type) + self._update_cloudwatch_hash_tag_value( + self.node_id, sha1_hash_value, config_type + ) + + def _add_cwa_installed_tag(self, node_id: str) -> None: + self.ec2_client.create_tags( + Resources=[node_id], + Tags=[{"Key": CLOUDWATCH_AGENT_INSTALLED_TAG, "Value": "True"}], + ) + logger.info( + "Successfully add Unified CloudWatch Agent installed " + "tag on {}".format(node_id) + ) + + def _update_cloudwatch_hash_tag_value( + self, node_id: str, sha1_hash_value: str, config_type: str + ): + hash_key_value = "-".join([CLOUDWATCH_CONFIG_HASH_TAG_BASE, config_type]) + self.ec2_client.create_tags( + Resources=[node_id], + Tags=[{"Key": hash_key_value, "Value": sha1_hash_value}], + ) + logger.info( + "Successfully update cloudwatch {} hash tag on {}".format( + config_type, node_id + ) + ) + + def _get_ssm_param_name(self, config_type: str) -> str: + """return the parameter name for cloudwatch configs""" + ssm_config_param_name = "AmazonCloudWatch-" + "ray_{}_config_{}".format( + config_type, self.cluster_name + ) + return ssm_config_param_name + + def _put_ssm_param(self, parameter: Dict[str, Any], parameter_name: str) -> None: + """upload cloudwatch config to the SSM parameter store""" + self.ssm_client.put_parameter( + Name=parameter_name, + Type="String", + Value=json.dumps(parameter), + Overwrite=True, + Tier="Intelligent-Tiering", + ) + + def _upload_config_to_ssm(self, param: Dict[str, Any], config_type: str): + param_name = self._get_ssm_param_name(config_type) + self._put_ssm_param(param, param_name) + + def _replace_cwa_config_vars(self, config_type: str) -> Dict[str, Any]: + """ + replace {instance_id}, {region}, {cluster_name} + variable occurrences in Unified Cloudwatch Agent config file + """ + cwa_config = self._load_config_file(config_type) + self._replace_all_config_variables( + cwa_config, + self.node_id, + self.cluster_name, + self.provider_config["region"], + ) + return cwa_config + + def _replace_dashboard_config_vars(self, config_type: str) -> List[str]: + """ + replace known variable occurrences in CloudWatch Dashboard config file + """ + data = self._load_config_file(config_type) + widgets = [] + for item in data: + item_out = self._replace_all_config_variables( + item, + self.node_id, + self.cluster_name, + self.provider_config["region"], + ) + widgets.append(item_out) + return widgets + + def _replace_alarm_config_vars(self, config_type: str) -> List[str]: + """ + replace {instance_id}, {region}, {cluster_name} + variable occurrences in cloudwatch alarm config file + """ + data = self._load_config_file(config_type) + param_data = [] + for item in data: + item_out = copy.deepcopy(item) + self._replace_all_config_variables( + item_out, + self.node_id, + self.cluster_name, + self.provider_config["region"], + ) + param_data.append(item_out) + return param_data + + def _restart_cloudwatch_agent(self) -> None: + """restart Unified CloudWatch Agent""" + cwa_param_name = self._get_ssm_param_name(CloudwatchConfigType.AGENT.value) + logger.info( + "Restarting Unified CloudWatch Agent package on node {}.".format( + self.node_id + ) + ) + self._stop_cloudwatch_agent() + self._start_cloudwatch_agent(cwa_param_name) + + def _stop_cloudwatch_agent(self) -> None: + """stop Unified CloudWatch Agent""" + logger.info( + "Stopping Unified CloudWatch Agent package on node {}.".format(self.node_id) + ) + parameters_stop_cwa = { + "action": ["stop"], + "mode": ["ec2"], + } + # don't retry failed stop commands + # (there's not always an agent to stop) + self._ssm_command_waiter( + "AmazonCloudWatch-ManageAgent", + parameters_stop_cwa, + self.node_id, + False, + ) + logger.info("Unified CloudWatch Agent stopped on node {}.".format(self.node_id)) + + def _start_cloudwatch_agent(self, cwa_param_name: str) -> None: + """start Unified CloudWatch Agent""" + logger.info( + "Starting Unified CloudWatch Agent package on node {}.".format(self.node_id) + ) + parameters_start_cwa = { + "action": ["configure"], + "mode": ["ec2"], + "optionalConfigurationSource": ["ssm"], + "optionalConfigurationLocation": [cwa_param_name], + "optionalRestart": ["yes"], + } + self._ssm_command_waiter( + "AmazonCloudWatch-ManageAgent", parameters_start_cwa, self.node_id + ) + logger.info( + "Unified CloudWatch Agent started successfully on node {}.".format( + self.node_id + ) + ) + + def _setup_cwa(self) -> bool: + cwa_installed = self._check_cwa_installed_ec2_tag() + if cwa_installed == "False": + res_cwa_installed = self._ensure_cwa_installed_ssm(self.node_id) + return res_cwa_installed + else: + return True + + def _get_head_node_config_hash(self, config_type: str) -> str: + hash_key_value = "-".join([CLOUDWATCH_CONFIG_HASH_TAG_BASE, config_type]) + filters = copy.deepcopy( + self._get_current_cluster_session_nodes(self.cluster_name) + ) + filters.append( + { + "Name": "tag:{}".format(TAG_RAY_NODE_KIND), + "Values": [NODE_KIND_HEAD], + } + ) + try: + instance = list(self.ec2_resource.instances.filter(Filters=filters)) + assert len(instance) == 1, "More than 1 head node found!" + for tag in instance[0].tags: + if tag["Key"] == hash_key_value: + return tag["Value"] + except botocore.exceptions.ClientError as e: + logger.warning( + "{} Error caught when getting value of {} tag on head node".format( + e.response["Error"], hash_key_value + ) + ) + + def _get_cur_node_config_hash(self, config_type: str) -> str: + hash_key_value = "-".join([CLOUDWATCH_CONFIG_HASH_TAG_BASE, config_type]) + try: + response = self.ec2_client.describe_instances(InstanceIds=[self.node_id]) + reservations = response["Reservations"] + message = "More than 1 response received from describing current node" + assert len(reservations) == 1, message + instances = reservations[0]["Instances"] + assert len(reservations) == 1, message + tags = instances[0]["Tags"] + hash_value = self._get_default_empty_config_file_hash() + for tag in tags: + if tag["Key"] == hash_key_value: + logger.info( + "Successfully get cloudwatch {} hash tag value from " + "node {}".format(config_type, self.node_id) + ) + hash_value = tag["Value"] + return hash_value + except botocore.exceptions.ClientError as e: + logger.warning( + "{} Error caught when getting hash tag {} tag".format( + e.response["Error"], hash_key_value + ) + ) + + def _ensure_cwa_installed_ssm(self, node_id: str) -> bool: + """ + Check if Unified Cloudwatch Agent is installed via ssm run command. + If not, notify user to use an AMI with + the Unified CloudWatch Agent installed. + """ + logger.info( + "Checking Unified Cloudwatch Agent status on node {}".format(node_id) + ) + parameters_status_cwa = { + "action": ["status"], + "mode": ["ec2"], + } + self._ec2_health_check_waiter(node_id) + cmd_invocation_res = self._ssm_command_waiter( + "AmazonCloudWatch-ManageAgent", parameters_status_cwa, node_id, False + ) + cwa_installed = cmd_invocation_res.get(node_id, False) + if not cwa_installed: + logger.warning( + "Unified CloudWatch Agent not installed on {}. " + "Ray logs, metrics not picked up. " + "Please use an AMI with Unified CloudWatch Agent installed.".format( + node_id + ) + ) + return False + else: + return True + + def _get_current_cluster_session_nodes(self, cluster_name: str) -> List[dict]: + filters = [ + { + "Name": "instance-state-name", + "Values": ["pending", "running"], + }, + { + "Name": "tag:{}".format(TAG_RAY_CLUSTER_NAME), + "Values": [cluster_name], + }, + ] + return filters + + def _check_cwa_installed_ec2_tag(self) -> List[str]: + """ + Filtering all nodes to get nodes + without Unified CloudWatch Agent installed + """ + try: + response = self.ec2_client.describe_instances(InstanceIds=[self.node_id]) + reservations = response["Reservations"] + message = "More than 1 response received from describing current node" + assert len(reservations) == 1, message + instances = reservations[0]["Instances"] + assert len(instances) == 1, message + tags = instances[0]["Tags"] + cwa_installed = str(False) + for tag in tags: + if tag["Key"] == CLOUDWATCH_AGENT_INSTALLED_TAG: + logger.info( + "Unified CloudWatch Agent is installed on " + "node {}".format(self.node_id) + ) + cwa_installed = tag["Value"] + return cwa_installed + except botocore.exceptions.ClientError as e: + logger.warning( + "{} Error caught when getting Unified CloudWatch Agent " + "status based on {} tag".format( + e.response["Error"], CLOUDWATCH_AGENT_INSTALLED_TAG + ) + ) + + @staticmethod + def resolve_instance_profile_name( + config: Dict[str, Any], default_instance_profile_name: str + ) -> str: + """Get default cloudwatch instance profile name. + + Args: + config: provider section of cluster config file. + default_instance_profile_name: default ray instance profile name. + + Returns: + default cloudwatch instance profile name if cloudwatch config file + exists. + default ray instance profile name if cloudwatch config file + doesn't exist. + """ + cwa_cfg_exists = CloudwatchHelper.cloudwatch_config_exists( + config, CloudwatchConfigType.AGENT.value + ) + return ( + CLOUDWATCH_RAY_INSTANCE_PROFILE + if cwa_cfg_exists + else default_instance_profile_name + ) + + @staticmethod + def resolve_iam_role_name( + config: Dict[str, Any], default_iam_role_name: str + ) -> str: + """Get default cloudwatch iam role name. + + Args: + config: provider section of cluster config file. + default_iam_role_name: default ray iam role name. + + Returns: + default cloudwatch iam role name if cloudwatch config file exists. + default ray iam role name if cloudwatch config file doesn't exist. + """ + cwa_cfg_exists = CloudwatchHelper.cloudwatch_config_exists( + config, CloudwatchConfigType.AGENT.value + ) + return CLOUDWATCH_RAY_IAM_ROLE if cwa_cfg_exists else default_iam_role_name + + @staticmethod + def resolve_policy_arns( + config: Dict[str, Any], iam: Any, default_policy_arns: List[str] + ) -> List[str]: + """Attach necessary AWS policies for CloudWatch related operations. + + Args: + config: provider section of cluster config file. + iam: AWS iam resource. + default_policy_arns: List of default ray AWS policies. + + Returns: + list of policy arns including additional policies for CloudWatch + related operations if cloudwatch agent config is specifed in + cluster config file. + """ + cwa_cfg_exists = CloudwatchHelper.cloudwatch_config_exists( + config, CloudwatchConfigType.AGENT.value + ) + if cwa_cfg_exists: + cloudwatch_managed_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ssm:SendCommand", + "ssm:ListCommandInvocations", + "iam:PassRole", + ], + "Resource": "*", + } + ], + } + iam_client = iam.meta.client + iam_client.create_policy( + PolicyName="CloudwatchManagedPolicies", + PolicyDocument=json.dumps(cloudwatch_managed_policy), + ) + sts_client = client_cache("sts", config["region"]) + account_id = sts_client.get_caller_identity().get("Account") + managed_policy_arn = ( + "arn:aws:iam::{}:policy/CloudwatchManagedPolicies".format(account_id) + ) + policy_waiter = iam_client.get_waiter("policy_exists") + policy_waiter.wait( + PolicyArn=managed_policy_arn, + WaiterConfig={"Delay": 2, "MaxAttempts": 200}, + ) + new_policy_arns = copy.copy(default_policy_arns) + new_policy_arns.extend( + [ + "arn:aws:iam::aws:policy/CloudWatchAgentAdminPolicy", + "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", + managed_policy_arn, + ] + ) + return new_policy_arns + else: + return default_policy_arns + + @staticmethod + def cloudwatch_config_exists(config: Dict[str, Any], config_type: str) -> bool: + """Check if CloudWatch configuration was specified by the user + in their cluster config file. + + Specifically, this function checks if a CloudWatch config file is + specified by the user in their cluster config file. + + Args: + config: provider section of cluster config file. + config_type: type of CloudWatch config file. + + Returns: + True if config file is specified by user. + False if config file is not specified. + """ + cfg = config.get("cloudwatch", {}).get(config_type, {}).get("config") + return bool(cfg) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/config.py b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/config.py new file mode 100644 index 0000000000000000000000000000000000000000..9099b0aacc121a85d8e64b4111cdf851170b0e39 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/config.py @@ -0,0 +1,1226 @@ +import copy +import itertools +import json +import logging +import os +import time +from collections import Counter +from functools import lru_cache, partial +from typing import Any, Dict, List, Optional, Set, Tuple + +import boto3 +import botocore +from packaging.version import Version + +from ray.autoscaler._private.aws.cloudwatch.cloudwatch_helper import ( + CloudwatchHelper as cwh, +) +from ray.autoscaler._private.aws.utils import ( + LazyDefaultDict, + handle_boto_error, + resource_cache, +) +from ray.autoscaler._private.cli_logger import cf, cli_logger +from ray.autoscaler._private.event_system import CreateClusterEvent, global_event_system +from ray.autoscaler._private.providers import _PROVIDER_PRETTY_NAMES +from ray.autoscaler._private.util import check_legacy_fields +from ray.autoscaler.tags import NODE_TYPE_LEGACY_HEAD, NODE_TYPE_LEGACY_WORKER + +logger = logging.getLogger(__name__) + +RAY = "ray-autoscaler" +DEFAULT_RAY_INSTANCE_PROFILE = RAY + "-v1" +DEFAULT_RAY_IAM_ROLE = RAY + "-v1" +SECURITY_GROUP_TEMPLATE = RAY + "-{}" + +# V71.0 has CUDA 11.2 +DEFAULT_AMI_NAME = "AWS Deep Learning AMI (Ubuntu 18.04) V71.0" + +# Obtained with: +# for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do +# echo "Region: $region"; +# aws ec2 describe-images \ +# --region $region \ +# --filters "Name=name,Values=Deep Learning AMI (Ubuntu 18.04) Version 71.0*" \ +# --query "Images[].[ImageId, Name]" \ +# --output text; +# echo "--------------------------"; +# done +# TODO(alex) : write a unit test to make sure we update AMI version used in +# ray/autoscaler/aws/example-full.yaml whenever we update this dict. +DEFAULT_AMI = { + "us-east-1": "ami-0271ce88f6c03e149", # US East (N. Virginia) + "us-east-2": "ami-0ce21b0ce8e0f5a37", # US East (Ohio) + "us-west-1": "ami-030896954b2b72361", # US West (N. California) + "us-west-2": "ami-0a2b85e15b7c0ac34", # US West (Oregon) + "ca-central-1": "ami-044b98bea12677052", # Canada (Central) + "eu-central-1": "ami-004d285ecf53fb335", # EU (Frankfurt) + "eu-west-1": "ami-0e17ed40febe794a6", # EU (Ireland) + "eu-west-2": "ami-0ad9e6b9d4e22df1a", # EU (London) + "eu-west-3": "ami-05698775025146698", # EU (Paris) + "sa-east-1": "ami-004b19c7ed8d790bc", # SA (Sao Paulo) + "ap-northeast-1": "ami-03e45b1f18378ea97", # Asia Pacific (Tokyo) + "ap-northeast-2": "ami-095bae3e623e2ff99", # Asia Pacific (Seoul) + "ap-northeast-3": "ami-02fd0ba69c1ef37ad", # Asia Pacific (Osaka) + "ap-southeast-1": "ami-03ef1223872072e95", # Asia Pacific (Singapore) + "ap-southeast-2": "ami-0bcf4f75e44e0a866", # Asia Pacific (Sydney) +} + +# todo: cli_logger should handle this assert properly +# this should probably also happens somewhere else +assert Version(boto3.__version__) >= Version( + "1.4.8" +), "Boto3 version >= 1.4.8 required, try `pip install -U boto3`" + + +def key_pair(i, region, key_name): + """ + If key_name is not None, key_pair will be named after key_name. + Returns the ith default (aws_key_pair_name, key_pair_path). + """ + if i == 0: + key_pair_name = "{}_{}".format(RAY, region) if key_name is None else key_name + return ( + key_pair_name, + os.path.expanduser("~/.ssh/{}.pem".format(key_pair_name)), + ) + + key_pair_name = ( + "{}_{}_{}".format(RAY, i, region) + if key_name is None + else key_name + "_key-{}".format(i) + ) + return (key_pair_name, os.path.expanduser("~/.ssh/{}.pem".format(key_pair_name))) + + +# Suppress excessive connection dropped logs from boto +logging.getLogger("botocore").setLevel(logging.WARNING) + +_log_info = {} + + +def reload_log_state(override_log_info): + _log_info.update(override_log_info) + + +def get_log_state(): + return _log_info.copy() + + +def _set_config_info(**kwargs): + """Record configuration artifacts useful for logging.""" + + # todo: this is technically fragile iff we ever use multiple configs + + for k, v in kwargs.items(): + _log_info[k] = v + + +def _arn_to_name(arn): + return arn.split(":")[-1].split("/")[-1] + + +def log_to_cli(config: Dict[str, Any]) -> None: + provider_name = _PROVIDER_PRETTY_NAMES.get("aws", None) + + cli_logger.doassert( + provider_name is not None, "Could not find a pretty name for the AWS provider." + ) + + head_node_type = config["head_node_type"] + head_node_config = config["available_node_types"][head_node_type]["node_config"] + + with cli_logger.group("{} config", provider_name): + + def print_info( + resource_string: str, + key: str, + src_key: str, + allowed_tags: Optional[List[str]] = None, + list_value: bool = False, + ) -> None: + if allowed_tags is None: + allowed_tags = ["default"] + + node_tags = {} + + # set of configurations corresponding to `key` + unique_settings = set() + + for node_type_key, node_type in config["available_node_types"].items(): + node_tags[node_type_key] = {} + tag = _log_info[src_key][node_type_key] + if tag in allowed_tags: + node_tags[node_type_key][tag] = True + setting = node_type["node_config"].get(key) + + if list_value: + unique_settings.add(tuple(setting)) + else: + unique_settings.add(setting) + + head_value_str = head_node_config[key] + if list_value: + head_value_str = cli_logger.render_list(head_value_str) + + if len(unique_settings) == 1: + # all node types are configured the same, condense + # log output + cli_logger.labeled_value( + resource_string + " (all available node types)", + "{}", + head_value_str, + _tags=node_tags[config["head_node_type"]], + ) + else: + # do head node type first + cli_logger.labeled_value( + resource_string + f" ({head_node_type})", + "{}", + head_value_str, + _tags=node_tags[head_node_type], + ) + + # go through remaining types + for node_type_key, node_type in config["available_node_types"].items(): + if node_type_key == head_node_type: + continue + workers_value_str = node_type["node_config"][key] + if list_value: + workers_value_str = cli_logger.render_list(workers_value_str) + cli_logger.labeled_value( + resource_string + f" ({node_type_key})", + "{}", + workers_value_str, + _tags=node_tags[node_type_key], + ) + + tags = {"default": _log_info["head_instance_profile_src"] == "default"} + # head_node_config is the head_node_type's config, + # config["head_node"] is a field that gets applied only to the actual + # head node (and not workers of the head's node_type) + assert ( + "IamInstanceProfile" in head_node_config + or "IamInstanceProfile" in config["head_node"] + ) + if "IamInstanceProfile" in head_node_config: + # If the user manually configured the role we're here. + IamProfile = head_node_config["IamInstanceProfile"] + elif "IamInstanceProfile" in config["head_node"]: + # If we filled the default IAM role, we're here. + IamProfile = config["head_node"]["IamInstanceProfile"] + profile_arn = IamProfile.get("Arn") + profile_name = _arn_to_name(profile_arn) if profile_arn else IamProfile["Name"] + cli_logger.labeled_value("IAM Profile", "{}", profile_name, _tags=tags) + + if all( + "KeyName" in node_type["node_config"] + for node_type in config["available_node_types"].values() + ): + print_info("EC2 Key pair", "KeyName", "keypair_src") + + print_info("VPC Subnets", "SubnetIds", "subnet_src", list_value=True) + print_info( + "EC2 Security groups", + "SecurityGroupIds", + "security_group_src", + list_value=True, + ) + print_info("EC2 AMI", "ImageId", "ami_src", allowed_tags=["dlami"]) + + cli_logger.newline() + + +def bootstrap_aws(config): + # create a copy of the input config to modify + config = copy.deepcopy(config) + + # Log warnings if user included deprecated `head_node` or `worker_nodes` + # fields. Raise error if no `available_node_types` + check_legacy_fields(config) + # Used internally to store head IAM role. + config["head_node"] = {} + + # If a LaunchTemplate is provided, extract the necessary fields for the + # config stages below. + config = _configure_from_launch_template(config) + + # If NetworkInterfaces are provided, extract the necessary fields for the + # config stages below. + config = _configure_from_network_interfaces(config) + + # The head node needs to have an IAM role that allows it to create further + # EC2 instances. + config = _configure_iam_role(config) + + # Configure SSH access, using an existing key pair if possible. + config = _configure_key_pair(config) + global_event_system.execute_callback( + CreateClusterEvent.ssh_keypair_downloaded, + {"ssh_key_path": config["auth"]["ssh_private_key"]}, + ) + + # Pick a reasonable subnet if not specified by the user. + config = _configure_subnet(config) + + # Cluster workers should be in a security group that permits traffic within + # the group, and also SSH access from outside. + config = _configure_security_group(config) + + # Provide a helpful message for missing AMI. + _check_ami(config) + + return config + + +def _configure_iam_role(config): + head_node_type = config["head_node_type"] + head_node_config = config["available_node_types"][head_node_type]["node_config"] + if "IamInstanceProfile" in head_node_config: + _set_config_info(head_instance_profile_src="config") + return config + _set_config_info(head_instance_profile_src="default") + + instance_profile_name = cwh.resolve_instance_profile_name( + config["provider"], + DEFAULT_RAY_INSTANCE_PROFILE, + ) + profile = _get_instance_profile(instance_profile_name, config) + + if profile is None: + cli_logger.verbose( + "Creating new IAM instance profile {} for use as the default.", + cf.bold(instance_profile_name), + ) + client = _client("iam", config) + client.create_instance_profile(InstanceProfileName=instance_profile_name) + profile = _get_instance_profile(instance_profile_name, config) + time.sleep(15) # wait for propagation + + cli_logger.doassert( + profile is not None, "Failed to create instance profile." + ) # todo: err msg + assert profile is not None, "Failed to create instance profile" + + if not profile.roles: + role_name = cwh.resolve_iam_role_name(config["provider"], DEFAULT_RAY_IAM_ROLE) + role = _get_role(role_name, config) + if role is None: + cli_logger.verbose( + "Creating new IAM role {} for use as the default instance role.", + cf.bold(role_name), + ) + iam = _resource("iam", config) + policy_doc = { + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "ec2.amazonaws.com"}, + "Action": "sts:AssumeRole", + }, + ] + } + attach_policy_arns = cwh.resolve_policy_arns( + config["provider"], + iam, + [ + "arn:aws:iam::aws:policy/AmazonEC2FullAccess", + "arn:aws:iam::aws:policy/AmazonS3FullAccess", + ], + ) + + iam.create_role( + RoleName=role_name, AssumeRolePolicyDocument=json.dumps(policy_doc) + ) + role = _get_role(role_name, config) + cli_logger.doassert( + role is not None, "Failed to create role." + ) # todo: err msg + + assert role is not None, "Failed to create role" + + for policy_arn in attach_policy_arns: + role.attach_policy(PolicyArn=policy_arn) + + profile.add_role(RoleName=role.name) + time.sleep(15) # wait for propagation + # Add IAM role to "head_node" field so that it is applied only to + # the head node -- not to workers with the same node type as the head. + config["head_node"]["IamInstanceProfile"] = {"Arn": profile.arn} + + return config + + +def _configure_key_pair(config): + node_types = config["available_node_types"] + + # map from node type key -> source of KeyName field + key_pair_src_info = {} + _set_config_info(keypair_src=key_pair_src_info) + + if "ssh_private_key" in config["auth"]: + for node_type_key in node_types: + # keypairs should be provided in the config + key_pair_src_info[node_type_key] = "config" + + # If the key is not configured via the cloudinit + # UserData, it should be configured via KeyName or + # else we will risk starting a node that we cannot + # SSH into: + + for node_type in node_types: + node_config = node_types[node_type]["node_config"] + if "UserData" not in node_config: + cli_logger.doassert( + "KeyName" in node_config, _key_assert_msg(node_type) + ) + assert "KeyName" in node_config + + return config + + for node_type_key in node_types: + key_pair_src_info[node_type_key] = "default" + + ec2 = _resource("ec2", config) + + # Writing the new ssh key to the filesystem fails if the ~/.ssh + # directory doesn't already exist. + os.makedirs(os.path.expanduser("~/.ssh"), exist_ok=True) + + # Try a few times to get or create a good key pair. + MAX_NUM_KEYS = 600 + for i in range(MAX_NUM_KEYS): + + key_name = config["provider"].get("key_pair", {}).get("key_name") + + key_name, key_path = key_pair(i, config["provider"]["region"], key_name) + key = _get_key(key_name, config) + + # Found a good key. + if key and os.path.exists(key_path): + break + + # We can safely create a new key. + if not key and not os.path.exists(key_path): + cli_logger.verbose( + "Creating new key pair {} for use as the default.", cf.bold(key_name) + ) + key = ec2.create_key_pair(KeyName=key_name) + + # We need to make sure to _create_ the file with the right + # permissions. In order to do that we need to change the default + # os.open behavior to include the mode we want. + with open(key_path, "w", opener=partial(os.open, mode=0o600)) as f: + f.write(key.key_material) + break + + if not key: + cli_logger.abort( + "No matching local key file for any of the key pairs in this " + "account with ids from 0..{}. " + "Consider deleting some unused keys pairs from your account.", + key_name, + ) + + cli_logger.doassert( + os.path.exists(key_path), + "Private key file " + cf.bold("{}") + " not found for " + cf.bold("{}"), + key_path, + key_name, + ) # todo: err msg + assert os.path.exists(key_path), "Private key file {} not found for {}".format( + key_path, key_name + ) + + config["auth"]["ssh_private_key"] = key_path + for node_type in node_types.values(): + node_config = node_type["node_config"] + node_config["KeyName"] = key_name + + return config + + +def _key_assert_msg(node_type: str) -> str: + if node_type == NODE_TYPE_LEGACY_WORKER: + return "`KeyName` missing for worker nodes." + elif node_type == NODE_TYPE_LEGACY_HEAD: + return "`KeyName` missing for head node." + else: + return ( + "`KeyName` missing from the `node_config` of" f" node type `{node_type}`." + ) + + +def _usable_subnet_ids( + user_specified_subnets: Optional[List[Any]], + all_subnets: List[Any], + azs: Optional[str], + vpc_id_of_sg: Optional[str], + use_internal_ips: bool, + node_type_key: str, +) -> Tuple[List[str], str]: + """Prunes subnets down to those that meet the following criteria. + + Subnets must be: + * 'Available' according to AWS. + * Public, unless `use_internal_ips` is specified. + * In one of the AZs, if AZs are provided. + * In the given VPC, if a VPC is specified for Security Groups. + + Returns: + List[str]: Subnets that are usable. + str: VPC ID of the first subnet. + """ + + def _are_user_subnets_pruned(current_subnets: List[Any]) -> bool: + return user_specified_subnets is not None and len(current_subnets) != len( + user_specified_subnets + ) + + def _get_pruned_subnets(current_subnets: List[Any]) -> Set[str]: + current_subnet_ids = {s.subnet_id for s in current_subnets} + user_specified_subnet_ids = {s.subnet_id for s in user_specified_subnets} + return user_specified_subnet_ids - current_subnet_ids + + try: + candidate_subnets = ( + user_specified_subnets + if user_specified_subnets is not None + else all_subnets + ) + if vpc_id_of_sg: + candidate_subnets = [ + s for s in candidate_subnets if s.vpc_id == vpc_id_of_sg + ] + subnets = sorted( + ( + s + for s in candidate_subnets + if s.state == "available" + and (use_internal_ips or s.map_public_ip_on_launch) + ), + reverse=True, # sort from Z-A + key=lambda subnet: subnet.availability_zone, + ) + except botocore.exceptions.ClientError as exc: + handle_boto_error(exc, "Failed to fetch available subnets from AWS.") + raise exc + + if not subnets: + cli_logger.abort( + f"No usable subnets found for node type {node_type_key}, try " + "manually creating an instance in your specified region to " + "populate the list of subnets and trying this again.\n" + "Note that the subnet must map public IPs " + "on instance launch unless you set `use_internal_ips: true` in " + "the `provider` config." + ) + elif _are_user_subnets_pruned(subnets): + cli_logger.abort( + f"The specified subnets for node type {node_type_key} are not " + f"usable: {_get_pruned_subnets(subnets)}" + ) + + if azs is not None: + azs = [az.strip() for az in azs.split(",")] + subnets = [ + s + for az in azs # Iterate over AZs first to maintain the ordering + for s in subnets + if s.availability_zone == az + ] + if not subnets: + cli_logger.abort( + f"No usable subnets matching availability zone {azs} found " + f"for node type {node_type_key}.\nChoose a different " + "availability zone or try manually creating an instance in " + "your specified region to populate the list of subnets and " + "trying this again." + ) + elif _are_user_subnets_pruned(subnets): + cli_logger.abort( + f"MISMATCH between specified subnets and Availability Zones! " + "The following Availability Zones were specified in the " + f"`provider section`: {azs}.\n The following subnets for node " + f"type `{node_type_key}` have no matching availability zone: " + f"{list(_get_pruned_subnets(subnets))}." + ) + + # Use subnets in only one VPC, so that _configure_security_groups only + # needs to create a security group in this one VPC. Otherwise, we'd need + # to set up security groups in all of the user's VPCs and set up networking + # rules to allow traffic between these groups. + # See https://github.com/ray-project/ray/pull/14868. + first_subnet_vpc_id = subnets[0].vpc_id + subnets = [s.subnet_id for s in subnets if s.vpc_id == subnets[0].vpc_id] + if _are_user_subnets_pruned(subnets): + subnet_vpcs = {s.subnet_id: s.vpc_id for s in user_specified_subnets} + cli_logger.abort( + f"Subnets specified in more than one VPC for node type `{node_type_key}`! " + f"Please ensure that all subnets share the same VPC and retry your " + "request. Subnet VPCs: {}", + subnet_vpcs, + ) + return subnets, first_subnet_vpc_id + + +def _configure_subnet(config): + ec2 = _resource("ec2", config) + + # If head or worker security group is specified, filter down to subnets + # belonging to the same VPC as the security group. + sg_ids = [] + for node_type in config["available_node_types"].values(): + node_config = node_type["node_config"] + sg_ids.extend(node_config.get("SecurityGroupIds", [])) + if sg_ids: + vpc_id_of_sg = _get_vpc_id_of_sg(sg_ids, config) + else: + vpc_id_of_sg = None + + # map from node type key -> source of SubnetIds field + subnet_src_info = {} + _set_config_info(subnet_src=subnet_src_info) + all_subnets = list(ec2.subnets.all()) + # separate node types with and without user-specified subnets + node_types_subnets = [] + node_types_no_subnets = [] + for key, node_type in config["available_node_types"].items(): + if "SubnetIds" in node_type["node_config"]: + node_types_subnets.append((key, node_type)) + else: + node_types_no_subnets.append((key, node_type)) + + vpc_id = None + + # iterate over node types with user-specified subnets first... + for key, node_type in node_types_subnets: + node_config = node_type["node_config"] + user_subnets = _get_subnets_or_die(ec2, tuple(node_config["SubnetIds"])) + subnet_ids, vpc_id = _usable_subnet_ids( + user_subnets, + all_subnets, + azs=config["provider"].get("availability_zone"), + vpc_id_of_sg=vpc_id_of_sg, + use_internal_ips=config["provider"].get("use_internal_ips", False), + node_type_key=key, + ) + subnet_src_info[key] = "config" + + # lock-in a good VPC shared by the last set of user-specified subnets... + if vpc_id and not vpc_id_of_sg: + vpc_id_of_sg = vpc_id + + # iterate over node types without user-specified subnets last... + for key, node_type in node_types_no_subnets: + node_config = node_type["node_config"] + subnet_ids, vpc_id = _usable_subnet_ids( + None, + all_subnets, + azs=config["provider"].get("availability_zone"), + vpc_id_of_sg=vpc_id_of_sg, + use_internal_ips=config["provider"].get("use_internal_ips", False), + node_type_key=key, + ) + subnet_src_info[key] = "default" + node_config["SubnetIds"] = subnet_ids + + return config + + +def _get_vpc_id_of_sg(sg_ids: List[str], config: Dict[str, Any]) -> str: + """Returns the VPC id of the security groups with the provided security + group ids. + + Errors if the provided security groups belong to multiple VPCs. + Errors if no security group with any of the provided ids is identified. + """ + # sort security group IDs to support deterministic unit test stubbing + sg_ids = sorted(set(sg_ids)) + + ec2 = _resource("ec2", config) + filters = [{"Name": "group-id", "Values": sg_ids}] + security_groups = ec2.security_groups.filter(Filters=filters) + vpc_ids = [sg.vpc_id for sg in security_groups] + vpc_ids = list(set(vpc_ids)) + + multiple_vpc_msg = ( + "All security groups specified in the cluster config " + "should belong to the same VPC." + ) + cli_logger.doassert(len(vpc_ids) <= 1, multiple_vpc_msg) + assert len(vpc_ids) <= 1, multiple_vpc_msg + + no_sg_msg = ( + "Failed to detect a security group with id equal to any of " + "the configured SecurityGroupIds." + ) + cli_logger.doassert(len(vpc_ids) > 0, no_sg_msg) + assert len(vpc_ids) > 0, no_sg_msg + + return vpc_ids[0] + + +def _configure_security_group(config): + # map from node type key -> source of SecurityGroupIds field + security_group_info_src = {} + _set_config_info(security_group_src=security_group_info_src) + + for node_type_key in config["available_node_types"]: + security_group_info_src[node_type_key] = "config" + + node_types_to_configure = [ + node_type_key + for node_type_key, node_type in config["available_node_types"].items() + if "SecurityGroupIds" not in node_type["node_config"] + ] + if not node_types_to_configure: + return config # have user-defined groups + head_node_type = config["head_node_type"] + if config["head_node_type"] in node_types_to_configure: + # configure head node security group last for determinism + # in tests + node_types_to_configure.remove(head_node_type) + node_types_to_configure.append(head_node_type) + security_groups = _upsert_security_groups(config, node_types_to_configure) + + for node_type_key in node_types_to_configure: + node_config = config["available_node_types"][node_type_key]["node_config"] + sg = security_groups[node_type_key] + node_config["SecurityGroupIds"] = [sg.id] + security_group_info_src[node_type_key] = "default" + + return config + + +def _check_ami(config): + """Provide helpful message for missing ImageId for node configuration.""" + + # map from node type key -> source of ImageId field + ami_src_info = {key: "config" for key in config["available_node_types"]} + _set_config_info(ami_src=ami_src_info) + + region = config["provider"]["region"] + default_ami = DEFAULT_AMI.get(region) + + for key, node_type in config["available_node_types"].items(): + node_config = node_type["node_config"] + node_ami = node_config.get("ImageId", "").lower() + if node_ami in ["", "latest_dlami"]: + if not default_ami: + cli_logger.abort( + f"Node type `{key}` has no ImageId in its node_config " + f"and no default AMI is available for the region `{region}`. " + "ImageId will need to be set manually in your cluster config." + ) + else: + node_config["ImageId"] = default_ami + ami_src_info[key] = "dlami" + + +def _upsert_security_groups(config, node_types): + security_groups = _get_or_create_vpc_security_groups(config, node_types) + _upsert_security_group_rules(config, security_groups) + + return security_groups + + +def _get_or_create_vpc_security_groups(conf, node_types): + # Figure out which VPC each node_type is in... + ec2 = _resource("ec2", conf) + node_type_to_vpc = { + node_type: _get_vpc_id_or_die( + ec2, + conf["available_node_types"][node_type]["node_config"]["SubnetIds"][0], + ) + for node_type in node_types + } + + # Generate the name of the security group we're looking for... + expected_sg_name = ( + conf["provider"] + .get("security_group", {}) + .get("GroupName", SECURITY_GROUP_TEMPLATE.format(conf["cluster_name"])) + ) + + # Figure out which security groups with this name exist for each VPC... + vpc_to_existing_sg = { + sg.vpc_id: sg + for sg in _get_security_groups( + conf, + node_type_to_vpc.values(), + [expected_sg_name], + ) + } + + # Lazily create any security group we're missing for each VPC... + vpc_to_sg = LazyDefaultDict( + partial(_create_security_group, conf, group_name=expected_sg_name), + vpc_to_existing_sg, + ) + + # Then return a mapping from each node_type to its security group... + return { + node_type: vpc_to_sg[vpc_id] for node_type, vpc_id in node_type_to_vpc.items() + } + + +def _get_vpc_id_or_die(ec2, subnet_id: str): + subnets = _get_subnets_or_die(ec2, (subnet_id,)) + cli_logger.doassert( + len(subnets) == 1, + f"Expected 1 subnet with ID `{subnet_id}` but found {len(subnets)}", + ) + return subnets[0].vpc_id + + +@lru_cache() +def _get_subnets_or_die(ec2, subnet_ids: Tuple[str]): + # Remove any duplicates as multiple interfaces are allowed to use same subnet + subnet_ids = tuple(Counter(subnet_ids).keys()) + subnets = list( + ec2.subnets.filter(Filters=[{"Name": "subnet-id", "Values": list(subnet_ids)}]) + ) + + # TODO: better error message + cli_logger.doassert( + len(subnets) == len(subnet_ids), "Not all subnet IDs found: {}", subnet_ids + ) + assert len(subnets) == len(subnet_ids), "Subnet ID not found: {}".format(subnet_ids) + return subnets + + +def _get_security_group(config, vpc_id, group_name): + security_group = _get_security_groups(config, [vpc_id], [group_name]) + return None if not security_group else security_group[0] + + +def _get_security_groups(config, vpc_ids, group_names): + unique_vpc_ids = list(set(vpc_ids)) + unique_group_names = set(group_names) + + ec2 = _resource("ec2", config) + existing_groups = list( + ec2.security_groups.filter( + Filters=[{"Name": "vpc-id", "Values": unique_vpc_ids}] + ) + ) + filtered_groups = [ + sg for sg in existing_groups if sg.group_name in unique_group_names + ] + return filtered_groups + + +def _create_security_group(config, vpc_id, group_name): + client = _client("ec2", config) + client.create_security_group( + Description="Auto-created security group for Ray workers", + GroupName=group_name, + VpcId=vpc_id, + TagSpecifications=[ + { + "ResourceType": "security-group", + "Tags": [ + {"Key": RAY, "Value": "true"}, + {"Key": "ray-cluster-name", "Value": config["cluster_name"]}, + ], + }, + ], + ) + security_group = _get_security_group(config, vpc_id, group_name) + cli_logger.doassert(security_group, "Failed to create security group") # err msg + + cli_logger.verbose( + "Created new security group {}", + cf.bold(security_group.group_name), + _tags=dict(id=security_group.id), + ) + cli_logger.doassert(security_group, "Failed to create security group") # err msg + assert security_group, "Failed to create security group" + return security_group + + +def _upsert_security_group_rules(conf, security_groups): + sgids = {sg.id for sg in security_groups.values()} + + # Update sgids to include user-specified security groups. + # This is necessary if the user specifies the head node type's security + # groups but not the worker's, or vice-versa. + for node_type in conf["available_node_types"]: + sgids.update( + conf["available_node_types"][node_type].get("SecurityGroupIds", []) + ) + + # sort security group items for deterministic inbound rule config order + # (mainly supports more precise stub-based boto3 unit testing) + for node_type, sg in sorted(security_groups.items()): + sg = security_groups[node_type] + if not sg.ip_permissions: + _update_inbound_rules(sg, sgids, conf) + + +def _update_inbound_rules(target_security_group, sgids, config): + extended_rules = ( + config["provider"].get("security_group", {}).get("IpPermissions", []) + ) + ip_permissions = _create_default_inbound_rules(sgids, extended_rules) + target_security_group.authorize_ingress(IpPermissions=ip_permissions) + + +def _create_default_inbound_rules(sgids, extended_rules=None): + if extended_rules is None: + extended_rules = [] + intracluster_rules = _create_default_intracluster_inbound_rules(sgids) + ssh_rules = _create_default_ssh_inbound_rules() + merged_rules = itertools.chain( + intracluster_rules, + ssh_rules, + extended_rules, + ) + return list(merged_rules) + + +def _create_default_intracluster_inbound_rules(intracluster_sgids): + return [ + { + "FromPort": -1, + "ToPort": -1, + "IpProtocol": "-1", + "UserIdGroupPairs": [ + {"GroupId": security_group_id} + for security_group_id in sorted(intracluster_sgids) + # sort security group IDs for deterministic IpPermission models + # (mainly supports more precise stub-based boto3 unit testing) + ], + } + ] + + +def _create_default_ssh_inbound_rules(): + return [ + { + "FromPort": 22, + "ToPort": 22, + "IpProtocol": "tcp", + "IpRanges": [{"CidrIp": "0.0.0.0/0"}], + } + ] + + +def _get_role(role_name, config): + iam = _resource("iam", config) + role = iam.Role(role_name) + try: + role.load() + return role + except botocore.exceptions.ClientError as exc: + if exc.response.get("Error", {}).get("Code") == "NoSuchEntity": + return None + else: + handle_boto_error( + exc, + "Failed to fetch IAM role data for {} from AWS.", + cf.bold(role_name), + ) + raise exc + + +def _get_instance_profile(profile_name, config): + iam = _resource("iam", config) + profile = iam.InstanceProfile(profile_name) + try: + profile.load() + return profile + except botocore.exceptions.ClientError as exc: + if exc.response.get("Error", {}).get("Code") == "NoSuchEntity": + return None + else: + handle_boto_error( + exc, + "Failed to fetch IAM instance profile data for {} from AWS.", + cf.bold(profile_name), + ) + raise exc + + +def _get_key(key_name, config): + ec2 = _resource("ec2", config) + try: + for key in ec2.key_pairs.filter( + Filters=[{"Name": "key-name", "Values": [key_name]}] + ): + if key.name == key_name: + return key + except botocore.exceptions.ClientError as exc: + handle_boto_error( + exc, "Failed to fetch EC2 key pair {} from AWS.", cf.bold(key_name) + ) + raise exc + + +def _configure_from_launch_template(config: Dict[str, Any]) -> Dict[str, Any]: + """Merges any launch template data referenced by the node config of all + available node type's into their parent node config. Any parameters + specified in node config override the same parameters in the launch + template, in compliance with the behavior of the ec2.create_instances + API. + + Args: + config (Dict[str, Any]): config to bootstrap + + Returns: + config (Dict[str, Any]): The input config with all launch template + data merged into the node config of all available node types. If no + launch template data is found, then the config is returned + unchanged. + + Raises: + ValueError: When no launch template is found for the given launch template + [name|id] and version, or more than one launch template is found. + """ + # create a copy of the input config to modify + config = copy.deepcopy(config) + node_types = config["available_node_types"] + + # iterate over sorted node types to support deterministic unit test stubs + for name, node_type in sorted(node_types.items()): + node_types[name] = _configure_node_type_from_launch_template(config, node_type) + return config + + +def _configure_node_type_from_launch_template( + config: Dict[str, Any], node_type: Dict[str, Any] +) -> Dict[str, Any]: + """Merges any launch template data referenced by the given node type's + node config into the parent node config. Any parameters specified in + node config override the same parameters in the launch template. + + Args: + config (Dict[str, Any]): config to bootstrap + node_type (Dict[str, Any]): node type config to bootstrap + + Returns: + node_type (Dict[str, Any]): The input config with all launch template + data merged into the node config of the input node type. If no + launch template data is found, then the config is returned + unchanged. + + Raises: + ValueError: When no launch template is found for the given launch template + [name|id] and version, or more than one launch template is found. + """ + # create a copy of the input config to modify + node_type = copy.deepcopy(node_type) + + node_cfg = node_type["node_config"] + if "LaunchTemplate" in node_cfg: + node_type["node_config"] = _configure_node_cfg_from_launch_template( + config, node_cfg + ) + return node_type + + +def _configure_node_cfg_from_launch_template( + config: Dict[str, Any], node_cfg: Dict[str, Any] +) -> Dict[str, Any]: + """Merges any launch template data referenced by the given node type's + node config into the parent node config. Any parameters specified in + node config override the same parameters in the launch template. + + Note that this merge is simply a bidirectional dictionary update, from + the node config to the launch template data, and from the launch + template data to the node config. Thus, the final result captures the + relative complement of launch template data with respect to node config, + and allows all subsequent config bootstrapping code paths to act as + if the complement was explicitly specified in the user's node config. A + deep merge of nested elements like tag specifications isn't required + here, since the AWSNodeProvider's ec2.create_instances call will do this + for us after it fetches the referenced launch template data. + + Args: + config (Dict[str, Any]): config to bootstrap + node_cfg (Dict[str, Any]): node config to bootstrap + + Returns: + node_cfg (Dict[str, Any]): The input node config merged with all launch + template data. If no launch template data is found, then the node + config is returned unchanged. + + Raises: + ValueError: When no launch template is found for the given launch template + [name|id] and version, or more than one launch template is found. + """ + # create a copy of the input config to modify + node_cfg = copy.deepcopy(node_cfg) + + ec2 = _client("ec2", config) + kwargs = copy.deepcopy(node_cfg["LaunchTemplate"]) + template_version = str(kwargs.pop("Version", "$Default")) + # save the launch template version as a string to prevent errors from + # passing an integer to ec2.create_instances in AWSNodeProvider + node_cfg["LaunchTemplate"]["Version"] = template_version + kwargs["Versions"] = [template_version] if template_version else [] + + template = ec2.describe_launch_template_versions(**kwargs) + lt_versions = template["LaunchTemplateVersions"] + if len(lt_versions) != 1: + raise ValueError( + f"Expected to find 1 launch template but found " f"{len(lt_versions)}" + ) + + lt_data = template["LaunchTemplateVersions"][0]["LaunchTemplateData"] + # override launch template parameters with explicit node config parameters + lt_data.update(node_cfg) + # copy all new launch template parameters back to node config + node_cfg.update(lt_data) + + return node_cfg + + +def _configure_from_network_interfaces(config: Dict[str, Any]) -> Dict[str, Any]: + """Copies all network interface subnet and security group IDs up to their + parent node config for each available node type. + + Args: + config (Dict[str, Any]): config to bootstrap + + Returns: + config (Dict[str, Any]): The input config with all network interface + subnet and security group IDs copied into the node config of all + available node types. If no network interfaces are found, then the + config is returned unchanged. + + Raises: + ValueError: If [1] subnet and security group IDs exist at both the + node config and network interface levels, [2] any network interface + doesn't have a subnet defined, or [3] any network interface doesn't + have a security group defined. + """ + # create a copy of the input config to modify + config = copy.deepcopy(config) + + node_types = config["available_node_types"] + for name, node_type in node_types.items(): + node_types[name] = _configure_node_type_from_network_interface(node_type) + return config + + +def _configure_node_type_from_network_interface( + node_type: Dict[str, Any] +) -> Dict[str, Any]: + """Copies all network interface subnet and security group IDs up to the + parent node config for the given node type. + + Args: + node_type (Dict[str, Any]): node type config to bootstrap + + Returns: + node_type (Dict[str, Any]): The input config with all network interface + subnet and security group IDs copied into the node config of the + given node type. If no network interfaces are found, then the + config is returned unchanged. + + Raises: + ValueError: If [1] subnet and security group IDs exist at both the + node config and network interface levels, [2] any network interface + doesn't have a subnet defined, or [3] any network interface doesn't + have a security group defined. + """ + # create a copy of the input config to modify + node_type = copy.deepcopy(node_type) + + node_cfg = node_type["node_config"] + if "NetworkInterfaces" in node_cfg: + node_type[ + "node_config" + ] = _configure_subnets_and_groups_from_network_interfaces(node_cfg) + return node_type + + +def _configure_subnets_and_groups_from_network_interfaces( + node_cfg: Dict[str, Any] +) -> Dict[str, Any]: + """Copies all network interface subnet and security group IDs into their + parent node config. + + Args: + node_cfg (Dict[str, Any]): node config to bootstrap + + Returns: + node_cfg (Dict[str, Any]): node config with all copied network + interface subnet and security group IDs + + Raises: + ValueError: If [1] subnet and security group IDs exist at both the + node config and network interface levels, [2] any network interface + doesn't have a subnet defined, or [3] any network interface doesn't + have a security group defined. + """ + # create a copy of the input config to modify + node_cfg = copy.deepcopy(node_cfg) + + # If NetworkInterfaces are defined, SubnetId and SecurityGroupIds + # can't be specified in the same node type config. + conflict_keys = ["SubnetId", "SubnetIds", "SecurityGroupIds"] + if any(conflict in node_cfg for conflict in conflict_keys): + raise ValueError( + "If NetworkInterfaces are defined, subnets and security groups " + "must ONLY be given in each NetworkInterface." + ) + subnets = _subnets_in_network_config(node_cfg) + if not all(subnets): + raise ValueError( + "NetworkInterfaces are defined but at least one is missing a " + "subnet. Please ensure all interfaces have a subnet assigned." + ) + security_groups = _security_groups_in_network_config(node_cfg) + if not all(security_groups): + raise ValueError( + "NetworkInterfaces are defined but at least one is missing a " + "security group. Please ensure all interfaces have a security " + "group assigned." + ) + node_cfg["SubnetIds"] = subnets + node_cfg["SecurityGroupIds"] = list(itertools.chain(*security_groups)) + + return node_cfg + + +def _subnets_in_network_config(config: Dict[str, Any]) -> List[str]: + """ + Returns all subnet IDs found in the given node config's network interfaces. + + Args: + config (Dict[str, Any]): node config + Returns: + subnet_ids (List[str]): List of subnet IDs for all network interfaces, + or an empty list if no network interfaces are defined. An empty string + is returned for each missing network interface subnet ID. + """ + return [ni.get("SubnetId", "") for ni in config.get("NetworkInterfaces", [])] + + +def _security_groups_in_network_config(config: Dict[str, Any]) -> List[List[str]]: + """ + Returns all security group IDs found in the given node config's network + interfaces. + + Args: + config (Dict[str, Any]): node config + Returns: + security_group_ids (List[List[str]]): List of security group ID lists + for all network interfaces, or an empty list if no network interfaces + are defined. An empty list is returned for each missing network + interface security group list. + """ + return [ni.get("Groups", []) for ni in config.get("NetworkInterfaces", [])] + + +def _client(name, config): + return _resource(name, config).meta.client + + +def _resource(name, config): + region = config["provider"]["region"] + aws_credentials = config["provider"].get("aws_credentials", {}) + return resource_cache(name, region, **aws_credentials) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..3b8673dbb7300bf56830e34dabf075cbfd5311cc --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/node_provider.py @@ -0,0 +1,713 @@ +import copy +import logging +import sys +import threading +import time +from collections import OrderedDict, defaultdict +from typing import Any, Dict, List + +import botocore +from boto3.resources.base import ServiceResource + +import ray +import ray._private.ray_constants as ray_constants +from ray.autoscaler._private.aws.cloudwatch.cloudwatch_helper import ( + CLOUDWATCH_AGENT_INSTALLED_AMI_TAG, + CLOUDWATCH_AGENT_INSTALLED_TAG, + CloudwatchHelper, +) +from ray.autoscaler._private.aws.config import bootstrap_aws +from ray.autoscaler._private.aws.utils import ( + boto_exception_handler, + client_cache, + resource_cache, +) +from ray.autoscaler._private.cli_logger import cf, cli_logger +from ray.autoscaler._private.constants import BOTO_CREATE_MAX_RETRIES, BOTO_MAX_RETRIES +from ray.autoscaler._private.log_timer import LogTimer +from ray.autoscaler.node_launch_exception import NodeLaunchException +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + TAG_RAY_CLUSTER_NAME, + TAG_RAY_LAUNCH_CONFIG, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_NAME, + TAG_RAY_USER_NODE_TYPE, +) + +logger = logging.getLogger(__name__) + +TAG_BATCH_DELAY = 1 +LIST_RETRY_DELAY_SEC = 1 + + +def to_aws_format(tags): + """Convert the Ray node name tag to the AWS-specific 'Name' tag.""" + + if TAG_RAY_NODE_NAME in tags: + tags["Name"] = tags[TAG_RAY_NODE_NAME] + del tags[TAG_RAY_NODE_NAME] + return tags + + +def from_aws_format(tags): + """Convert the AWS-specific 'Name' tag to the Ray node name tag.""" + + if "Name" in tags: + tags[TAG_RAY_NODE_NAME] = tags["Name"] + del tags["Name"] + return tags + + +def make_ec2_resource(region, max_retries, aws_credentials=None) -> ServiceResource: + """Make client, retrying requests up to `max_retries`.""" + aws_credentials = aws_credentials or {} + return resource_cache("ec2", region, max_retries, **aws_credentials) + + +def list_ec2_instances( + region: str, aws_credentials: Dict[str, Any] = None +) -> List[Dict[str, Any]]: + """Get all instance-types/resources available in the user's AWS region. + Args: + region: the region of the AWS provider. e.g., "us-west-2". + Returns: + final_instance_types: a list of instances. An example of one element in + the list: + {'InstanceType': 'm5a.xlarge', 'ProcessorInfo': + {'SupportedArchitectures': ['x86_64'], 'SustainedClockSpeedInGhz': + 2.5},'VCpuInfo': {'DefaultVCpus': 4, 'DefaultCores': 2, + 'DefaultThreadsPerCore': 2, 'ValidCores': [2], + 'ValidThreadsPerCore': [1, 2]}, 'MemoryInfo': {'SizeInMiB': 16384}, + ...} + + """ + final_instance_types = [] + aws_credentials = aws_credentials or {} + ec2 = client_cache("ec2", region, BOTO_MAX_RETRIES, **aws_credentials) + instance_types = ec2.describe_instance_types() + final_instance_types.extend(copy.deepcopy(instance_types["InstanceTypes"])) + while "NextToken" in instance_types: + instance_types = ec2.describe_instance_types( + NextToken=instance_types["NextToken"] + ) + final_instance_types.extend(copy.deepcopy(instance_types["InstanceTypes"])) + + return final_instance_types + + +class AWSNodeProvider(NodeProvider): + max_terminate_nodes = 1000 + + def __init__(self, provider_config, cluster_name): + NodeProvider.__init__(self, provider_config, cluster_name) + self.cache_stopped_nodes = provider_config.get("cache_stopped_nodes", True) + aws_credentials = provider_config.get("aws_credentials") + + self.ec2 = make_ec2_resource( + region=provider_config["region"], + max_retries=BOTO_MAX_RETRIES, + aws_credentials=aws_credentials, + ) + self.ec2_fail_fast = make_ec2_resource( + region=provider_config["region"], + max_retries=0, + aws_credentials=aws_credentials, + ) + + # Tags that we believe to actually be on EC2. + self.tag_cache = {} + # Tags that we will soon upload. + self.tag_cache_pending = defaultdict(dict) + # Number of threads waiting for a batched tag update. + self.batch_thread_count = 0 + self.batch_update_done = threading.Event() + self.batch_update_done.set() + self.ready_for_new_batch = threading.Event() + self.ready_for_new_batch.set() + self.tag_cache_lock = threading.Lock() + self.count_lock = threading.Lock() + # Prevent concurrent create_node calls to get the same stopped/stopping node to reuse. + self._reuse_node_lock = threading.Lock() + + # Cache of node objects from the last nodes() call. This avoids + # excessive DescribeInstances requests. + self.cached_nodes = {} + + def non_terminated_nodes(self, tag_filters): + # Note that these filters are acceptable because they are set on + # node initialization, and so can never be sitting in the cache. + tag_filters = to_aws_format(tag_filters) + filters = [ + { + "Name": "instance-state-name", + "Values": ["pending", "running"], + }, + { + "Name": "tag:{}".format(TAG_RAY_CLUSTER_NAME), + "Values": [self.cluster_name], + }, + ] + for k, v in tag_filters.items(): + filters.append( + { + "Name": "tag:{}".format(k), + "Values": [v], + } + ) + + with boto_exception_handler("Failed to fetch running instances from AWS."): + nodes = list(self.ec2.instances.filter(Filters=filters)) + + # Populate the tag cache with initial information if necessary + for node in nodes: + if node.id in self.tag_cache: + continue + + self.tag_cache[node.id] = from_aws_format( + {x["Key"]: x["Value"] for x in node.tags} + ) + + self.cached_nodes = {node.id: node for node in nodes} + return [node.id for node in nodes] + + def is_running(self, node_id): + node = self._get_cached_node(node_id) + return node.state["Name"] == "running" + + def is_terminated(self, node_id): + node = self._get_cached_node(node_id) + state = node.state["Name"] + return state not in ["running", "pending"] + + def node_tags(self, node_id): + with self.tag_cache_lock: + d1 = self.tag_cache[node_id] + d2 = self.tag_cache_pending.get(node_id, {}) + return dict(d1, **d2) + + def external_ip(self, node_id): + node = self._get_cached_node(node_id) + + if node.public_ip_address is None: + node = self._get_node(node_id) + + return node.public_ip_address + + def internal_ip(self, node_id): + node = self._get_cached_node(node_id) + + if node.private_ip_address is None: + node = self._get_node(node_id) + + return node.private_ip_address + + def set_node_tags(self, node_id, tags): + is_batching_thread = False + with self.tag_cache_lock: + if not self.tag_cache_pending: + is_batching_thread = True + # Wait for threads in the last batch to exit + self.ready_for_new_batch.wait() + self.ready_for_new_batch.clear() + self.batch_update_done.clear() + self.tag_cache_pending[node_id].update(tags) + + if is_batching_thread: + time.sleep(TAG_BATCH_DELAY) + with self.tag_cache_lock: + self._update_node_tags() + self.batch_update_done.set() + + with self.count_lock: + self.batch_thread_count += 1 + self.batch_update_done.wait() + + with self.count_lock: + self.batch_thread_count -= 1 + if self.batch_thread_count == 0: + self.ready_for_new_batch.set() + + def _update_node_tags(self): + batch_updates = defaultdict(list) + + for node_id, tags in self.tag_cache_pending.items(): + for x in tags.items(): + batch_updates[x].append(node_id) + self.tag_cache[node_id].update(tags) + + self.tag_cache_pending = defaultdict(dict) + + self._create_tags(batch_updates) + + def _create_tags(self, batch_updates): + for (k, v), node_ids in batch_updates.items(): + m = "Set tag {}={} on {}".format(k, v, node_ids) + with LogTimer("AWSNodeProvider: {}".format(m)): + if k == TAG_RAY_NODE_NAME: + k = "Name" + self.ec2.meta.client.create_tags( + Resources=node_ids, + Tags=[{"Key": k, "Value": v}], + ) + + def create_node(self, node_config, tags, count) -> Dict[str, Any]: + """Creates instances. + + Returns dict mapping instance id to ec2.Instance object for the created + instances. + """ + # sort tags by key to support deterministic unit test stubbing + tags = OrderedDict(sorted(copy.deepcopy(tags).items())) + + reused_nodes_dict = {} + # Try to reuse previously stopped nodes with compatible configs + if self.cache_stopped_nodes: + # TODO(ekl) this is breaking the abstraction boundary a little by + # peeking into the tag set. + filters = [ + { + "Name": "instance-state-name", + "Values": ["stopped", "stopping"], + }, + { + "Name": "tag:{}".format(TAG_RAY_CLUSTER_NAME), + "Values": [self.cluster_name], + }, + { + "Name": "tag:{}".format(TAG_RAY_NODE_KIND), + "Values": [tags[TAG_RAY_NODE_KIND]], + }, + { + "Name": "tag:{}".format(TAG_RAY_LAUNCH_CONFIG), + "Values": [tags[TAG_RAY_LAUNCH_CONFIG]], + }, + ] + # This tag may not always be present. + if TAG_RAY_USER_NODE_TYPE in tags: + filters.append( + { + "Name": "tag:{}".format(TAG_RAY_USER_NODE_TYPE), + "Values": [tags[TAG_RAY_USER_NODE_TYPE]], + } + ) + + with self._reuse_node_lock: + reuse_nodes = list(self.ec2.instances.filter(Filters=filters))[:count] + reuse_node_ids = [n.id for n in reuse_nodes] + reused_nodes_dict = {n.id: n for n in reuse_nodes} + if reuse_nodes: + cli_logger.print( + # todo: handle plural vs singular? + "Reusing nodes {}. " + "To disable reuse, set `cache_stopped_nodes: False` " + "under `provider` in the cluster configuration.", + cli_logger.render_list(reuse_node_ids), + ) + + # todo: timed? + with cli_logger.group("Stopping instances to reuse"): + for node in reuse_nodes: + self.tag_cache[node.id] = from_aws_format( + {x["Key"]: x["Value"] for x in node.tags} + ) + if node.state["Name"] == "stopping": + cli_logger.print( + "Waiting for instance {} to stop", node.id + ) + node.wait_until_stopped() + + self.ec2.meta.client.start_instances(InstanceIds=reuse_node_ids) + for node_id in reuse_node_ids: + self.set_node_tags(node_id, tags) + count -= len(reuse_node_ids) + + created_nodes_dict = {} + if count: + created_nodes_dict = self._create_node(node_config, tags, count) + + all_created_nodes = reused_nodes_dict + all_created_nodes.update(created_nodes_dict) + return all_created_nodes + + @staticmethod + def _merge_tag_specs( + tag_specs: List[Dict[str, Any]], user_tag_specs: List[Dict[str, Any]] + ) -> None: + """ + Merges user-provided node config tag specifications into a base + list of node provider tag specifications. The base list of + node provider tag specs is modified in-place. + + This allows users to add tags and override values of existing + tags with their own, and only applies to the resource type + "instance". All other resource types are appended to the list of + tag specs. + + Args: + tag_specs (List[Dict[str, Any]]): base node provider tag specs + user_tag_specs (List[Dict[str, Any]]): user's node config tag specs + """ + + for user_tag_spec in user_tag_specs: + if user_tag_spec["ResourceType"] == "instance": + for user_tag in user_tag_spec["Tags"]: + exists = False + for tag in tag_specs[0]["Tags"]: + if user_tag["Key"] == tag["Key"]: + exists = True + tag["Value"] = user_tag["Value"] + break + if not exists: + tag_specs[0]["Tags"] += [user_tag] + else: + tag_specs += [user_tag_spec] + + def _create_node(self, node_config, tags, count): + created_nodes_dict = {} + + tags = to_aws_format(tags) + conf = node_config.copy() + + tag_pairs = [ + { + "Key": TAG_RAY_CLUSTER_NAME, + "Value": self.cluster_name, + } + ] + for k, v in tags.items(): + tag_pairs.append( + { + "Key": k, + "Value": v, + } + ) + if CloudwatchHelper.cloudwatch_config_exists(self.provider_config, "agent"): + cwa_installed = self._check_ami_cwa_installation(node_config) + if cwa_installed: + tag_pairs.extend( + [ + { + "Key": CLOUDWATCH_AGENT_INSTALLED_TAG, + "Value": "True", + } + ] + ) + tag_specs = [ + { + "ResourceType": "instance", + "Tags": tag_pairs, + } + ] + user_tag_specs = conf.get("TagSpecifications", []) + AWSNodeProvider._merge_tag_specs(tag_specs, user_tag_specs) + + # SubnetIds is not a real config key: we must resolve to a + # single SubnetId before invoking the AWS API. + subnet_ids = conf.pop("SubnetIds") + + # update config with min/max node counts and tag specs + conf.update({"MinCount": 1, "MaxCount": count, "TagSpecifications": tag_specs}) + + # Try to always launch in the first listed subnet. + subnet_idx = 0 + cli_logger_tags = {} + # NOTE: This ensures that we try ALL availability zones before + # throwing an error. + max_tries = max(BOTO_CREATE_MAX_RETRIES, len(subnet_ids)) + for attempt in range(1, max_tries + 1): + try: + if "NetworkInterfaces" in conf: + net_ifs = conf["NetworkInterfaces"] + # remove security group IDs previously copied from network + # interfaces (create_instances call fails otherwise) + conf.pop("SecurityGroupIds", None) + cli_logger_tags["network_interfaces"] = str(net_ifs) + else: + subnet_id = subnet_ids[subnet_idx % len(subnet_ids)] + conf["SubnetId"] = subnet_id + cli_logger_tags["subnet_id"] = subnet_id + + created = self.ec2_fail_fast.create_instances(**conf) + created_nodes_dict = {n.id: n for n in created} + + # todo: timed? + # todo: handle plurality? + with cli_logger.group( + "Launched {} nodes", count, _tags=cli_logger_tags + ): + for instance in created: + # NOTE(maximsmol): This is needed for mocking + # boto3 for tests. This is likely a bug in moto + # but AWS docs don't seem to say. + # You can patch moto/ec2/responses/instances.py + # to fix this (add to EC2_RUN_INSTANCES) + + # The correct value is technically + # {"code": "0", "Message": "pending"} + state_reason = "pending" + if instance.state_reason: + state_reason = ( + instance.state_reason["Message"] or state_reason + ) + + cli_logger.print( + "Launched instance {}", + instance.instance_id, + _tags=dict( + state=instance.state["Name"], + info=state_reason, + ), + ) + break + except botocore.exceptions.ClientError as exc: + # Launch failure may be due to instance type availability in + # the given AZ + subnet_idx += 1 + if attempt == max_tries: + try: + exc = NodeLaunchException( + category=exc.response["Error"]["Code"], + description=exc.response["Error"]["Message"], + src_exc_info=sys.exc_info(), + ) + except Exception: + # In theory, all ClientError's we expect to get should + # have these fields, but just in case we can't parse + # it, it's fine, just throw the original error. + logger.warning("Couldn't parse exception.", exc) + pass + cli_logger.abort( + "Failed to launch instances. Max attempts exceeded.", + exc=exc, + ) + else: + cli_logger.warning( + "create_instances: Attempt failed with {}, retrying.", exc + ) + + return created_nodes_dict + + def terminate_node(self, node_id): + node = self._get_cached_node(node_id) + if self.cache_stopped_nodes: + if node.spot_instance_request_id: + cli_logger.print( + "Terminating instance {} " + + cf.dimmed("(cannot stop spot instances, only terminate)"), + node_id, + ) # todo: show node name? + node.terminate() + else: + cli_logger.print( + "Stopping instance {} " + + cf.dimmed( + "(to terminate instead, " + "set `cache_stopped_nodes: False` " + "under `provider` in the cluster configuration)" + ), + node_id, + ) # todo: show node name? + node.stop() + else: + node.terminate() + + # TODO (Alex): We are leaking the tag cache here. Naively, we would + # want to just remove the cache entry here, but terminating can be + # asyncrhonous or error, which would result in a use after free error. + # If this leak becomes bad, we can garbage collect the tag cache when + # the node cache is updated. + + def _check_ami_cwa_installation(self, config): + response = self.ec2.meta.client.describe_images(ImageIds=[config["ImageId"]]) + cwa_installed = False + images = response.get("Images") + if images: + assert len(images) == 1, ( + f"Expected to find only 1 AMI with the given ID, " + f"but found {len(images)}." + ) + image_name = images[0].get("Name", "") + if CLOUDWATCH_AGENT_INSTALLED_AMI_TAG in image_name: + cwa_installed = True + return cwa_installed + + def terminate_nodes(self, node_ids): + if not node_ids: + return + + terminate_instances_func = self.ec2.meta.client.terminate_instances + stop_instances_func = self.ec2.meta.client.stop_instances + + # In some cases, this function stops some nodes, but terminates others. + # Each of these requires a different EC2 API call. So, we use the + # "nodes_to_terminate" dict below to keep track of exactly which API + # call will be used to stop/terminate which set of nodes. The key is + # the function to use, and the value is the list of nodes to terminate + # with that function. + nodes_to_terminate = {terminate_instances_func: [], stop_instances_func: []} + + if self.cache_stopped_nodes: + spot_ids = [] + on_demand_ids = [] + + for node_id in node_ids: + if self._get_cached_node(node_id).spot_instance_request_id: + spot_ids += [node_id] + else: + on_demand_ids += [node_id] + + if on_demand_ids: + # todo: show node names? + cli_logger.print( + "Stopping instances {} " + + cf.dimmed( + "(to terminate instead, " + "set `cache_stopped_nodes: False` " + "under `provider` in the cluster configuration)" + ), + cli_logger.render_list(on_demand_ids), + ) + + if spot_ids: + cli_logger.print( + "Terminating instances {} " + + cf.dimmed("(cannot stop spot instances, only terminate)"), + cli_logger.render_list(spot_ids), + ) + + nodes_to_terminate[stop_instances_func] = on_demand_ids + nodes_to_terminate[terminate_instances_func] = spot_ids + else: + nodes_to_terminate[terminate_instances_func] = node_ids + + max_terminate_nodes = ( + self.max_terminate_nodes + if self.max_terminate_nodes is not None + else len(node_ids) + ) + + for terminate_func, nodes in nodes_to_terminate.items(): + for start in range(0, len(nodes), max_terminate_nodes): + terminate_func(InstanceIds=nodes[start : start + max_terminate_nodes]) + + def _get_node(self, node_id): + """Refresh and get info for this node, updating the cache.""" + self.non_terminated_nodes({}) # Side effect: updates cache + + if node_id in self.cached_nodes: + return self.cached_nodes[node_id] + + # Node not in {pending, running} -- retry with a point query. This + # usually means the node was recently preempted or terminated. + # The EC2 API is eventually consistent. This means that an instance + # might not be immediately visible. So we need to retry the query a few times. + # See: https://docs.aws.amazon.com/ec2/latest/devguide/eventual-consistency.html + # and https://github.com/ray-project/ray/issues/51861 + for attempts in range(max(BOTO_MAX_RETRIES, 1)): # at least try once. + matches = list(self.ec2.instances.filter(InstanceIds=[node_id])) + if len(matches) == 1: + return matches[0] + cli_logger.warning( + "Attempt to fetch EC2 instances that have instance ID {}. Got {} matching EC2 instances. Will retry after {} second. This is retry number {}, and the maximum number of retries is {}.", + node_id, + len(matches), + LIST_RETRY_DELAY_SEC, + attempts + 1, + BOTO_MAX_RETRIES, + ) + time.sleep(LIST_RETRY_DELAY_SEC) + raise AssertionError("Invalid instance id {}".format(node_id)) + + def _get_cached_node(self, node_id): + """Return node info from cache if possible, otherwise fetches it.""" + if node_id in self.cached_nodes: + return self.cached_nodes[node_id] + + return self._get_node(node_id) + + @staticmethod + def bootstrap_config(cluster_config): + return bootstrap_aws(cluster_config) + + @staticmethod + def fillout_available_node_types_resources( + cluster_config: Dict[str, Any] + ) -> Dict[str, Any]: + """Fills out missing "resources" field for available_node_types.""" + if "available_node_types" not in cluster_config: + return cluster_config + cluster_config = copy.deepcopy(cluster_config) + + instances_list = list_ec2_instances( + cluster_config["provider"]["region"], + cluster_config["provider"].get("aws_credentials"), + ) + instances_dict = { + instance["InstanceType"]: instance for instance in instances_list + } + available_node_types = cluster_config["available_node_types"] + head_node_type = cluster_config["head_node_type"] + for node_type in available_node_types: + instance_type = available_node_types[node_type]["node_config"][ + "InstanceType" + ] + if instance_type in instances_dict: + cpus = instances_dict[instance_type]["VCpuInfo"]["DefaultVCpus"] + + autodetected_resources = {"CPU": cpus} + if node_type != head_node_type: + # we only autodetect worker node type memory resource + memory_total = instances_dict[instance_type]["MemoryInfo"][ + "SizeInMiB" + ] + memory_total = int(memory_total) * 1024 * 1024 + prop = 1 - ray_constants.DEFAULT_OBJECT_STORE_MEMORY_PROPORTION + memory_resources = int(memory_total * prop) + autodetected_resources["memory"] = memory_resources + + for ( + accelerator_manager + ) in ray._private.accelerators.get_all_accelerator_managers(): + num_accelerators = ( + accelerator_manager.get_ec2_instance_num_accelerators( + instance_type, instances_dict + ) + ) + accelerator_type = ( + accelerator_manager.get_ec2_instance_accelerator_type( + instance_type, instances_dict + ) + ) + if num_accelerators: + autodetected_resources[ + accelerator_manager.get_resource_name() + ] = num_accelerators + if accelerator_type: + autodetected_resources[ + f"accelerator_type:{accelerator_type}" + ] = 1 + + autodetected_resources.update( + available_node_types[node_type].get("resources", {}) + ) + if autodetected_resources != available_node_types[node_type].get( + "resources", {} + ): + available_node_types[node_type][ + "resources" + ] = autodetected_resources + logger.debug( + "Updating the resources of {} to {}.".format( + node_type, autodetected_resources + ) + ) + else: + raise ValueError( + "Instance type " + + instance_type + + " is not available in AWS region: " + + cluster_config["provider"]["region"] + + "." + ) + return cluster_config diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/aws/utils.py b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..72dda831a8093b460f238bb69266da1a35db8037 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/aws/utils.py @@ -0,0 +1,181 @@ +from collections import defaultdict +from functools import lru_cache + +import boto3 +from boto3.exceptions import ResourceNotExistsError +from boto3.resources.base import ServiceResource +from botocore.client import BaseClient +from botocore.config import Config + +from ray.autoscaler._private.cli_logger import cf, cli_logger +from ray.autoscaler._private.constants import BOTO_MAX_RETRIES + + +class LazyDefaultDict(defaultdict): + """ + LazyDefaultDict(default_factory[, ...]) --> dict with default factory + + The default factory is call with the key argument to produce + a new value when a key is not present, in __getitem__ only. + A LazyDefaultDict compares equal to a dict with the same items. + All remaining arguments are treated the same as if they were + passed to the dict constructor, including keyword arguments. + """ + + def __missing__(self, key): + """ + __missing__(key) # Called by __getitem__ for missing key; pseudo-code: + if self.default_factory is None: raise KeyError((key,)) + self[key] = value = self.default_factory(key) + return value + """ + self[key] = self.default_factory(key) + return self[key] + + +def handle_boto_error(exc, msg, *args, **kwargs): + error_code = None + error_info = None + # todo: not sure if these exceptions always have response + if hasattr(exc, "response"): + error_info = exc.response.get("Error", None) + if error_info is not None: + error_code = error_info.get("Code", None) + + generic_message_args = [ + "{}\nError code: {}", + msg.format(*args, **kwargs), + cf.bold(error_code), + ] + + # apparently + # ExpiredTokenException + # ExpiredToken + # RequestExpired + # are all the same pretty much + credentials_expiration_codes = [ + "ExpiredTokenException", + "ExpiredToken", + "RequestExpired", + ] + + if error_code in credentials_expiration_codes: + # "An error occurred (ExpiredToken) when calling the + # GetInstanceProfile operation: The security token + # included in the request is expired" + + # "An error occurred (RequestExpired) when calling the + # DescribeKeyPairs operation: Request has expired." + + token_command = ( + "aws sts get-session-token " + "--serial-number arn:aws:iam::" + + cf.underlined("ROOT_ACCOUNT_ID") + + ":mfa/" + + cf.underlined("AWS_USERNAME") + + " --token-code " + + cf.underlined("TWO_FACTOR_AUTH_CODE") + ) + + secret_key_var = ( + "export AWS_SECRET_ACCESS_KEY = " + + cf.underlined("REPLACE_ME") + + " # found at Credentials.SecretAccessKey" + ) + session_token_var = ( + "export AWS_SESSION_TOKEN = " + + cf.underlined("REPLACE_ME") + + " # found at Credentials.SessionToken" + ) + access_key_id_var = ( + "export AWS_ACCESS_KEY_ID = " + + cf.underlined("REPLACE_ME") + + " # found at Credentials.AccessKeyId" + ) + + # fixme: replace with a Github URL that points + # to our repo + aws_session_script_url = ( + "https://gist.github.com/maximsmol/a0284e1d97b25d417bd9ae02e5f450cf" + ) + + cli_logger.verbose_error(*generic_message_args) + cli_logger.verbose(vars(exc)) + + cli_logger.panic("Your AWS session has expired.") + cli_logger.newline() + cli_logger.panic("You can request a new one using") + cli_logger.panic(cf.bold(token_command)) + cli_logger.panic("then expose it to Ray by setting") + cli_logger.panic(cf.bold(secret_key_var)) + cli_logger.panic(cf.bold(session_token_var)) + cli_logger.panic(cf.bold(access_key_id_var)) + cli_logger.newline() + cli_logger.panic("You can find a script that automates this at:") + cli_logger.panic(cf.underlined(aws_session_script_url)) + # Do not re-raise the exception here because it looks awful + # and we already print all the info in verbose + cli_logger.abort() + + # todo: any other errors that we should catch separately? + + cli_logger.panic(*generic_message_args) + cli_logger.newline() + with cli_logger.verbatim_error_ctx("Boto3 error:"): + cli_logger.verbose("{}", str(vars(exc))) + cli_logger.panic("{}", str(exc)) + cli_logger.abort() + + +def boto_exception_handler(msg, *args, **kwargs): + # todo: implement timer + class ExceptionHandlerContextManager: + def __enter__(self): + pass + + def __exit__(self, type, value, tb): + import botocore + + if type is botocore.exceptions.ClientError: + handle_boto_error(value, msg, *args, **kwargs) + + return ExceptionHandlerContextManager() + + +@lru_cache() +def resource_cache( + name, region, max_retries=BOTO_MAX_RETRIES, **kwargs +) -> ServiceResource: + cli_logger.verbose( + "Creating AWS resource `{}` in `{}`", cf.bold(name), cf.bold(region) + ) + kwargs.setdefault( + "config", + Config(retries={"max_attempts": max_retries}), + ) + return boto3.resource( + name, + region, + **kwargs, + ) + + +@lru_cache() +def client_cache(name, region, max_retries=BOTO_MAX_RETRIES, **kwargs) -> BaseClient: + try: + # try to re-use a client from the resource cache first + return resource_cache(name, region, max_retries, **kwargs).meta.client + except ResourceNotExistsError: + # fall back for clients without an associated resource + cli_logger.verbose( + "Creating AWS client `{}` in `{}`", cf.bold(name), cf.bold(region) + ) + kwargs.setdefault( + "config", + Config(retries={"max_attempts": max_retries}), + ) + return boto3.client( + name, + region, + **kwargs, + ) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/cli_logger.py b/lib/python3.12/site-packages/ray/autoscaler/_private/cli_logger.py new file mode 100644 index 0000000000000000000000000000000000000000..5172891d311964c281c4e748cb72713dc361a3ed --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/cli_logger.py @@ -0,0 +1,825 @@ +"""Logger implementing the Command Line Interface. + +A replacement for the standard Python `logging` API +designed for implementing a better CLI UX for the cluster launcher. + +Supports color, bold text, italics, underlines, etc. +(depending on TTY features) +as well as indentation and other structured output. +""" +import inspect +import logging +import os +import sys +import time +from contextlib import contextmanager +from functools import wraps +from typing import Any, Callable, Dict, List, Optional, Tuple + +import click +import colorama + +# Import ray first to use the bundled colorama +import ray # noqa: F401 + +if sys.platform == "win32": + import msvcrt +else: + import select + + +class _ColorfulMock: + def __init__(self): + # do not do any color work + self.identity = lambda x: x + + self.colorful = self + self.colormode = None + + self.NO_COLORS = None + self.ANSI_8_COLORS = None + + def disable(self): + pass + + @contextmanager + def with_style(self, x): + class IdentityClass: + def __getattr__(self, name): + return lambda y: y + + yield IdentityClass() + + def __getattr__(self, name): + if name == "with_style": + return self.with_style + + return self.identity + + +try: + import colorful as _cf + from colorful.core import ColorfulString + + _cf.use_8_ansi_colors() +except ModuleNotFoundError: + # We mock Colorful to restrict the colors used for consistency + # anyway, so we also allow for not having colorful at all. + # If the Ray Core dependency on colorful is ever removed, + # the CliLogger code will still work. + class ColorfulString: + pass + + _cf = _ColorfulMock() + + +# We want to only allow specific formatting +# to prevent people from accidentally making bad looking color schemes. +# +# This is especially important since most will look bad on either light +# or dark themes. +class _ColorfulProxy: + _proxy_allowlist = [ + "disable", + "reset", + "bold", + "italic", + "underlined", + # used instead of `gray` as `dimmed` adapts to + # both light and dark themes + "dimmed", + "dodgerBlue", # group + "limeGreen", # success + "red", # error + "orange", # warning + "skyBlue", # label + "magenta", # syntax highlighting key words and symbols + "yellow", # syntax highlighting strings + ] + + def __getattr__(self, name): + res = getattr(_cf, name) + if callable(res) and name not in _ColorfulProxy._proxy_allowlist: + raise ValueError( + "Usage of the colorful method '" + name + "' is forbidden " + "by the proxy to keep a consistent color scheme. " + "Check `cli_logger.py` for allowed methods" + ) + return res + + +cf = _ColorfulProxy() + +colorama.init(strip=False) + + +def _external_caller_info(): + """Get the info from the caller frame. + + Used to override the logging function and line number with the correct + ones. See the comment on _patched_makeRecord for more info. + """ + + frame = inspect.currentframe() + caller = frame + levels = 0 + while caller.f_code.co_filename == __file__: + caller = caller.f_back + levels += 1 + return { + "lineno": caller.f_lineno, + "filename": os.path.basename(caller.f_code.co_filename), + } + + +def _format_msg( + msg: str, + *args: Any, + no_format: bool = None, + _tags: Dict[str, Any] = None, + _numbered: Tuple[str, int, int] = None, + **kwargs: Any, +): + """Formats a message for printing. + + Renders `msg` using the built-in `str.format` and the passed-in + `*args` and `**kwargs`. + + Args: + *args (Any): `.format` arguments for `msg`. + no_format (bool): + If `no_format` is `True`, + `.format` will not be called on the message. + + Useful if the output is user-provided or may otherwise + contain an unexpected formatting string (e.g. "{}"). + _tags (Dict[str, Any]): + key-value pairs to display at the end of + the message in square brackets. + + If a tag is set to `True`, it is printed without the value, + the presence of the tag treated as a "flag". + + E.g. `_format_msg("hello", _tags=dict(from=mom, signed=True))` + `hello [from=Mom, signed]` + _numbered (Tuple[str, int, int]): + `(brackets, i, n)` + + The `brackets` string is composed of two "bracket" characters, + `i` is the index, `n` is the total. + + The string `{i}/{n}` surrounded by the "brackets" is + prepended to the message. + + This is used to number steps in a procedure, with different + brackets specifying different major tasks. + + E.g. `_format_msg("hello", _numbered=("[]", 0, 5))` + `[0/5] hello` + + Returns: + The formatted message. + """ + + if isinstance(msg, str) or isinstance(msg, ColorfulString): + tags_str = "" + if _tags is not None: + tags_list = [] + for k, v in _tags.items(): + if v is True: + tags_list += [k] + continue + if v is False: + continue + + tags_list += [k + "=" + v] + if tags_list: + tags_str = cf.reset(cf.dimmed(" [{}]".format(", ".join(tags_list)))) + + numbering_str = "" + if _numbered is not None: + chars, i, n = _numbered + numbering_str = cf.dimmed(chars[0] + str(i) + "/" + str(n) + chars[1]) + " " + + if no_format: + # todo: throw if given args/kwargs? + return numbering_str + msg + tags_str + return numbering_str + msg.format(*args, **kwargs) + tags_str + + if kwargs: + raise ValueError("We do not support printing kwargs yet.") + + res = [msg, *args] + res = [str(x) for x in res] + return ", ".join(res) + + +# TODO: come up with a plan to unify logging. +# formatter = logging.Formatter( +# # TODO(maximsmol): figure out the required log level padding +# # width automatically +# fmt="[{asctime}] {levelname:6} {message}", +# datefmt="%x %X", +# # We want alignment on our level names +# style="{") + + +def _isatty(): + """More robust check for interactive terminal/tty.""" + try: + # https://stackoverflow.com/questions/6108330/ + # checking-for-interactive-shell-in-a-python-script + return sys.__stdin__.isatty() + except Exception: + # sometimes this can fail due to closed output + # either way, no-tty is generally safe fallback. + return False + + +class _CliLogger: + """Singleton class for CLI logging. + + Without calling 'cli_logger.configure', the CLILogger will default + to 'record' style logging. + + Attributes: + color_mode (str): + Can be "true", "false", or "auto". + + Enables or disables `colorful`. + + If `color_mode` is "auto", is set to `not stdout.isatty()` + indent_level (int): + The current indentation level. + + All messages will be indented by prepending `" " * indent_level` + vebosity (int): + Output verbosity. + + Low verbosity will disable `verbose` and `very_verbose` messages. + """ + + color_mode: str + # color_mode: Union[Literal["auto"], Literal["false"], Literal["true"]] + indent_level: int + interactive: bool + VALID_LOG_STYLES = ("auto", "record", "pretty") + + _autodetected_cf_colormode: int + + def __init__(self): + self.indent_level = 0 + + self._verbosity = 0 + self._verbosity_overriden = False + self._color_mode = "auto" + self._log_style = "record" + self.pretty = False + self.interactive = False + + # store whatever colorful has detected for future use if + # the color ouput is toggled (colorful detects # of supported colors, + # so it has some non-trivial logic to determine this) + self._autodetected_cf_colormode = cf.colorful.colormode + self.set_format() + + def set_format(self, format_tmpl=None): + if not format_tmpl: + from ray.autoscaler._private.constants import LOGGER_FORMAT + + format_tmpl = LOGGER_FORMAT + self._formatter = logging.Formatter(format_tmpl) + + def configure(self, log_style=None, color_mode=None, verbosity=None): + """Configures the logger according to values.""" + if log_style is not None: + self._set_log_style(log_style) + + if color_mode is not None: + self._set_color_mode(color_mode) + + if verbosity is not None: + self._set_verbosity(verbosity) + + self.detect_colors() + + @property + def log_style(self): + return self._log_style + + def _set_log_style(self, x): + """Configures interactivity and formatting.""" + self._log_style = x.lower() + self.interactive = _isatty() + + if self._log_style == "auto": + self.pretty = _isatty() + elif self._log_style == "record": + self.pretty = False + self._set_color_mode("false") + elif self._log_style == "pretty": + self.pretty = True + + @property + def color_mode(self): + return self._color_mode + + def _set_color_mode(self, x): + self._color_mode = x.lower() + self.detect_colors() + + @property + def verbosity(self): + if self._verbosity_overriden: + return self._verbosity + elif not self.pretty: + return 999 + return self._verbosity + + def _set_verbosity(self, x): + self._verbosity = x + self._verbosity_overriden = True + + def detect_colors(self): + """Update color output settings. + + Parse the `color_mode` string and optionally disable or force-enable + color output + (8-color ANSI if no terminal detected to be safe) in colorful. + """ + if self.color_mode == "true": + if self._autodetected_cf_colormode != cf.NO_COLORS: + cf.colormode = self._autodetected_cf_colormode + else: + cf.colormode = cf.ANSI_8_COLORS + return + if self.color_mode == "false": + cf.disable() + return + if self.color_mode == "auto": + # colorful autodetects tty settings + return + + raise ValueError("Invalid log color setting: " + self.color_mode) + + def newline(self): + """Print a line feed.""" + self.print("") + + def _print( + self, + msg: str, + _level_str: str = "INFO", + _linefeed: bool = True, + end: str = None, + ): + """Proxy for printing messages. + + Args: + msg: Message to print. + linefeed (bool): + If `linefeed` is `False` no linefeed is printed at the + end of the message. + """ + if self.pretty: + rendered_message = " " * self.indent_level + msg + else: + if msg.strip() == "": + return + caller_info = _external_caller_info() + record = logging.LogRecord( + name="cli", + # We override the level name later + # TODO(maximsmol): give approximate level #s to our log levels + level=0, + # The user-facing logs do not need this information anyway + # and it would be very tedious to extract since _print + # can be at varying depths in the call stack + # TODO(maximsmol): do it anyway to be extra + pathname=caller_info["filename"], + lineno=caller_info["lineno"], + msg=msg, + args={}, + # No exception + exc_info=None, + ) + record.levelname = _level_str + rendered_message = self._formatter.format(record) + + # We aren't using standard python logging convention, so we hardcode + # the log levels for now. + if _level_str in ["WARNING", "ERROR", "PANIC"]: + stream = sys.stderr + else: + stream = sys.stdout + + if not _linefeed: + stream.write(rendered_message) + stream.flush() + return + + kwargs = {"end": end} + print(rendered_message, file=stream, **kwargs) + + def indented(self): + """Context manager that starts an indented block of output.""" + cli_logger = self + + class IndentedContextManager: + def __enter__(self): + cli_logger.indent_level += 1 + + def __exit__(self, type, value, tb): + cli_logger.indent_level -= 1 + + return IndentedContextManager() + + def group(self, msg: str, *args: Any, **kwargs: Any): + """Print a group title in a special color and start an indented block. + + For arguments, see `_format_msg`. + """ + self.print(cf.dodgerBlue(msg), *args, **kwargs) + + return self.indented() + + def verbatim_error_ctx(self, msg: str, *args: Any, **kwargs: Any): + """Context manager for printing multi-line error messages. + + Displays a start sequence "!!! {optional message}" + and a matching end sequence "!!!". + + The string "!!!" can be used as a "tombstone" for searching. + + For arguments, see `_format_msg`. + """ + cli_logger = self + + class VerbatimErorContextManager: + def __enter__(self): + cli_logger.error(cf.bold("!!! ") + "{}", msg, *args, **kwargs) + + def __exit__(self, type, value, tb): + cli_logger.error(cf.bold("!!!")) + + return VerbatimErorContextManager() + + def labeled_value(self, key: str, msg: str, *args: Any, **kwargs: Any): + """Displays a key-value pair with special formatting. + + Args: + key: Label that is prepended to the message. + + For other arguments, see `_format_msg`. + """ + self._print(cf.skyBlue(key) + ": " + _format_msg(cf.bold(msg), *args, **kwargs)) + + def verbose(self, msg: str, *args: Any, **kwargs: Any): + """Prints a message if verbosity is not 0. + + For arguments, see `_format_msg`. + """ + if self.verbosity > 0: + self.print(msg, *args, _level_str="VINFO", **kwargs) + + def verbose_warning(self, msg, *args, **kwargs): + """Prints a formatted warning if verbosity is not 0. + + For arguments, see `_format_msg`. + """ + if self.verbosity > 0: + self._warning(msg, *args, _level_str="VWARN", **kwargs) + + def verbose_error(self, msg: str, *args: Any, **kwargs: Any): + """Logs an error if verbosity is not 0. + + For arguments, see `_format_msg`. + """ + if self.verbosity > 0: + self._error(msg, *args, _level_str="VERR", **kwargs) + + def very_verbose(self, msg: str, *args: Any, **kwargs: Any): + """Prints if verbosity is > 1. + + For arguments, see `_format_msg`. + """ + if self.verbosity > 1: + self.print(msg, *args, _level_str="VVINFO", **kwargs) + + def success(self, msg: str, *args: Any, **kwargs: Any): + """Prints a formatted success message. + + For arguments, see `_format_msg`. + """ + self.print(cf.limeGreen(msg), *args, _level_str="SUCC", **kwargs) + + def _warning(self, msg: str, *args: Any, _level_str: str = None, **kwargs: Any): + """Prints a formatted warning message. + + For arguments, see `_format_msg`. + """ + if _level_str is None: + raise ValueError("Log level not set.") + self.print(cf.orange(msg), *args, _level_str=_level_str, **kwargs) + + def warning(self, *args, **kwargs): + self._warning(*args, _level_str="WARN", **kwargs) + + def _error(self, msg: str, *args: Any, _level_str: str = None, **kwargs: Any): + """Prints a formatted error message. + + For arguments, see `_format_msg`. + """ + if _level_str is None: + raise ValueError("Log level not set.") + self.print(cf.red(msg), *args, _level_str=_level_str, **kwargs) + + def error(self, *args, **kwargs): + self._error(*args, _level_str="ERR", **kwargs) + + def panic(self, *args, **kwargs): + self._error(*args, _level_str="PANIC", **kwargs) + + # Fine to expose _level_str here, since this is a general log function. + def print( + self, + msg: str, + *args: Any, + _level_str: str = "INFO", + end: str = None, + **kwargs: Any, + ): + """Prints a message. + + For arguments, see `_format_msg`. + """ + self._print(_format_msg(msg, *args, **kwargs), _level_str=_level_str, end=end) + + def info(self, msg: str, no_format=True, *args, **kwargs): + self.print(msg, no_format=no_format, *args, **kwargs) + + def abort( + self, msg: Optional[str] = None, *args: Any, exc: Any = None, **kwargs: Any + ): + """Prints an error and aborts execution. + + Print an error and throw an exception to terminate the program + (the exception will not print a message). + """ + if msg is not None: + self._error(msg, *args, _level_str="PANIC", **kwargs) + + if exc is not None: + raise exc + + exc_cls = click.ClickException + if self.pretty: + exc_cls = SilentClickException + + if msg is None: + msg = "Exiting due to cli_logger.abort()" + raise exc_cls(msg) + + def doassert(self, val: bool, msg: str, *args: Any, **kwargs: Any): + """Handle assertion without throwing a scary exception. + + Args: + val: Value to check. + + For other arguments, see `_format_msg`. + """ + if not val: + exc = None + if not self.pretty: + exc = AssertionError() + + # TODO(maximsmol): rework asserts so that we get the expression + # that triggered the assert + # to do this, install a global try-catch + # for AssertionError and raise them normally + self.abort(msg, *args, exc=exc, **kwargs) + + def render_list(self, xs: List[str], separator: str = cf.reset(", ")): + """Render a list of bolded values using a non-bolded separator.""" + return separator.join([str(cf.bold(x)) for x in xs]) + + def confirm( + self, + yes: bool, + msg: str, + *args: Any, + _abort: bool = False, + _default: bool = False, + _timeout_s: Optional[float] = None, + **kwargs: Any, + ): + """Display a confirmation dialog. + + Valid answers are "y/yes/true/1" and "n/no/false/0". + + Args: + yes: If `yes` is `True` the dialog will default to "yes" + and continue without waiting for user input. + _abort (bool): + If `_abort` is `True`, + "no" means aborting the program. + _default (bool): + The default action to take if the user just presses enter + with no input. + _timeout_s (float): + If user has no input within _timeout_s seconds, the default + action is taken. None means no timeout. + """ + should_abort = _abort + default = _default + + if not self.interactive and not yes: + # no formatting around --yes here since this is non-interactive + self.error( + "This command requires user confirmation. " + "When running non-interactively, supply --yes to skip." + ) + raise ValueError("Non-interactive confirm without --yes.") + + if default: + yn_str = "Y/n" + else: + yn_str = "y/N" + + confirm_str = cf.underlined("Confirm [" + yn_str + "]:") + " " + + rendered_message = _format_msg(msg, *args, **kwargs) + # the rendered message ends with ascii coding + if rendered_message and not msg.endswith("\n"): + rendered_message += " " + + msg_len = len(rendered_message.split("\n")[-1]) + complete_str = rendered_message + confirm_str + + if yes: + self._print(complete_str + "y " + cf.dimmed("[automatic, due to --yes]")) + return True + + self._print(complete_str, _linefeed=False) + + res = None + yes_answers = ["y", "yes", "true", "1"] + no_answers = ["n", "no", "false", "0"] + try: + while True: + if _timeout_s is None: + ans = sys.stdin.readline() + elif sys.platform == "win32": + # Windows doesn't support select + start_time = time.time() + ans = "" + while True: + if (time.time() - start_time) >= _timeout_s: + self.newline() + ans = "\n" + break + elif msvcrt.kbhit(): + ch = msvcrt.getwch() + if ch in ("\n", "\r"): + self.newline() + ans = ans + "\n" + break + elif ch == "\b": + if ans: + ans = ans[:-1] + # Emulate backspace erasing + print("\b \b", end="", flush=True) + else: + ans = ans + ch + print(ch, end="", flush=True) + else: + time.sleep(0.1) + else: + ready, _, _ = select.select([sys.stdin], [], [], _timeout_s) + if not ready: + self.newline() + ans = "\n" + else: + ans = sys.stdin.readline() + + ans = ans.lower() + + if ans == "\n": + res = default + break + + ans = ans.strip() + if ans in yes_answers: + res = True + break + if ans in no_answers: + res = False + break + + indent = " " * msg_len + self.error( + "{}Invalid answer: {}. Expected {} or {}", + indent, + cf.bold(ans.strip()), + self.render_list(yes_answers, "/"), + self.render_list(no_answers, "/"), + ) + self._print(indent + confirm_str, _linefeed=False) + except KeyboardInterrupt: + self.newline() + res = default + + if not res and should_abort: + # todo: make sure we tell the user if they + # need to do cleanup + self._print("Exiting...") + raise SilentClickException( + "Exiting due to the response to confirm(should_abort=True)." + ) + + return res + + def prompt(self, msg: str, *args, **kwargs): + """Prompt the user for some text input. + + Args: + msg: The mesage to display to the user before the prompt. + + Returns: + The string entered by the user. + """ + complete_str = cf.underlined(msg) + rendered_message = _format_msg(complete_str, *args, **kwargs) + # the rendered message ends with ascii coding + if rendered_message and not msg.endswith("\n"): + rendered_message += " " + self._print(rendered_message, linefeed=False) + + res = "" + try: + ans = sys.stdin.readline() + ans = ans.lower() + res = ans.strip() + except KeyboardInterrupt: + self.newline() + + return res + + def flush(self): + sys.stdout.flush() + sys.stderr.flush() + + +class SilentClickException(click.ClickException): + """`ClickException` that does not print a message. + + Some of our tooling relies on catching ClickException in particular. + + However the default prints a message, which is undesirable since we expect + our code to log errors manually using `cli_logger.error()` to allow for + colors and other formatting. + """ + + def __init__(self, message: str): + super(SilentClickException, self).__init__(message) + + def show(self, file=None): + pass + + +cli_logger = _CliLogger() + +CLICK_LOGGING_OPTIONS = [ + click.option( + "--log-style", + required=False, + type=click.Choice(cli_logger.VALID_LOG_STYLES, case_sensitive=False), + default="auto", + help=( + "If 'pretty', outputs with formatting and color. If 'record', " + "outputs record-style without formatting. " + "'auto' defaults to 'pretty', and disables pretty logging " + "if stdin is *not* a TTY." + ), + ), + click.option( + "--log-color", + required=False, + type=click.Choice(["auto", "false", "true"], case_sensitive=False), + default="auto", + help=("Use color logging. Auto enables color logging if stdout is a TTY."), + ), + click.option("-v", "--verbose", default=None, count=True), +] + + +def add_click_logging_options(f: Callable) -> Callable: + for option in reversed(CLICK_LOGGING_OPTIONS): + f = option(f) + + @wraps(f) + def wrapper(*args, log_style=None, log_color=None, verbose=None, **kwargs): + cli_logger.configure(log_style, log_color, verbose) + return f(*args, **kwargs) + + return wrapper diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/cli_logger_demoall.py b/lib/python3.12/site-packages/ray/autoscaler/_private/cli_logger_demoall.py new file mode 100644 index 0000000000000000000000000000000000000000..2f8dcd2404b1e3cfdfb29adb69c387c7e99e7a7b --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/cli_logger_demoall.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +# This is an executable script that runs an example of every single CliLogger +# function for demonstration purposes. Primarily useful for tuning color and +# other formatting. + +from ray.autoscaler._private.cli_logger import cf, cli_logger + +cli_logger.configure(log_style="auto", verbosity=999) + +cli_logger.print(cf.bold("Bold ") + cf.italic("Italic ") + cf.underlined("Underlined")) +cli_logger.labeled_value("Label", "value") +cli_logger.print("List: {}", cli_logger.render_list([1, 2, 3])) +cli_logger.newline() +cli_logger.very_verbose("Very verbose") +cli_logger.verbose("Verbose") +cli_logger.verbose_warning("Verbose warning") +cli_logger.verbose_error("Verbose error") +cli_logger.print("Info") +cli_logger.success("Success") +cli_logger.warning("Warning") +cli_logger.error("Error") +cli_logger.newline() +try: + cli_logger.abort("Abort") +except Exception: + pass +try: + cli_logger.doassert(False, "Assert") +except Exception: + pass +cli_logger.newline() +cli_logger.confirm(True, "example") +cli_logger.newline() +with cli_logger.indented(): + cli_logger.print("Indented") +with cli_logger.group("Group"): + cli_logger.print("Group contents") +with cli_logger.verbatim_error_ctx("Verbtaim error"): + cli_logger.print("Error contents") diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/cluster_dump.py b/lib/python3.12/site-packages/ray/autoscaler/_private/cluster_dump.py new file mode 100644 index 0000000000000000000000000000000000000000..6984c16064361e17013da52f2cf4e6d3e51b3a5b --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/cluster_dump.py @@ -0,0 +1,652 @@ +import os +import re +import subprocess +import sys +import tarfile +import tempfile +import threading +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from typing import List, Optional, Sequence, Tuple + +import yaml + +import ray # noqa: F401 +from ray.autoscaler._private.cli_logger import cli_logger +from ray.autoscaler._private.providers import _get_node_provider +from ray.autoscaler.tags import NODE_KIND_HEAD, NODE_KIND_WORKER, TAG_RAY_NODE_KIND + +# Import psutil after ray so the packaged version is used. +import psutil + +MAX_PARALLEL_SSH_WORKERS = 8 +DEFAULT_SSH_USER = "ubuntu" +DEFAULT_SSH_KEYS = ["~/ray_bootstrap_key.pem", "~/.ssh/ray-autoscaler_2_us-west-2.pem"] + + +class CommandFailed(RuntimeError): + pass + + +class LocalCommandFailed(CommandFailed): + pass + + +class RemoteCommandFailed(CommandFailed): + pass + + +class GetParameters: + def __init__( + self, + logs: bool = True, + debug_state: bool = True, + pip: bool = True, + processes: bool = True, + processes_verbose: bool = True, + processes_list: Optional[List[Tuple[str, bool]]] = None, + ): + self.logs = logs + self.debug_state = debug_state + self.pip = pip + self.processes = processes + self.processes_verbose = processes_verbose + self.processes_list = processes_list + + +class Node: + """Node (as in "machine")""" + + def __init__( + self, + host: str, + ssh_user: str = "ubuntu", + ssh_key: str = "~/ray_bootstrap_key.pem", + docker_container: Optional[str] = None, + is_head: bool = False, + ): + self.host = host + self.ssh_user = ssh_user + self.ssh_key = ssh_key + self.docker_container = docker_container + self.is_head = is_head + + +class Archive: + """Archive object to collect and compress files into a single file. + + Objects of this class can be passed around to different data collection + functions. These functions can use the :meth:`subdir` method to add + files to a sub directory of the archive. + + """ + + def __init__(self, file: Optional[str] = None): + self.file = file or tempfile.mkstemp(prefix="ray_logs_", suffix=".tar.gz")[1] + self.tar = None + self._lock = threading.Lock() + + @property + def is_open(self): + return bool(self.tar) + + def open(self): + self.tar = tarfile.open(self.file, "w:gz") + + def close(self): + self.tar.close() + self.tar = None + + def __enter__(self): + self.open() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + @contextmanager + def subdir(self, subdir: str, root: Optional[str] = "/"): + """Open a context to add files to the archive. + + Example: + + .. code-block:: python + + with Archive("file.tar.gz") as archive: + with archive.subdir("logfiles", root="/tmp/logs") as sd: + # Will be added as `logfiles/nested/file.txt` + sd.add("/tmp/logs/nested/file.txt") + + Args: + subdir: Subdir to which to add files to. Calling the + ``add(path)`` command will place files into the ``subdir`` + directory of the archive. + root: Root path. Files without an explicit ``arcname`` + will be named relatively to this path. + + Yields: + A context object that can be used to add files to the archive. + """ + root = os.path.abspath(root) + + class _Context: + @staticmethod + def add(path: str, arcname: Optional[str] = None): + path = os.path.abspath(path) + arcname = arcname or os.path.join(subdir, os.path.relpath(path, root)) + + self._lock.acquire() + self.tar.add(path, arcname=arcname) + self._lock.release() + + yield _Context() + + +### +# Functions to gather logs and information on the local node +### + + +def get_local_ray_logs( + archive: Archive, + exclude: Optional[Sequence[str]] = None, + session_log_dir: str = "/tmp/ray/session_latest", +) -> Archive: + """Copy local log files into an archive. + + Args: + archive: Archive object to add log files to. + exclude (Sequence[str]): Sequence of regex patterns. Files that match + any of these patterns will not be included in the archive. + session_dir: Path to the Ray session files. Defaults to + ``/tmp/ray/session_latest`` + + Returns: + Open archive object. + + """ + if not archive.is_open: + archive.open() + + exclude = exclude or [] + + session_log_dir = os.path.join(os.path.expanduser(session_log_dir), "logs") + + with archive.subdir("logs", root=session_log_dir) as sd: + for root, dirs, files in os.walk(session_log_dir): + for file in files: + file_path = os.path.join(root, file) + rel_path = os.path.relpath(file_path, start=session_log_dir) + # Skip file if it matches any pattern in `exclude` + if any(re.match(pattern, rel_path) for pattern in exclude): + continue + sd.add(file_path) + + return archive + + +def get_local_debug_state( + archive: Archive, session_dir: str = "/tmp/ray/session_latest" +) -> Archive: + """Copy local log files into an archive. + + Args: + archive: Archive object to add log files to. + session_dir: Path to the Ray session files. Defaults to + ``/tmp/ray/session_latest`` + + Returns: + Open archive object. + + """ + if not archive.is_open: + archive.open() + + session_dir = os.path.expanduser(session_dir) + debug_state_file = os.path.join(session_dir, "logs/debug_state.txt") + + if not os.path.exists(debug_state_file): + raise LocalCommandFailed("No `debug_state.txt` file found.") + + with archive.subdir("", root=session_dir) as sd: + sd.add(debug_state_file) + + return archive + + +def get_local_pip_packages(archive: Archive): + """Get currently installed pip packages and write into an archive. + + Args: + archive: Archive object to add meta files to. + + Returns: + Open archive object. + """ + if not archive.is_open: + archive.open() + + try: + from pip._internal.operations import freeze + except ImportError: # pip < 10.0 + from pip.operations import freeze + + with tempfile.NamedTemporaryFile("wt") as fp: + for line in freeze.freeze(): + fp.writelines([line, "\n"]) + + fp.flush() + with archive.subdir("") as sd: + sd.add(fp.name, "pip_packages.txt") + + return archive + + +def get_local_ray_processes( + archive: Archive, + processes: Optional[List[Tuple[str, bool]]] = None, + verbose: bool = False, +): + """Get the status of all the relevant ray processes. + Args: + archive: Archive object to add process info files to. + processes: List of processes to get information on. The first + element of the tuple is a string to filter by, and the second + element is a boolean indicating if we should filter by command + name (True) or command line including parameters (False) + verbose: If True, show entire executable command line. + If False, show just the first term. + Returns: + Open archive object. + """ + if not processes: + # local import to avoid circular dependencies + from ray.autoscaler._private.constants import RAY_PROCESSES + + processes = RAY_PROCESSES + + process_infos = [] + for process in psutil.process_iter(["pid", "name", "cmdline", "status"]): + try: + with process.oneshot(): + cmdline = " ".join(process.cmdline()) + process_infos.append( + ( + { + "executable": cmdline + if verbose + else cmdline.split("--", 1)[0][:-1], + "name": process.name(), + "pid": process.pid, + "status": process.status(), + }, + process.cmdline(), + ) + ) + except Exception as exc: + raise LocalCommandFailed(exc) from exc + + relevant_processes = {} + for process_dict, cmdline in process_infos: + for keyword, filter_by_cmd in processes: + if filter_by_cmd: + corpus = process_dict["name"] + else: + corpus = subprocess.list2cmdline(cmdline) + if keyword in corpus and process_dict["pid"] not in relevant_processes: + relevant_processes[process_dict["pid"]] = process_dict + + with tempfile.NamedTemporaryFile("wt") as fp: + for line in relevant_processes.values(): + fp.writelines([yaml.dump(line), "\n"]) + + fp.flush() + with archive.subdir("meta") as sd: + sd.add(fp.name, "process_info.txt") + + return archive + + +def get_all_local_data(archive: Archive, parameters: GetParameters): + """Get all local data. + + Gets: + - The Ray logs of the latest session + - The currently installed pip packages + + Args: + archive: Archive object to add meta files to. + parameters: Parameters (settings) for getting data. + + Returns: + Open archive object. + """ + if not archive.is_open: + archive.open() + + if parameters.logs: + try: + get_local_ray_logs(archive=archive) + except LocalCommandFailed as exc: + cli_logger.error(exc) + if parameters.debug_state: + try: + get_local_debug_state(archive=archive) + except LocalCommandFailed as exc: + cli_logger.error(exc) + if parameters.pip: + try: + get_local_pip_packages(archive=archive) + except LocalCommandFailed as exc: + cli_logger.error(exc) + if parameters.processes: + try: + get_local_ray_processes( + archive=archive, + processes=parameters.processes_list, + verbose=parameters.processes_verbose, + ) + except LocalCommandFailed as exc: + cli_logger.error(exc) + + return archive + + +### +# Functions to invoke remote scripts and gather data from remote nodes +### + + +def _wrap(items: List[str], quotes="'"): + return f"{quotes}{' '.join(items)}{quotes}" + + +def create_and_get_archive_from_remote_node( + remote_node: Node, parameters: GetParameters, script_path: str = "ray" +) -> Optional[str]: + """Create an archive containing logs on a remote node and transfer. + + This will call ``ray local-dump --stream`` on the remote + node. The resulting file will be saved locally in a temporary file and + returned. + + Args: + remote_node: Remote node to gather archive from. + script_path: Path to this script on the remote node. + parameters: Parameters (settings) for getting data. + + Returns: + Path to a temporary file containing the node's collected data. + + """ + cmd = [ + "ssh", + "-o StrictHostKeyChecking=no", + "-o UserKnownHostsFile=/dev/null", + "-o LogLevel=ERROR", + "-i", + remote_node.ssh_key, + f"{remote_node.ssh_user}@{remote_node.host}", + ] + + if remote_node.docker_container: + cmd += [ + "docker", + "exec", + remote_node.docker_container, + ] + + collect_cmd = [script_path, "local-dump", "--stream"] + collect_cmd += ["--logs"] if parameters.logs else ["--no-logs"] + collect_cmd += ["--debug-state"] if parameters.debug_state else ["--no-debug-state"] + collect_cmd += ["--pip"] if parameters.pip else ["--no-pip"] + collect_cmd += ["--processes"] if parameters.processes else ["--no-processes"] + if parameters.processes: + collect_cmd += ( + ["--processes-verbose"] + if parameters.processes_verbose + else ["--no-proccesses-verbose"] + ) + + cmd += ["/bin/bash", "-c", _wrap(collect_cmd, quotes='"')] + + cat = "node" if not remote_node.is_head else "head" + + cli_logger.print(f"Collecting data from remote node: {remote_node.host}") + tmp = tempfile.mkstemp(prefix=f"ray_{cat}_{remote_node.host}_", suffix=".tar.gz")[1] + with open(tmp, "wb") as fp: + try: + subprocess.check_call(cmd, stdout=fp, stderr=sys.stderr) + except subprocess.CalledProcessError as exc: + raise RemoteCommandFailed( + f"Gathering logs from remote node failed: {' '.join(cmd)}" + ) from exc + + return tmp + + +def create_and_add_remote_data_to_local_archive( + archive: Archive, remote_node: Node, parameters: GetParameters +): + """Create and get data from remote node and add to local archive. + + Args: + archive: Archive object to add remote data to. + remote_node: Remote node to gather archive from. + parameters: Parameters (settings) for getting data. + + Returns: + Open archive object. + """ + tmp = create_and_get_archive_from_remote_node(remote_node, parameters) + + if not archive.is_open: + archive.open() + + cat = "node" if not remote_node.is_head else "head" + + with archive.subdir("", root=os.path.dirname(tmp)) as sd: + sd.add(tmp, arcname=f"ray_{cat}_{remote_node.host}.tar.gz") + + return archive + + +def create_and_add_local_data_to_local_archive( + archive: Archive, parameters: GetParameters +): + """Create and get data from this node and add to archive. + + Args: + archive: Archive object to add remote data to. + parameters: Parameters (settings) for getting data. + + Returns: + Open archive object. + """ + with Archive() as local_data_archive: + get_all_local_data(local_data_archive, parameters) + + if not archive.is_open: + archive.open() + + with archive.subdir("", root=os.path.dirname(local_data_archive.file)) as sd: + sd.add(local_data_archive.file, arcname="local_node.tar.gz") + + os.remove(local_data_archive.file) + + return archive + + +def create_archive_for_remote_nodes( + archive: Archive, remote_nodes: Sequence[Node], parameters: GetParameters +): + """Create an archive combining data from the remote nodes. + + This will parallelize calls to get data from remote nodes. + + Args: + archive: Archive object to add remote data to. + remote_nodes (Sequence[Node]): Sequence of remote nodes. + parameters: Parameters (settings) for getting data. + + Returns: + Open archive object. + + """ + if not archive.is_open: + archive.open() + + with ThreadPoolExecutor(max_workers=MAX_PARALLEL_SSH_WORKERS) as executor: + for remote_node in remote_nodes: + executor.submit( + create_and_add_remote_data_to_local_archive, + archive=archive, + remote_node=remote_node, + parameters=parameters, + ) + + return archive + + +def create_archive_for_local_and_remote_nodes( + archive: Archive, remote_nodes: Sequence[Node], parameters: GetParameters +): + """Create an archive combining data from the local and remote nodes. + + This will parallelize calls to get data from remote nodes. + + Args: + archive: Archive object to add data to. + remote_nodes (Sequence[Node]): Sequence of remote nodes. + parameters: Parameters (settings) for getting data. + + Returns: + Open archive object. + + """ + if not archive.is_open: + archive.open() + + try: + create_and_add_local_data_to_local_archive(archive, parameters) + except CommandFailed as exc: + cli_logger.error(exc) + + create_archive_for_remote_nodes(archive, remote_nodes, parameters) + + cli_logger.print( + f"Collected data from local node and {len(remote_nodes)} " f"remote nodes." + ) + return archive + + +### +# Ray cluster info +### +def get_info_from_ray_cluster_config( + cluster_config: str, +) -> Tuple[List[str], str, str, Optional[str], Optional[str]]: + """Get information from Ray cluster config. + + Return list of host IPs, ssh user, ssh key file, and optional docker + container. + + Args: + cluster_config: Path to ray cluster config. + + Returns: + Tuple of list of host IPs, ssh user name, ssh key file path, + optional docker container name, optional cluster name. + """ + from ray.autoscaler._private.commands import _bootstrap_config + + cli_logger.print( + f"Retrieving cluster information from ray cluster file: " f"{cluster_config}" + ) + + cluster_config = os.path.expanduser(cluster_config) + + config = yaml.safe_load(open(cluster_config).read()) + config = _bootstrap_config(config, no_config_cache=True) + + provider = _get_node_provider(config["provider"], config["cluster_name"]) + head_nodes = provider.non_terminated_nodes({TAG_RAY_NODE_KIND: NODE_KIND_HEAD}) + worker_nodes = provider.non_terminated_nodes({TAG_RAY_NODE_KIND: NODE_KIND_WORKER}) + + hosts = [provider.external_ip(node) for node in head_nodes + worker_nodes] + ssh_user = config["auth"]["ssh_user"] + ssh_key = config["auth"]["ssh_private_key"] + + docker = None + docker_config = config.get("docker", None) + if docker_config: + docker = docker_config.get("container_name", None) + + cluster_name = config.get("cluster_name", None) + + return hosts, ssh_user, ssh_key, docker, cluster_name + + +def _info_from_params( + cluster: Optional[str] = None, + host: Optional[str] = None, + ssh_user: Optional[str] = None, + ssh_key: Optional[str] = None, + docker: Optional[str] = None, +): + """Parse command line arguments. + + Note: This returns a list of hosts, not a comma separated string! + """ + if not host and not cluster: + bootstrap_config = os.path.expanduser("~/ray_bootstrap_config.yaml") + if os.path.exists(bootstrap_config): + cluster = bootstrap_config + cli_logger.warning( + f"Detected cluster config file at {cluster}. " + f"If this is incorrect, specify with " + f"`ray cluster-dump `" + ) + elif cluster: + cluster = os.path.expanduser(cluster) + + cluster_name = None + + if cluster: + h, u, k, d, cluster_name = get_info_from_ray_cluster_config(cluster) + + ssh_user = ssh_user or u + ssh_key = ssh_key or k + docker = docker or d + hosts = host.split(",") if host else h + + if not hosts: + raise LocalCommandFailed( + f"Invalid cluster file or cluster has no running nodes: " f"{cluster}" + ) + elif host: + hosts = host.split(",") + else: + raise LocalCommandFailed( + "You need to either specify a `` or `--host`." + ) + + if not ssh_user: + ssh_user = DEFAULT_SSH_USER + cli_logger.warning( + f"Using default SSH user `{ssh_user}`. " + f"If this is incorrect, specify with `--ssh-user `" + ) + + if not ssh_key: + for cand_key in DEFAULT_SSH_KEYS: + cand_key_file = os.path.expanduser(cand_key) + if os.path.exists(cand_key_file): + ssh_key = cand_key_file + cli_logger.warning( + f"Auto detected SSH key file: {ssh_key}. " + f"If this is incorrect, specify with `--ssh-key `" + ) + break + + return cluster, hosts, ssh_user, ssh_key, docker, cluster_name diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/command_runner.py b/lib/python3.12/site-packages/ray/autoscaler/_private/command_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..930367c420b810310aaf776e1a1de33eea47672a --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/command_runner.py @@ -0,0 +1,962 @@ +import hashlib +import json +import logging +import os +import subprocess +import sys +import time +from getpass import getuser +from shlex import quote +from typing import Dict, List, Optional, Tuple + +import click + +from ray._private.ray_constants import DEFAULT_OBJECT_STORE_MAX_MEMORY_BYTES +from ray.autoscaler._private.cli_logger import cf, cli_logger +from ray.autoscaler._private.constants import ( + AUTOSCALER_NODE_SSH_INTERVAL_S, + AUTOSCALER_NODE_START_WAIT_S, + DEFAULT_OBJECT_STORE_MEMORY_PROPORTION, +) +from ray.autoscaler._private.docker import ( + check_bind_mounts_cmd, + check_docker_image, + check_docker_running_cmd, + docker_start_cmds, + with_docker_exec, +) +from ray.autoscaler._private.log_timer import LogTimer +from ray.autoscaler._private.subprocess_output_util import ( + ProcessRunnerError, + is_output_redirected, + run_cmd_redirected, +) +from ray.autoscaler.command_runner import CommandRunnerInterface + +logger = logging.getLogger(__name__) + +# How long to wait for a node to start, in seconds +HASH_MAX_LENGTH = 10 +KUBECTL_RSYNC = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "_kubernetes/kubectl-rsync.sh" +) +MAX_HOME_RETRIES = 3 +HOME_RETRY_DELAY_S = 5 + +_config = {"use_login_shells": True, "silent_rsync": True} + + +def is_rsync_silent(): + return _config["silent_rsync"] + + +def set_rsync_silent(val): + """Choose whether to silence rsync output. + + Most commands will want to list rsync'd files themselves rather than + print the default rsync spew. + """ + _config["silent_rsync"] = val + + +def is_using_login_shells(): + return _config["use_login_shells"] + + +def set_using_login_shells(val: bool): + """Choose between login and non-interactive shells. + + Non-interactive shells have the benefit of receiving less output from + subcommands (since progress bars and TTY control codes are not printed). + Sometimes this can be significant since e.g. `pip install` prints + hundreds of progress bar lines when downloading. + + Login shells have the benefit of working very close to how a proper bash + session does, regarding how scripts execute and how the environment is + setup. This is also how all commands were ran in the past. The only reason + to use login shells over non-interactive shells is if you need some weird + and non-robust tool to work. + + Args: + val: If true, login shells will be used to run all commands. + """ + _config["use_login_shells"] = val + + +def _with_environment_variables(cmd: str, environment_variables: Dict[str, object]): + """Prepend environment variables to a shell command. + + Args: + cmd: The base command. + environment_variables (Dict[str, object]): The set of environment + variables. If an environment variable value is a dict, it will + automatically be converted to a one line yaml string. + """ + + as_strings = [] + for key, val in environment_variables.items(): + val = json.dumps(val, separators=(",", ":")) + s = "export {}={};".format(key, quote(val)) + as_strings.append(s) + all_vars = "".join(as_strings) + return all_vars + cmd + + +def _with_interactive(cmd): + force_interactive = ( + f"source ~/.bashrc; " + f"export OMP_NUM_THREADS=1 PYTHONWARNINGS=ignore && ({cmd})" + ) + return ["bash", "--login", "-c", "-i", quote(force_interactive)] + + +class SSHOptions: + def __init__(self, ssh_key, control_path=None, **kwargs): + self.ssh_key = ssh_key + self.arg_dict = { + # Supresses initial fingerprint verification. + "StrictHostKeyChecking": "no", + # SSH IP and fingerprint pairs no longer added to known_hosts. + # This is to remove a "REMOTE HOST IDENTIFICATION HAS CHANGED" + # warning if a new node has the same IP as a previously + # deleted node, because the fingerprints will not match in + # that case. + "UserKnownHostsFile": os.devnull, + # Try fewer extraneous key pairs. + "IdentitiesOnly": "yes", + # Abort if port forwarding fails (instead of just printing to + # stderr). + "ExitOnForwardFailure": "yes", + # Quickly kill the connection if network connection breaks (as + # opposed to hanging/blocking). + "ServerAliveInterval": 5, + "ServerAliveCountMax": 3, + } + if control_path: + if sys.platform == "win32": + # Don't set any control path options on Windows + pass + else: + self.arg_dict.update( + { + "ControlMaster": "auto", + "ControlPath": "{}/%C".format(control_path), + "ControlPersist": "10s", + } + ) + + self.arg_dict.update(kwargs) + + def to_ssh_options_list(self, *, timeout=60): + self.arg_dict["ConnectTimeout"] = "{}s".format(timeout) + ssh_key_option = ["-i", self.ssh_key] if self.ssh_key else [] + return ssh_key_option + [ + x + for y in ( + ["-o", "{}={}".format(k, v)] + for k, v in self.arg_dict.items() + if v is not None + ) + for x in y + ] + + +class SSHCommandRunner(CommandRunnerInterface): + def __init__( + self, + log_prefix, + node_id, + provider, + auth_config, + cluster_name, + process_runner, + use_internal_ip, + ): + + ssh_control_hash = hashlib.sha1(cluster_name.encode()).hexdigest() + ssh_user_hash = hashlib.sha1(getuser().encode()).hexdigest() + if sys.platform == "win32": + # Disable SSH control paths on Windows - currently using it cause socket errors + ssh_control_path = None + else: + ssh_control_path = "/tmp/ray_ssh_{}/{}".format( + ssh_user_hash[:HASH_MAX_LENGTH], ssh_control_hash[:HASH_MAX_LENGTH] + ) + + self.cluster_name = cluster_name + self.log_prefix = log_prefix + self.process_runner = process_runner + self.node_id = node_id + self.use_internal_ip = use_internal_ip + self.provider = provider + self.ssh_private_key = auth_config.get("ssh_private_key") + self.ssh_user = auth_config["ssh_user"] + self.ssh_control_path = ssh_control_path + self.ssh_ip = None + self.ssh_proxy_command = auth_config.get("ssh_proxy_command", None) + self.ssh_options = SSHOptions( + self.ssh_private_key, + self.ssh_control_path, + ProxyCommand=self.ssh_proxy_command, + ) + + def _get_node_ip(self): + if self.use_internal_ip: + return self.provider.internal_ip(self.node_id) + else: + return self.provider.external_ip(self.node_id) + + def _wait_for_ip(self, deadline): + # if we have IP do not print waiting info + ip = self._get_node_ip() + if ip is not None: + cli_logger.labeled_value("Fetched IP", ip) + return ip + + interval = AUTOSCALER_NODE_SSH_INTERVAL_S + with cli_logger.group("Waiting for IP"): + while time.time() < deadline and not self.provider.is_terminated( + self.node_id + ): + ip = self._get_node_ip() + if ip is not None: + cli_logger.labeled_value("Received", ip) + return ip + cli_logger.print( + "Not yet available, retrying in {} seconds", cf.bold(str(interval)) + ) + time.sleep(interval) + + return None + + def _set_ssh_ip_if_required(self): + if self.ssh_ip is not None: + return + + # We assume that this never changes. + # I think that's reasonable. + deadline = time.time() + AUTOSCALER_NODE_START_WAIT_S + with LogTimer(self.log_prefix + "Got IP"): + ip = self._wait_for_ip(deadline) + + cli_logger.doassert(ip is not None, "Could not get node IP.") # todo: msg + assert ip is not None, "Unable to find IP of node" + + self.ssh_ip = ip + + # This should run before any SSH commands and therefore ensure that + # the ControlPath directory exists, allowing SSH to maintain + # persistent sessions later on. + if self.ssh_control_path is not None: + try: + os.makedirs(self.ssh_control_path, mode=0o700, exist_ok=True) + except OSError as e: + cli_logger.warning("{}", str(e)) # todo: msg + + def _run_helper( + self, + final_cmd: List[str], + with_output: bool = False, + exit_on_fail: bool = False, + silent: bool = False, + ): + """Run a command that was already setup with SSH and `bash` settings. + + Args: + final_cmd (List[str]): + Full command to run. Should include SSH options and other + processing that we do. + with_output (bool): + If `with_output` is `True`, command stdout will be captured and + returned. + exit_on_fail (bool): + If `exit_on_fail` is `True`, the process will exit + if the command fails (exits with a code other than 0). + silent: If true, the command output will be silenced. + + Raises: + ProcessRunnerError: If using new log style and disabled + login shells. + click.ClickException: If using login shells. + """ + try: + # For now, if the output is needed we just skip the new logic. + # In the future we could update the new logic to support + # capturing output, but it is probably not needed. + if not with_output: + return run_cmd_redirected( + final_cmd, + process_runner=self.process_runner, + silent=silent, + use_login_shells=is_using_login_shells(), + ) + else: + return self.process_runner.check_output(final_cmd) + except subprocess.CalledProcessError as e: + joined_cmd = " ".join(final_cmd) + if not is_using_login_shells(): + raise ProcessRunnerError( + "Command failed", + "ssh_command_failed", + code=e.returncode, + command=joined_cmd, + ) + + if exit_on_fail: + raise click.ClickException( + "Command failed:\n\n {}\n".format(joined_cmd) + ) from None + else: + fail_msg = "SSH command failed." + if is_output_redirected(): + fail_msg += " See above for the output from the failure." + raise click.ClickException(fail_msg) from None + finally: + # Do our best to flush output to terminal. + # See https://github.com/ray-project/ray/pull/19473. + sys.stdout.flush() + sys.stderr.flush() + + def run( + self, + cmd: Optional[str] = None, + timeout: int = 120, + exit_on_fail: bool = False, + port_forward: Optional[List[Tuple[int, int]]] = None, + with_output: bool = False, + environment_variables: Optional[Dict[str, object]] = None, + run_env: str = "auto", # Unused argument. + ssh_options_override_ssh_key: str = "", + shutdown_after_run: bool = False, + silent: bool = False, + ) -> str: + if shutdown_after_run: + cmd += "; sudo shutdown -h now" + + if ssh_options_override_ssh_key: + if self.ssh_proxy_command: + ssh_options = SSHOptions( + ssh_options_override_ssh_key, ProxyCommand=self.ssh_proxy_command + ) + else: + ssh_options = SSHOptions(ssh_options_override_ssh_key) + else: + ssh_options = self.ssh_options + + assert isinstance( + ssh_options, SSHOptions + ), "ssh_options must be of type SSHOptions, got {}".format(type(ssh_options)) + + self._set_ssh_ip_if_required() + + if is_using_login_shells(): + ssh = ["ssh", "-tt"] + else: + ssh = ["ssh"] + + if port_forward: + with cli_logger.group("Forwarding ports"): + if not isinstance(port_forward, list): + port_forward = [port_forward] + for local, remote in port_forward: + cli_logger.verbose( + "Forwarding port {} to port {} on localhost.", + cf.bold(local), + cf.bold(remote), + ) # todo: msg + ssh += ["-L", "{}:localhost:{}".format(local, remote)] + + final_cmd = ( + ssh + + ssh_options.to_ssh_options_list(timeout=timeout) + + ["{}@{}".format(self.ssh_user, self.ssh_ip)] + ) + if cmd: + if environment_variables: + cmd = _with_environment_variables(cmd, environment_variables) + if is_using_login_shells(): + final_cmd += _with_interactive(cmd) + else: + final_cmd += [cmd] + else: + # We do this because `-o ControlMaster` causes the `-N` flag to + # still create an interactive shell in some ssh versions. + final_cmd.append("while true; do sleep 86400; done") + + cli_logger.verbose("Running `{}`", cf.bold(cmd)) + with cli_logger.indented(): + cli_logger.very_verbose( + "Full command is `{}`", cf.bold(" ".join(final_cmd)) + ) + + if cli_logger.verbosity > 0: + with cli_logger.indented(): + return self._run_helper( + final_cmd, with_output, exit_on_fail, silent=silent + ) + else: + return self._run_helper(final_cmd, with_output, exit_on_fail, silent=silent) + + def _create_rsync_filter_args(self, options): + rsync_excludes = options.get("rsync_exclude") or [] + rsync_filters = options.get("rsync_filter") or [] + + exclude_args = [ + ["--exclude", rsync_exclude] for rsync_exclude in rsync_excludes + ] + filter_args = [ + ["--filter", "dir-merge,- {}".format(rsync_filter)] + for rsync_filter in rsync_filters + ] + + # Combine and flatten the two lists + return [arg for args_list in exclude_args + filter_args for arg in args_list] + + def run_rsync_up(self, source, target, options=None): + self._set_ssh_ip_if_required() + options = options or {} + + # on windows use scp -r instead of rsync + if sys.platform == "win32": + # Use scp as fallback for Windows + command = ["scp", "-r"] + command += self.ssh_options.to_ssh_options_list(timeout=120) + command += [source, "{}@{}:{}".format(self.ssh_user, self.ssh_ip, target)] + else: + command = ["rsync"] + command += [ + "--rsh", + subprocess.list2cmdline( + ["ssh"] + self.ssh_options.to_ssh_options_list(timeout=120) + ), + ] + command += ["-avz"] + command += self._create_rsync_filter_args(options=options) + command += [source, "{}@{}:{}".format(self.ssh_user, self.ssh_ip, target)] + + cli_logger.verbose("Running `{}`", cf.bold(" ".join(command))) + self._run_helper(command, silent=is_rsync_silent()) + + def run_rsync_down(self, source, target, options=None): + self._set_ssh_ip_if_required() + + # on Windows use scp -r instead of rsync + if sys.platform == "win32": + # Use scp as fallback for Windows + command = ["scp", "-r"] + command += self.ssh_options.to_ssh_options_list(timeout=120) + command += ["{}@{}:{}".format(self.ssh_user, self.ssh_ip, source), target] + else: + command = ["rsync"] + command += [ + "--rsh", + subprocess.list2cmdline( + ["ssh"] + self.ssh_options.to_ssh_options_list(timeout=120) + ), + ] + command += ["-avz"] + command += self._create_rsync_filter_args(options=options) + command += ["{}@{}:{}".format(self.ssh_user, self.ssh_ip, source), target] + + cli_logger.verbose("Running `{}`", cf.bold(" ".join(command))) + self._run_helper(command, silent=is_rsync_silent()) + + def remote_shell_command_str(self): + if self.ssh_private_key: + return "ssh -o IdentitiesOnly=yes -i {} {}@{}\n".format( + self.ssh_private_key, self.ssh_user, self.ssh_ip + ) + else: + return "ssh -o IdentitiesOnly=yes {}@{}\n".format( + self.ssh_user, self.ssh_ip + ) + + +class DockerCommandRunner(CommandRunnerInterface): + def __init__(self, docker_config, **common_args): + self.ssh_command_runner = SSHCommandRunner(**common_args) + self.container_name = docker_config["container_name"] + self.docker_config = docker_config + self.home_dir = None + self.initialized = False + # Optionally use 'podman' instead of 'docker' + use_podman = docker_config.get("use_podman", False) + self.docker_cmd = "podman" if use_podman else "docker" + + def run( + self, + cmd: Optional[str] = None, + timeout: int = 120, + exit_on_fail: bool = False, + port_forward: Optional[List[Tuple[int, int]]] = None, + with_output: bool = False, + environment_variables: Optional[Dict[str, object]] = None, + run_env: str = "auto", + ssh_options_override_ssh_key: str = "", + shutdown_after_run: bool = False, + ) -> str: + if run_env == "auto": + run_env = ( + "host" + if (not bool(cmd) or cmd.find(self.docker_cmd) == 0) + else self.docker_cmd + ) + + if environment_variables: + cmd = _with_environment_variables(cmd, environment_variables) + + if run_env == self.docker_cmd: + cmd = self._docker_expand_user(cmd, any_char=True) + if is_using_login_shells(): + cmd = " ".join(_with_interactive(cmd)) + cmd = with_docker_exec( + [cmd], + container_name=self.container_name, + with_interactive=is_using_login_shells(), + docker_cmd=self.docker_cmd, + )[0] + + if shutdown_after_run: + # sudo shutdown should run after `with_docker_exec` command above + cmd += "; sudo shutdown -h now" + # Do not pass shutdown_after_run argument to ssh_command_runner.run() + # since it is handled above. + return self.ssh_command_runner.run( + cmd, + timeout=timeout, + exit_on_fail=exit_on_fail, + port_forward=port_forward, + with_output=with_output, + ssh_options_override_ssh_key=ssh_options_override_ssh_key, + ) + + def run_rsync_up(self, source, target, options=None): + options = options or {} + host_destination = os.path.join( + self._get_docker_host_mount_location(self.ssh_command_runner.cluster_name), + target.lstrip("/"), + ) + host_mount_location = os.path.dirname(host_destination.rstrip("/")) + if sys.platform == "win32": + # fix paths if running on Windows + source = source.replace("\\", "/") + host_mount_location = host_mount_location.replace("\\", "/") + host_destination = host_destination.replace("\\", "/") + + self.ssh_command_runner.run( + f"mkdir -p {host_mount_location} && chown -R " + f"{self.ssh_command_runner.ssh_user} {host_mount_location}", + silent=is_rsync_silent(), + ) + + self.ssh_command_runner.run_rsync_up(source, host_destination, options=options) + if self._check_container_status() and not options.get( + "docker_mount_if_possible", False + ): + if os.path.isdir(source): + # Adding a "." means that docker copies the *contents* + # Without it, docker copies the source *into* the target + host_destination += "/." + + # This path may not exist inside the container. This ensures + # that the path is created! + prefix = with_docker_exec( + [ + "mkdir -p {}".format( + os.path.dirname(self._docker_expand_user(target)) + ) + ], + container_name=self.container_name, + with_interactive=is_using_login_shells(), + docker_cmd=self.docker_cmd, + )[0] + + self.ssh_command_runner.run( + "{} && rsync -e '{} exec -i' -avz {} {}:{}".format( + prefix, + self.docker_cmd, + host_destination, + self.container_name, + self._docker_expand_user(target), + ), + silent=is_rsync_silent(), + ) + + def run_rsync_down(self, source, target, options=None): + options = options or {} + host_source = os.path.join( + self._get_docker_host_mount_location(self.ssh_command_runner.cluster_name), + source.lstrip("/"), + ) + host_mount_location = os.path.dirname(host_source.rstrip("/")) + # Convert Windows paths to Unix-style for remote commands + host_mount_location_unix = host_mount_location.replace("\\", "/") + self.ssh_command_runner.run( + f"mkdir -p {host_mount_location_unix} && chown -R " + f"{self.ssh_command_runner.ssh_user} {host_mount_location_unix}", + silent=is_rsync_silent(), + ) + if source[-1] == "/": + source += "." + # Adding a "." means that docker copies the *contents* + # Without it, docker copies the source *into* the target + if not options.get("docker_mount_if_possible", False): + # NOTE: `--delete` is okay here because the container is the source + # of truth. + self.ssh_command_runner.run( + "rsync -e '{} exec -i' -avz --delete {}:{} {}".format( + self.docker_cmd, + self.container_name, + self._docker_expand_user(source), + host_source.replace( + "\\", "/" + ), # Convert Windows paths to Unix-style for rsync + ), + silent=is_rsync_silent(), + ) + self.ssh_command_runner.run_rsync_down(host_source, target, options=options) + + def remote_shell_command_str(self): + inner_str = ( + self.ssh_command_runner.remote_shell_command_str() + .replace("ssh", "ssh -tt", 1) + .strip("\n") + ) + return inner_str + " {} exec -it {} /bin/bash\n".format( + self.docker_cmd, self.container_name + ) + + def _check_docker_installed(self): + no_exist = "NoExist" + output = self.ssh_command_runner.run( + f"command -v {self.docker_cmd} || echo '{no_exist}'", with_output=True + ) + cleaned_output = output.decode().strip() + if no_exist in cleaned_output or "docker" not in cleaned_output: + if self.docker_cmd == "docker": + install_commands = [ + "curl -fsSL https://get.docker.com -o get-docker.sh", + "sudo sh get-docker.sh", + "sudo usermod -aG docker $USER", + "sudo systemctl restart docker -f", + ] + else: + install_commands = [ + "sudo apt-get update", + "sudo apt-get -y install podman", + ] + + logger.error( + f"{self.docker_cmd.capitalize()} not installed. You can " + f"install {self.docker_cmd.capitalize()} by adding the " + "following commands to 'initialization_commands':\n" + + "\n".join(install_commands) + ) + + def _check_container_status(self): + if self.initialized: + return True + output = ( + self.ssh_command_runner.run( + check_docker_running_cmd(self.container_name, self.docker_cmd), + with_output=True, + ) + .decode("utf-8") + .strip() + ) + # Checks for the false positive where "true" is in the container name + return "true" in output.lower() and "no such object" not in output.lower() + + def _docker_expand_user(self, string, any_char=False): + user_pos = string.find("~") + if user_pos > -1: + if self.home_dir is None: + self.home_dir = ( + self.ssh_command_runner.run( + f"{self.docker_cmd} exec {self.container_name} " + "printenv HOME", + with_output=True, + ) + .decode("utf-8") + .strip() + ) + + if any_char: + return string.replace("~/", self.home_dir + "/") + + elif not any_char and user_pos == 0: + return string.replace("~", self.home_dir, 1) + + return string + + def _check_if_container_restart_is_needed( + self, image: str, cleaned_bind_mounts: Dict[str, str] + ) -> bool: + re_init_required = False + running_image = ( + self.run( + check_docker_image(self.container_name, self.docker_cmd), + with_output=True, + run_env="host", + ) + .decode("utf-8") + .strip() + ) + if running_image != image: + cli_logger.error( + "A container with name {} is running image {} instead " + + "of {} (which was provided in the YAML)", + self.container_name, + running_image, + image, + ) + mounts = ( + self.run( + check_bind_mounts_cmd(self.container_name, self.docker_cmd), + with_output=True, + run_env="host", + ) + .decode("utf-8") + .strip() + ) + try: + active_mounts = json.loads(mounts) + active_remote_mounts = { + mnt["Destination"].strip("/") for mnt in active_mounts + } + # Ignore ray bootstrap files. + requested_remote_mounts = { + self._docker_expand_user(remote).strip("/") + for remote in cleaned_bind_mounts.keys() + } + unfulfilled_mounts = requested_remote_mounts - active_remote_mounts + if unfulfilled_mounts: + re_init_required = True + cli_logger.warning( + "This Docker Container is already running. " + "Restarting the Docker container on " + "this node to pick up the following file_mounts {}", + unfulfilled_mounts, + ) + except json.JSONDecodeError: + cli_logger.verbose( + "Unable to check if file_mounts specified in the YAML " + "differ from those on the running container." + ) + return re_init_required + + def run_init( + self, *, as_head: bool, file_mounts: Dict[str, str], sync_run_yet: bool + ): + BOOTSTRAP_MOUNTS = ["~/ray_bootstrap_config.yaml", "~/ray_bootstrap_key.pem"] + + specific_image = self.docker_config.get( + f"{'head' if as_head else 'worker'}_image", self.docker_config.get("image") + ) + + self._check_docker_installed() + if self.docker_config.get("pull_before_run", True): + assert specific_image, ( + "Image must be included in config if " + "pull_before_run is specified" + ) + self.run( + "{} pull {}".format(self.docker_cmd, specific_image), run_env="host" + ) + else: + self.run( + f"{self.docker_cmd} image inspect {specific_image} " + "1> /dev/null 2>&1 || " + f"{self.docker_cmd} pull {specific_image}" + ) + + # Bootstrap files cannot be bind mounted because docker opens the + # underlying inode. When the file is switched, docker becomes outdated. + cleaned_bind_mounts = file_mounts.copy() + for mnt in BOOTSTRAP_MOUNTS: + cleaned_bind_mounts.pop(mnt, None) + + docker_run_executed = False + + container_running = self._check_container_status() + requires_re_init = False + if container_running: + requires_re_init = self._check_if_container_restart_is_needed( + specific_image, cleaned_bind_mounts + ) + if requires_re_init: + docker_stop_cmd = f"{self.docker_cmd} stop {self.container_name}" + logger.info("Executing Docker command: %s", docker_stop_cmd) + self.run(docker_stop_cmd, run_env="host") + + if (not container_running) or requires_re_init: + if not sync_run_yet: + # Do not start the actual image as we need to run file_sync + # first to ensure that all folders are created with the + # correct ownership. Docker will create the folders with + # `root` as the owner. + return True + # Get home directory + image_env = ( + self.ssh_command_runner.run( + f"{self.docker_cmd} " + + "inspect -f '{{json .Config.Env}}' " + + specific_image, + with_output=True, + ) + .decode() + .strip() + ) + home_directory = "/root" + try: + for env_var in json.loads(image_env): + if env_var.startswith("HOME="): + home_directory = env_var.split("HOME=")[1] + break + except json.JSONDecodeError as e: + cli_logger.error( + "Unable to deserialize `image_env` to Python object. " + f"The `image_env` is:\n{image_env}" + ) + raise e + + user_docker_run_options = self.docker_config.get( + "run_options", [] + ) + self.docker_config.get( + f"{'head' if as_head else 'worker'}_run_options", [] + ) + start_command = docker_start_cmds( + self.ssh_command_runner.ssh_user, + specific_image, + cleaned_bind_mounts, + self.container_name, + self._configure_runtime( + self._auto_configure_shm(user_docker_run_options) + ), + self.ssh_command_runner.cluster_name, + home_directory, + self.docker_cmd, + ) + self.run(start_command, run_env="host") + docker_run_executed = True + + # Explicitly copy in ray bootstrap files. + for mount in BOOTSTRAP_MOUNTS: + if mount in file_mounts: + if not sync_run_yet: + # NOTE(ilr) This rsync is needed because when starting from + # a stopped instance, /tmp may be deleted and `run_init` + # is called before the first `file_sync` happens + self.run_rsync_up(file_mounts[mount], mount) + self.ssh_command_runner.run( + "rsync -e '{cmd} exec -i' -avz {src} {container}:{dst}".format( + cmd=self.docker_cmd, + src=os.path.join( + self._get_docker_host_mount_location( + self.ssh_command_runner.cluster_name + ), + mount, + ).replace( + "\\", "/" + ), # Convert Windows paths to Unix-style for rsync + container=self.container_name, + dst=self._docker_expand_user(mount), + ) + ) + try: + # Check if the current user has read permission. + # If they do not, try to change ownership! + self.run( + f"cat {mount} >/dev/null 2>&1 || " + f"sudo chown $(id -u):$(id -g) {mount}" + ) + except Exception: + lsl_string = ( + self.run(f"ls -l {mount}", with_output=True) + .decode("utf-8") + .strip() + ) + # The string is of format + # + permissions = lsl_string.split(" ")[0] + owner = lsl_string.split(" ")[2] + group = lsl_string.split(" ")[3] + current_user = ( + self.run("whoami", with_output=True).decode("utf-8").strip() + ) + cli_logger.warning( + f"File ({mount}) is owned by user:{owner} and group:" + f"{group} with permissions ({permissions}). The " + f"current user ({current_user}) does not have " + "permission to read these files, and Ray may not be " + "able to autoscale. This can be resolved by " + "installing `sudo` in your container, or adding a " + f"command like 'chown {current_user} {mount}' to " + "your `setup_commands`." + ) + self.initialized = True + return docker_run_executed + + def _configure_runtime(self, run_options: List[str]) -> List[str]: + if self.docker_config.get("disable_automatic_runtime_detection"): + return run_options + + runtime_output = ( + self.ssh_command_runner.run( + f"{self.docker_cmd} " + "info -f '{{.Runtimes}}' ", with_output=True + ) + .decode() + .strip() + ) + if "nvidia-container-runtime" in runtime_output: + try: + self.ssh_command_runner.run("nvidia-smi", with_output=False) + return run_options + ["--runtime=nvidia"] + except Exception as e: + logger.warning( + "NVIDIA Container Runtime is present, but no GPUs found." + ) + logger.debug(f"nvidia-smi error: {e}") + return run_options + + return run_options + + def _auto_configure_shm(self, run_options: List[str]) -> List[str]: + if self.docker_config.get("disable_shm_size_detection"): + return run_options + for run_opt in run_options: + if "--shm-size" in run_opt: + logger.info( + "Bypassing automatic SHM-Detection because of " + f"`run_option`: {run_opt}" + ) + return run_options + try: + shm_output = ( + self.ssh_command_runner.run( + "cat /proc/meminfo || true", with_output=True + ) + .decode() + .strip() + ) + available_memory = int( + [ln for ln in shm_output.split("\n") if "MemAvailable" in ln][ + 0 + ].split()[1] + ) + available_memory_bytes = available_memory * 1024 + # Overestimate SHM size by 10% + shm_size = min( + (available_memory_bytes * DEFAULT_OBJECT_STORE_MEMORY_PROPORTION * 1.1), + DEFAULT_OBJECT_STORE_MAX_MEMORY_BYTES, + ) + return run_options + [f"--shm-size='{shm_size}b'"] + except Exception as e: + logger.warning(f"Received error while trying to auto-compute SHM size {e}") + return run_options + + def _get_docker_host_mount_location(self, cluster_name: str) -> str: + """Return the docker host mount directory location.""" + # Imported here due to circular dependency in imports. + from ray.autoscaler.sdk import get_docker_host_mount_location + + return get_docker_host_mount_location(cluster_name) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/commands.py b/lib/python3.12/site-packages/ray/autoscaler/_private/commands.py new file mode 100644 index 0000000000000000000000000000000000000000..b3585d0bdeb771417c3f2fe66cc3caa1e01c8451 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/commands.py @@ -0,0 +1,1705 @@ +import copy +import datetime +import hashlib +import json +import logging +import os +import random +import shutil +import subprocess +import sys +import tempfile +import time +from concurrent.futures import ThreadPoolExecutor +from types import ModuleType +from typing import Any, Dict, List, Optional, Tuple, Union + +import click +import yaml + +import ray +from ray._common.usage import usage_lib +from ray.autoscaler._private import subprocess_output_util as cmd_output_util +from ray.autoscaler._private.autoscaler import AutoscalerSummary +from ray.autoscaler._private.cli_logger import cf, cli_logger +from ray.autoscaler._private.cluster_dump import ( + Archive, + GetParameters, + Node, + _info_from_params, + create_archive_for_local_and_remote_nodes, + create_archive_for_remote_nodes, + get_all_local_data, +) +from ray.autoscaler._private.command_runner import ( + set_rsync_silent, + set_using_login_shells, +) +from ray.autoscaler._private.constants import ( + AUTOSCALER_RESOURCE_REQUEST_CHANNEL, + MAX_PARALLEL_SHUTDOWN_WORKERS, +) +from ray.autoscaler._private.event_system import CreateClusterEvent, global_event_system +from ray.autoscaler._private.log_timer import LogTimer +from ray.autoscaler._private.node_provider_availability_tracker import ( + NodeAvailabilitySummary, +) +from ray.autoscaler._private.providers import ( + _NODE_PROVIDERS, + _PROVIDER_PRETTY_NAMES, + _get_node_provider, +) +from ray.autoscaler._private.updater import NodeUpdaterThread +from ray.autoscaler._private.util import ( + LoadMetricsSummary, + format_info_string, + hash_launch_conf, + hash_runtime_conf, + prepare_config, + validate_config, + with_envs, +) +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + NODE_KIND_WORKER, + STATUS_UNINITIALIZED, + STATUS_UP_TO_DATE, + TAG_RAY_LAUNCH_CONFIG, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_NAME, + TAG_RAY_NODE_STATUS, + TAG_RAY_USER_NODE_TYPE, +) +from ray.experimental.internal_kv import _internal_kv_put, internal_kv_get_gcs_client +from ray.util.debug import log_once + +try: # py3 + from shlex import quote +except ImportError: # py2 + from pipes import quote + + +logger = logging.getLogger(__name__) + +RUN_ENV_TYPES = ["auto", "host", "docker"] + +POLL_INTERVAL = 5 + +Port_forward = Union[Tuple[int, int], List[Tuple[int, int]]] + + +def try_logging_config(config: Dict[str, Any]) -> None: + if config["provider"]["type"] == "aws": + from ray.autoscaler._private.aws.config import log_to_cli + + log_to_cli(config) + + +def try_get_log_state(provider_config: Dict[str, Any]) -> Optional[dict]: + if provider_config["type"] == "aws": + from ray.autoscaler._private.aws.config import get_log_state + + return get_log_state() + return None + + +def try_reload_log_state(provider_config: Dict[str, Any], log_state: dict) -> None: + if not log_state: + return + if provider_config["type"] == "aws": + from ray.autoscaler._private.aws.config import reload_log_state + + return reload_log_state(log_state) + + +def debug_status( + status, error, verbose: bool = False, address: Optional[str] = None +) -> str: + """ + Return a debug string for the autoscaler. + + Args: + status: The autoscaler status string for v1 + error: The autoscaler error string for v1 + verbose: Whether to print verbose information. + address: The address of the cluster (gcs address). + + Returns: + str: A debug string for the cluster's status. + """ + from ray.autoscaler.v2.utils import is_autoscaler_v2 + + if is_autoscaler_v2(): + from ray.autoscaler.v2.sdk import get_cluster_status + from ray.autoscaler.v2.utils import ClusterStatusFormatter + + cluster_status = get_cluster_status(address) + status = ClusterStatusFormatter.format(cluster_status, verbose=verbose) + elif status: + status = status.decode("utf-8") + status_dict = json.loads(status) + lm_summary_dict = status_dict.get("load_metrics_report") + autoscaler_summary_dict = status_dict.get("autoscaler_report") + timestamp = status_dict.get("time") + gcs_request_time = status_dict.get("gcs_request_time") + non_terminated_nodes_time = status_dict.get("non_terminated_nodes_time") + if lm_summary_dict and autoscaler_summary_dict and timestamp: + lm_summary = LoadMetricsSummary(**lm_summary_dict) + node_availability_summary_dict = autoscaler_summary_dict.pop( + "node_availability_summary", {} + ) + node_availability_summary = NodeAvailabilitySummary.from_fields( + **node_availability_summary_dict + ) + autoscaler_summary = AutoscalerSummary( + node_availability_summary=node_availability_summary, + **autoscaler_summary_dict, + ) + report_time = datetime.datetime.fromtimestamp(timestamp) + status = format_info_string( + lm_summary, + autoscaler_summary, + time=report_time, + gcs_request_time=gcs_request_time, + non_terminated_nodes_time=non_terminated_nodes_time, + verbose=verbose, + ) + else: + status = ( + "No cluster status. It may take a few seconds " + "for the Ray internal services to start up." + ) + else: + status = ( + "No cluster status. It may take a few seconds " + "for the Ray internal services to start up." + ) + + if error: + status += "\n" + status += error.decode("utf-8") + + return status + + +def request_resources( + num_cpus: Optional[int] = None, + bundles: Optional[List[dict]] = None, + bundle_label_selectors: Optional[List[dict]] = None, +) -> None: + """Remotely request some CPU or GPU resources from the autoscaler. Optionally + specify label selectors for nodes with the requested resources. + + If `bundle_label_selectors` is provided, `bundles` must also be provided. + Both must be lists of the same length, and `bundle_label_selectors` expects a list + of string dictionaries. + + This function is to be called e.g. on a node before submitting a bunch of + ray.remote calls to ensure that resources rapidly become available. + + Args: + num_cpus: Scale the cluster to ensure this number of CPUs are + available. This request is persistent until another call to + request_resources() is made. + bundles (List[ResourceDict]): Scale the cluster to ensure this set of + resource shapes can fit. This request is persistent until another + call to request_resources() is made. + bundle_label_selectors (List[Dict[str,str]]): Optional label selectors + that new nodes must satisfy. (e.g. [{"accelerator-type": "A100"}]) + The elements in the bundle_label_selectors should be one-to-one mapping + to the elements in bundles. + """ + if not ray.is_initialized(): + raise RuntimeError("Ray is not initialized yet") + to_request = [] + for _ in range(num_cpus or 0): + to_request.append({"resources": {"CPU": 1}, "label_selector": {}}) + assert not bundle_label_selectors or ( + bundles and len(bundles) == len(bundle_label_selectors) + ), "If bundle_label_selectors is provided, bundles must also be provided and have the same length." + if bundles: + for i, bundle in enumerate(bundles): + selector = bundle_label_selectors[i] if bundle_label_selectors else {} + to_request.append({"resources": bundle, "label_selector": selector}) + + _internal_kv_put( + AUTOSCALER_RESOURCE_REQUEST_CHANNEL, json.dumps(to_request), overwrite=True + ) + + from ray.autoscaler.v2.utils import is_autoscaler_v2 + + if is_autoscaler_v2(): + from ray.autoscaler.v2.sdk import request_cluster_resources + + gcs_address = internal_kv_get_gcs_client().address + request_cluster_resources(gcs_address, to_request) + + +def create_or_update_cluster( + config_file: str, + override_min_workers: Optional[int], + override_max_workers: Optional[int], + no_restart: bool, + restart_only: bool, + yes: bool, + override_cluster_name: Optional[str] = None, + no_config_cache: bool = False, + redirect_command_output: Optional[bool] = False, + use_login_shells: bool = True, + no_monitor_on_head: bool = False, +) -> Dict[str, Any]: + """Creates or updates an autoscaling Ray cluster from a config json.""" + # no_monitor_on_head is an internal flag used by the Ray K8s operator. + # If True, prevents autoscaling config sync to the Ray head during cluster + # creation. See https://github.com/ray-project/ray/pull/13720. + set_using_login_shells(use_login_shells) + if not use_login_shells: + cmd_output_util.set_allow_interactive(False) + if redirect_command_output is None: + # Do not redirect by default. + cmd_output_util.set_output_redirected(False) + else: + cmd_output_util.set_output_redirected(redirect_command_output) + + def handle_yaml_error(e): + cli_logger.error("Cluster config invalid") + cli_logger.newline() + cli_logger.error("Failed to load YAML file " + cf.bold("{}"), config_file) + cli_logger.newline() + with cli_logger.verbatim_error_ctx("PyYAML error:"): + cli_logger.error(e) + cli_logger.abort() + + try: + config = yaml.safe_load(open(config_file).read()) + except FileNotFoundError: + cli_logger.abort( + "Provided cluster configuration file ({}) does not exist", + cf.bold(config_file), + ) + except yaml.parser.ParserError as e: + handle_yaml_error(e) + raise + except yaml.scanner.ScannerError as e: + handle_yaml_error(e) + raise + global_event_system.execute_callback( + CreateClusterEvent.up_started, {"cluster_config": config} + ) + + # todo: validate file_mounts, ssh keys, etc. + + importer = _NODE_PROVIDERS.get(config["provider"]["type"]) + if not importer: + cli_logger.abort( + "Unknown provider type " + cf.bold("{}") + "\n" + "Available providers are: {}", + config["provider"]["type"], + cli_logger.render_list( + [k for k in _NODE_PROVIDERS.keys() if _NODE_PROVIDERS[k] is not None] + ), + ) + + printed_overrides = False + + def handle_cli_override(key, override): + if override is not None: + if key in config: + nonlocal printed_overrides + printed_overrides = True + cli_logger.warning( + "`{}` override provided on the command line.\n" + " Using " + + cf.bold("{}") + + cf.dimmed(" [configuration file has " + cf.bold("{}") + "]"), + key, + override, + config[key], + ) + config[key] = override + + handle_cli_override("min_workers", override_min_workers) + handle_cli_override("max_workers", override_max_workers) + handle_cli_override("cluster_name", override_cluster_name) + + if printed_overrides: + cli_logger.newline() + + cli_logger.labeled_value("Cluster", config["cluster_name"]) + + cli_logger.newline() + config = _bootstrap_config(config, no_config_cache=no_config_cache) + + try_logging_config(config) + get_or_create_head_node( + config, + config_file, + no_restart, + restart_only, + yes, + override_cluster_name, + no_monitor_on_head, + ) + return config + + +CONFIG_CACHE_VERSION = 1 + + +def _bootstrap_config( + config: Dict[str, Any], no_config_cache: bool = False +) -> Dict[str, Any]: + config = prepare_config(config) + # NOTE: multi-node-type autoscaler is guaranteed to be in use after this. + + hasher = hashlib.sha1() + hasher.update(json.dumps([config], sort_keys=True).encode("utf-8")) + cache_key = os.path.join( + tempfile.gettempdir(), "ray-config-{}".format(hasher.hexdigest()) + ) + + if os.path.exists(cache_key) and not no_config_cache: + config_cache = json.loads(open(cache_key).read()) + if config_cache.get("_version", -1) == CONFIG_CACHE_VERSION: + # todo: is it fine to re-resolve? afaik it should be. + # we can have migrations otherwise or something + # but this seems overcomplicated given that resolving is + # relatively cheap + try_reload_log_state( + config_cache["config"]["provider"], + config_cache.get("provider_log_info"), + ) + + if log_once("_printed_cached_config_warning"): + cli_logger.verbose_warning( + "Loaded cached provider configuration from " + cf.bold("{}"), + cache_key, + ) + if cli_logger.verbosity == 0: + cli_logger.warning("Loaded cached provider configuration") + cli_logger.warning( + "If you experience issues with " + "the cloud provider, try re-running " + "the command with {}.", + cf.bold("--no-config-cache"), + ) + + cached_config = config_cache["config"] + if "provider" in cached_config: + cached_config["provider"]["_config_cache_path"] = cache_key + return cached_config + else: + cli_logger.warning( + "Found cached cluster config " + "but the version " + cf.bold("{}") + " " + "(expected " + cf.bold("{}") + ") does not match.\n" + "This is normal if cluster launcher was updated.\n" + "Config will be re-resolved.", + config_cache.get("_version", "none"), + CONFIG_CACHE_VERSION, + ) + + importer = _NODE_PROVIDERS.get(config["provider"]["type"]) + if not importer: + raise NotImplementedError("Unsupported provider {}".format(config["provider"])) + + provider_cls = importer(config["provider"]) + + cli_logger.print( + "Checking {} environment settings", + _PROVIDER_PRETTY_NAMES.get(config["provider"]["type"]), + ) + try: + config = provider_cls.fillout_available_node_types_resources(config) + except Exception as exc: + if cli_logger.verbosity > 2: + logger.exception("Failed to autodetect node resources.") + else: + cli_logger.warning( + f"Failed to autodetect node resources: {str(exc)}. " + "You can see full stack trace with higher verbosity." + ) + + try: + # NOTE: if `resources` field is missing, validate_config for providers + # other than AWS and Kubernetes will fail (the schema error will ask + # the user to manually fill the resources) as we currently support + # autofilling resources for AWS and Kubernetes only. + validate_config(config) + except (ModuleNotFoundError, ImportError): + cli_logger.abort( + "Not all Ray autoscaler dependencies were found. " + "In Ray 1.4+, the Ray CLI, autoscaler, and dashboard will " + 'only be usable via `pip install "ray[default]"`. Please ' + "update your install command." + ) + resolved_config = provider_cls.bootstrap_config(config) + resolved_config["provider"]["_config_cache_path"] = cache_key + + if not no_config_cache: + with open(cache_key, "w") as f: + config_cache = { + "_version": CONFIG_CACHE_VERSION, + "provider_log_info": try_get_log_state(resolved_config["provider"]), + "config": resolved_config, + } + f.write(json.dumps(config_cache)) + return resolved_config + + +def teardown_cluster( + config_file: str, + yes: bool, + workers_only: bool, + override_cluster_name: Optional[str], + keep_min_workers: bool, +) -> None: + """Destroys all nodes of a Ray cluster described by a config json.""" + config = yaml.safe_load(open(config_file).read()) + if override_cluster_name is not None: + config["cluster_name"] = override_cluster_name + + config = _bootstrap_config(config) + + cli_logger.confirm(yes, "Destroying cluster.", _abort=True) + + if not workers_only: + try: + exec_cluster( + config_file, + cmd="ray stop", + run_env="auto", + screen=False, + tmux=False, + stop=False, + start=False, + override_cluster_name=override_cluster_name, + port_forward=None, + with_output=False, + ) + except Exception as e: + # todo: add better exception info + cli_logger.verbose_error("{}", str(e)) + cli_logger.warning( + "Exception occurred when stopping the cluster Ray runtime " + "(use -v to dump teardown exceptions)." + ) + cli_logger.warning( + "Ignoring the exception and " + "attempting to shut down the cluster nodes anyway." + ) + + provider = _get_node_provider(config["provider"], config["cluster_name"]) + + def remaining_nodes(): + workers = provider.non_terminated_nodes({TAG_RAY_NODE_KIND: NODE_KIND_WORKER}) + + if keep_min_workers: + min_workers = config.get("min_workers", 0) + cli_logger.print( + "{} random worker nodes will not be shut down. " + + cf.dimmed("(due to {})"), + cf.bold(min_workers), + cf.bold("--keep-min-workers"), + ) + + workers = random.sample(workers, len(workers) - min_workers) + + # todo: it's weird to kill the head node but not all workers + if workers_only: + cli_logger.print( + "The head node will not be shut down. " + cf.dimmed("(due to {})"), + cf.bold("--workers-only"), + ) + + return workers + + head = provider.non_terminated_nodes({TAG_RAY_NODE_KIND: NODE_KIND_HEAD}) + + return head + workers + + def run_docker_stop(node, container_name): + try: + updater = NodeUpdaterThread( + node_id=node, + provider_config=config["provider"], + provider=provider, + auth_config=config["auth"], + cluster_name=config["cluster_name"], + file_mounts=config["file_mounts"], + initialization_commands=[], + setup_commands=[], + ray_start_commands=[], + runtime_hash="", + file_mounts_contents_hash="", + is_head_node=False, + docker_config=config.get("docker"), + ) + + _exec( + updater, + f"docker stop {container_name}", + with_output=False, + run_env="host", + ) + except Exception: + cli_logger.warning(f"Docker stop failed on {node}") + + # Loop here to check that both the head and worker nodes are actually + # really gone + A = remaining_nodes() + + container_name = config.get("docker", {}).get("container_name") + if container_name: + # This is to ensure that the parallel SSH calls below do not mess with + # the users terminal. + output_redir = cmd_output_util.is_output_redirected() + cmd_output_util.set_output_redirected(True) + allow_interactive = cmd_output_util.does_allow_interactive() + cmd_output_util.set_allow_interactive(False) + + with ThreadPoolExecutor(max_workers=MAX_PARALLEL_SHUTDOWN_WORKERS) as executor: + for node in A: + executor.submit( + run_docker_stop, node=node, container_name=container_name + ) + cmd_output_util.set_output_redirected(output_redir) + cmd_output_util.set_allow_interactive(allow_interactive) + with LogTimer("teardown_cluster: done."): + while A: + provider.terminate_nodes(A) + + cli_logger.print( + "Requested {} nodes to shut down.", + cf.bold(len(A)), + _tags=dict(interval="1s"), + ) + + time.sleep(POLL_INTERVAL) # todo: interval should be a variable + A = remaining_nodes() + cli_logger.print( + "{} nodes remaining after {} second(s).", cf.bold(len(A)), POLL_INTERVAL + ) + cli_logger.success("No nodes remaining.") + + # Cleanup shared cluster resources if provider supports it + if hasattr(provider, "cleanup_cluster_resources") and not workers_only: + try: + cli_logger.print("Cleaning up shared cluster resources...") + provider.cleanup_cluster_resources() + cli_logger.success("Shared cluster resources cleaned up.") + except Exception as e: + cli_logger.verbose_error("{}", str(e)) + cli_logger.warning( + "Failed to cleanup shared cluster resources " + "(use -v to see details). " + "You may need to manually delete MSI, NSG, and Subnet resources." + ) + + +def kill_node( + config_file: str, yes: bool, hard: bool, override_cluster_name: Optional[str] +) -> Optional[str]: + """Kills a random Raylet worker.""" + + config = yaml.safe_load(open(config_file).read()) + if override_cluster_name is not None: + config["cluster_name"] = override_cluster_name + config = _bootstrap_config(config) + + cli_logger.confirm(yes, "A random node will be killed.") + + provider = _get_node_provider(config["provider"], config["cluster_name"]) + nodes = provider.non_terminated_nodes({TAG_RAY_NODE_KIND: NODE_KIND_WORKER}) + if not nodes: + cli_logger.print("No worker nodes detected.") + return None + node = random.choice(nodes) + cli_logger.print("Shutdown " + cf.bold("{}"), node) + if hard: + provider.terminate_node(node) + else: + updater = NodeUpdaterThread( + node_id=node, + provider_config=config["provider"], + provider=provider, + auth_config=config["auth"], + cluster_name=config["cluster_name"], + file_mounts=config["file_mounts"], + initialization_commands=[], + setup_commands=[], + ray_start_commands=[], + runtime_hash="", + file_mounts_contents_hash="", + is_head_node=False, + docker_config=config.get("docker"), + ) + + _exec(updater, "ray stop", False, False) + + time.sleep(POLL_INTERVAL) + + if config.get("provider", {}).get("use_internal_ips", False): + node_ip = provider.internal_ip(node) + else: + node_ip = provider.external_ip(node) + + return node_ip + + +def monitor_cluster( + cluster_config_file: str, num_lines: int, override_cluster_name: Optional[str] +) -> None: + """Tails the autoscaler logs of a Ray cluster.""" + cmd = f"tail -n {num_lines} -f /tmp/ray/session_latest/logs/monitor*" + exec_cluster( + cluster_config_file, + cmd=cmd, + run_env="auto", + screen=False, + tmux=False, + stop=False, + start=False, + override_cluster_name=override_cluster_name, + port_forward=None, + ) + + +def warn_about_bad_start_command( + start_commands: List[str], no_monitor_on_head: bool = False +) -> None: + ray_start_cmd = list(filter(lambda x: "ray start" in x, start_commands)) + if len(ray_start_cmd) == 0: + cli_logger.warning( + "Ray runtime will not be started because `{}` is not in `{}`.", + cf.bold("ray start"), + cf.bold("head_start_ray_commands"), + ) + + autoscaling_config_in_ray_start_cmd = any( + "autoscaling-config" in x for x in ray_start_cmd + ) + if not (autoscaling_config_in_ray_start_cmd or no_monitor_on_head): + cli_logger.warning( + "The head node will not launch any workers because " + "`{}` does not have `{}` set.\n" + "Potential fix: add `{}` to the `{}` command under `{}`.", + cf.bold("ray start"), + cf.bold("--autoscaling-config"), + cf.bold("--autoscaling-config=~/ray_bootstrap_config.yaml"), + cf.bold("ray start"), + cf.bold("head_start_ray_commands"), + ) + + +def get_or_create_head_node( + config: Dict[str, Any], + printable_config_file: str, + no_restart: bool, + restart_only: bool, + yes: bool, + override_cluster_name: Optional[str], + no_monitor_on_head: bool = False, + _provider: Optional[NodeProvider] = None, + _runner: ModuleType = subprocess, +) -> None: + """Create the cluster head node, which in turn creates the workers.""" + global_event_system.execute_callback(CreateClusterEvent.cluster_booting_started) + provider = _provider or _get_node_provider( + config["provider"], config["cluster_name"] + ) + + config = copy.deepcopy(config) + head_node_tags = { + TAG_RAY_NODE_KIND: NODE_KIND_HEAD, + } + nodes = provider.non_terminated_nodes(head_node_tags) + if len(nodes) > 0: + head_node = nodes[0] + else: + head_node = None + + if not head_node: + cli_logger.confirm( + yes, "No head node found. Launching a new cluster.", _abort=True + ) + cli_logger.newline() + usage_lib.show_usage_stats_prompt(cli=True) + + if head_node: + if restart_only: + cli_logger.confirm( + yes, + "Updating cluster configuration and " + "restarting the cluster Ray runtime. " + "Setup commands will not be run due to `{}`.\n", + cf.bold("--restart-only"), + _abort=True, + ) + cli_logger.newline() + usage_lib.show_usage_stats_prompt(cli=True) + elif no_restart: + cli_logger.print( + "Cluster Ray runtime will not be restarted due to `{}`.", + cf.bold("--no-restart"), + ) + cli_logger.confirm( + yes, + "Updating cluster configuration and running setup commands.", + _abort=True, + ) + else: + cli_logger.print("Updating cluster configuration and running full setup.") + cli_logger.confirm( + yes, cf.bold("Cluster Ray runtime will be restarted."), _abort=True + ) + cli_logger.newline() + usage_lib.show_usage_stats_prompt(cli=True) + + cli_logger.newline() + # TODO(ekl) this logic is duplicated in node_launcher.py (keep in sync) + head_node_config = copy.deepcopy(config.get("head_node", {})) + # The above `head_node` field is deprecated in favor of per-node-type + # node_configs. We allow it for backwards-compatibility. + head_node_resources = None + head_node_labels = None + head_node_type = config.get("head_node_type") + if head_node_type: + head_node_tags[TAG_RAY_USER_NODE_TYPE] = head_node_type + head_config = config["available_node_types"][head_node_type] + head_node_config.update(head_config["node_config"]) + + # Not necessary to keep in sync with node_launcher.py + # Keep in sync with autoscaler.py _node_resources + head_node_resources = head_config.get("resources") + head_node_labels = head_config.get("labels") + + launch_hash = hash_launch_conf(head_node_config, config["auth"]) + creating_new_head = _should_create_new_head( + head_node, launch_hash, head_node_type, provider + ) + if creating_new_head: + with cli_logger.group("Acquiring an up-to-date head node"): + global_event_system.execute_callback( + CreateClusterEvent.acquiring_new_head_node + ) + if head_node is not None: + cli_logger.confirm(yes, "Relaunching the head node.", _abort=True) + + provider.terminate_node(head_node) + cli_logger.print("Terminated head node {}", head_node) + + head_node_tags[TAG_RAY_LAUNCH_CONFIG] = launch_hash + head_node_tags[TAG_RAY_NODE_NAME] = "ray-{}-head".format( + config["cluster_name"] + ) + head_node_tags[TAG_RAY_NODE_STATUS] = STATUS_UNINITIALIZED + provider.create_node(head_node_config, head_node_tags, 1) + cli_logger.print("Launched a new head node") + + start = time.time() + head_node = None + with cli_logger.group("Fetching the new head node"): + while True: + if time.time() - start > 50: + cli_logger.abort( + "Head node fetch timed out. Failed to create head node." + ) + nodes = provider.non_terminated_nodes(head_node_tags) + if len(nodes) == 1: + head_node = nodes[0] + break + time.sleep(POLL_INTERVAL) + cli_logger.newline() + + global_event_system.execute_callback(CreateClusterEvent.head_node_acquired) + + with cli_logger.group( + "Setting up head node", + _numbered=("<>", 1, 1), + # cf.bold(provider.node_tags(head_node)[TAG_RAY_NODE_NAME]), + _tags=dict(), + ): # add id, ARN to tags? + # TODO(ekl) right now we always update the head node even if the + # hash matches. + # We could prompt the user for what they want to do here. + # No need to pass in cluster_sync_files because we use this + # hash to set up the head node + (runtime_hash, file_mounts_contents_hash) = hash_runtime_conf( + config["file_mounts"], None, config + ) + + if not no_monitor_on_head: + # Return remote_config_file to avoid prematurely closing it. + config, remote_config_file = _set_up_config_for_head_node( + config, provider, no_restart + ) + cli_logger.print("Prepared bootstrap config") + + if restart_only: + # Docker may re-launch nodes, requiring setup + # commands to be rerun. + if config.get("docker", {}).get("container_name"): + setup_commands = config["head_setup_commands"] + else: + setup_commands = [] + ray_start_commands = config["head_start_ray_commands"] + # If user passed in --no-restart and we're not creating a new head, + # omit start commands. + elif no_restart and not creating_new_head: + setup_commands = config["head_setup_commands"] + ray_start_commands = [] + else: + setup_commands = config["head_setup_commands"] + ray_start_commands = config["head_start_ray_commands"] + + if not no_restart: + warn_about_bad_start_command(ray_start_commands, no_monitor_on_head) + + # Use RAY_UP_enable_autoscaler_v2 instead of RAY_enable_autoscaler_v2 + # to avoid accidentally enabling autoscaler v2 for ray up + # due to env inheritance. The default value is 1 since Ray 2.50.0. + if os.getenv("RAY_UP_enable_autoscaler_v2", "1") == "1": + if "RAY_UP_enable_autoscaler_v2" not in os.environ: + # TODO (rueian): Remove this notice after Ray 2.52.0. + cli_logger.print( + "Autoscaler v2 is now enabled by default (since Ray 2.50.0). " + "To switch back to v1, set {}=0. This message can be suppressed by setting {} explicitly.", + cf.bold("RAY_UP_enable_autoscaler_v2"), + cf.bold("RAY_UP_enable_autoscaler_v2"), + ) + ray_start_commands = with_envs( + ray_start_commands, + { + "RAY_enable_autoscaler_v2": "1", + "RAY_CLOUD_INSTANCE_ID": head_node, + "RAY_NODE_TYPE_NAME": head_node_type, + }, + ) + + updater = NodeUpdaterThread( + node_id=head_node, + provider_config=config["provider"], + provider=provider, + auth_config=config["auth"], + cluster_name=config["cluster_name"], + file_mounts=config["file_mounts"], + initialization_commands=config["initialization_commands"], + setup_commands=setup_commands, + ray_start_commands=ray_start_commands, + process_runner=_runner, + runtime_hash=runtime_hash, + file_mounts_contents_hash=file_mounts_contents_hash, + is_head_node=True, + node_resources=head_node_resources, + node_labels=head_node_labels, + rsync_options={ + "rsync_exclude": config.get("rsync_exclude"), + "rsync_filter": config.get("rsync_filter"), + }, + docker_config=config.get("docker"), + restart_only=restart_only, + ) + updater.start() + updater.join() + + # Refresh the node cache so we see the external ip if available + provider.non_terminated_nodes(head_node_tags) + + if updater.exitcode != 0: + # todo: this does not follow the mockup and is not good enough + cli_logger.abort("Failed to setup head node.") + sys.exit(1) + + global_event_system.execute_callback( + CreateClusterEvent.cluster_booting_completed, + { + "head_node_id": head_node, + }, + ) + + monitor_str = "tail -n 100 -f /tmp/ray/session_latest/logs/monitor*" + if override_cluster_name: + modifiers = " --cluster-name={}".format(quote(override_cluster_name)) + else: + modifiers = "" + + cli_logger.newline() + with cli_logger.group("Useful commands:"): + printable_config_file = os.path.abspath(printable_config_file) + + cli_logger.print("To terminate the cluster:") + cli_logger.print(cf.bold(f" ray down {printable_config_file}{modifiers}")) + cli_logger.newline() + + cli_logger.print("To retrieve the IP address of the cluster head:") + cli_logger.print( + cf.bold(f" ray get-head-ip {printable_config_file}{modifiers}") + ) + cli_logger.newline() + + cli_logger.print( + "To port-forward the cluster's Ray Dashboard to the local machine:" + ) + cli_logger.print(cf.bold(f" ray dashboard {printable_config_file}{modifiers}")) + cli_logger.newline() + + cli_logger.print( + "To submit a job to the cluster, port-forward the " + "Ray Dashboard in another terminal and run:" + ) + cli_logger.print( + cf.bold( + " ray job submit --address http://localhost: " + "--working-dir . -- python my_script.py" + ) + ) + cli_logger.newline() + + cli_logger.print("To connect to a terminal on the cluster head for debugging:") + cli_logger.print(cf.bold(f" ray attach {printable_config_file}{modifiers}")) + cli_logger.newline() + + cli_logger.print("To monitor autoscaling:") + cli_logger.print( + cf.bold( + f" ray exec {printable_config_file}{modifiers} {quote(monitor_str)}" + ) + ) + cli_logger.newline() + + # Clean up temporary config file if it was created + # Clean up temporary config file if it was created on Windows + if ( + sys.platform == "win32" + and not no_monitor_on_head + and "remote_config_file" in locals() + ): + try: + os.remove(remote_config_file.name) + except OSError: + pass # Ignore cleanup errors + + +def _should_create_new_head( + head_node_id: Optional[str], + new_launch_hash: str, + new_head_node_type: str, + provider: NodeProvider, +) -> bool: + """Decides whether a new head node needs to be created. + + We need a new head if at least one of the following holds: + (a) There isn't an existing head node + (b) The user-submitted head node_config differs from the existing head + node's node_config. + (c) The user-submitted head node_type key differs from the existing head + node's node_type. + + Args: + head_node_id (Optional[str]): head node id if a head exists, else None + new_launch_hash: hash of current user-submitted head config + new_head_node_type: current user-submitted head node-type key + + Returns: + bool: True if a new Ray head node should be launched, False otherwise + """ + if not head_node_id: + # No head node exists, need to create it. + return True + + # Pull existing head's data. + head_tags = provider.node_tags(head_node_id) + current_launch_hash = head_tags.get(TAG_RAY_LAUNCH_CONFIG) + current_head_type = head_tags.get(TAG_RAY_USER_NODE_TYPE) + + # Compare to current head + hashes_mismatch = new_launch_hash != current_launch_hash + types_mismatch = new_head_node_type != current_head_type + + new_head_required = hashes_mismatch or types_mismatch + + # Warn user + if new_head_required: + with cli_logger.group( + "Currently running head node is out-of-date with cluster configuration" + ): + if hashes_mismatch: + cli_logger.print( + "Current hash is {}, expected {}", + cf.bold(current_launch_hash), + cf.bold(new_launch_hash), + ) + + if types_mismatch: + cli_logger.print( + "Current head node type is {}, expected {}", + cf.bold(current_head_type), + cf.bold(new_head_node_type), + ) + + return new_head_required + + +def _set_up_config_for_head_node( + config: Dict[str, Any], provider: NodeProvider, no_restart: bool +) -> Tuple[Dict[str, Any], Any]: + """Prepares autoscaling config and, if needed, ssh key, to be mounted onto + the Ray head node for use by the autoscaler. + + Returns the modified config and the temporary config file that will be + mounted onto the head node. + """ + # Rewrite the auth config so that the head + # node can update the workers + remote_config = copy.deepcopy(config) + + # drop proxy options if they exist, otherwise + # head node won't be able to connect to workers + remote_config["auth"].pop("ssh_proxy_command", None) + + # Drop the head_node field if it was introduced. It is technically not a + # valid field in the config, but it may have been introduced after + # validation (see _bootstrap_config() call to + # provider_cls.bootstrap_config(config)). The head node will never try to + # launch a head node so it doesn't need these defaults. + remote_config.pop("head_node", None) + + if "ssh_private_key" in config["auth"]: + remote_key_path = "~/ray_bootstrap_key.pem" + remote_config["auth"]["ssh_private_key"] = remote_key_path + + # Adjust for new file locations + new_mounts = {} + for remote_path in config["file_mounts"]: + new_mounts[remote_path] = remote_path + remote_config["file_mounts"] = new_mounts + remote_config["no_restart"] = no_restart + + remote_config = provider.prepare_for_head_node(remote_config) + + # Now inject the rewritten config and SSH key into the head node + is_windows = sys.platform == "win32" + remote_config_file = tempfile.NamedTemporaryFile( + "w", prefix="ray-bootstrap-", delete=not is_windows + ) + remote_config_file.write(json.dumps(remote_config)) + remote_config_file.flush() + if is_windows: + remote_config_file.close() # Close the file handle to ensure it's accessible + config["file_mounts"].update( + {"~/ray_bootstrap_config.yaml": remote_config_file.name} + ) + + if "ssh_private_key" in config["auth"]: + config["file_mounts"].update( + { + remote_key_path: config["auth"]["ssh_private_key"], + } + ) + + return config, remote_config_file + + +def attach_cluster( + config_file: str, + start: bool, + use_screen: bool, + use_tmux: bool, + override_cluster_name: Optional[str], + no_config_cache: bool = False, + new: bool = False, + port_forward: Optional[Port_forward] = None, +) -> None: + """Attaches to a screen for the specified cluster. + + Arguments: + config_file: path to the cluster yaml + start: whether to start the cluster if it isn't up + use_screen: whether to use screen as multiplexer + use_tmux: whether to use tmux as multiplexer + override_cluster_name: set the name of the cluster + new: whether to force a new screen + port_forward ( (int,int) or list[(int,int)] ): port(s) to forward + """ + + if use_tmux: + if new: + cmd = "tmux new" + else: + cmd = "tmux attach || tmux new" + elif use_screen: + if new: + cmd = "screen -L" + else: + cmd = "screen -L -xRR" + else: + if new: + raise ValueError("--new only makes sense if passing --screen or --tmux") + cmd = "$SHELL" + + exec_cluster( + config_file, + cmd=cmd, + run_env="auto", + screen=False, + tmux=False, + stop=False, + start=start, + override_cluster_name=override_cluster_name, + no_config_cache=no_config_cache, + port_forward=port_forward, + _allow_uninitialized_state=True, + ) + + +def exec_cluster( + config_file: str, + *, + cmd: Optional[str] = None, + run_env: str = "auto", + screen: bool = False, + tmux: bool = False, + stop: bool = False, + start: bool = False, + override_cluster_name: Optional[str] = None, + no_config_cache: bool = False, + port_forward: Optional[Port_forward] = None, + with_output: bool = False, + _allow_uninitialized_state: bool = False, + extra_screen_args: Optional[str] = None, +) -> str: + """Runs a command on the specified cluster. + + Arguments: + config_file: path to the cluster yaml + cmd: command to run + run_env: whether to run the command on the host or in a container. + Select between "auto", "host" and "docker" + screen: whether to run in a screen + extra_screen_args: optional custom additional args to screen command + tmux: whether to run in a tmux session + stop: whether to stop the cluster after command run + start: whether to start the cluster if it isn't up + override_cluster_name: set the name of the cluster + port_forward ( (int, int) or list[(int, int)] ): port(s) to forward + _allow_uninitialized_state: whether to execute on an uninitialized head + node. + """ + assert not (screen and tmux), "Can specify only one of `screen` or `tmux`." + assert run_env in RUN_ENV_TYPES, "--run_env must be in {}".format(RUN_ENV_TYPES) + # TODO(rliaw): We default this to True to maintain backwards-compat. + # In the future we would want to support disabling login-shells + # and interactivity. + cmd_output_util.set_allow_interactive(True) + + config = yaml.safe_load(open(config_file).read()) + if override_cluster_name is not None: + config["cluster_name"] = override_cluster_name + config = _bootstrap_config(config, no_config_cache=no_config_cache) + + head_node = _get_running_head_node( + config, + config_file, + override_cluster_name, + create_if_needed=start, + _allow_uninitialized_state=_allow_uninitialized_state, + ) + + provider = _get_node_provider(config["provider"], config["cluster_name"]) + updater = NodeUpdaterThread( + node_id=head_node, + provider_config=config["provider"], + provider=provider, + auth_config=config["auth"], + cluster_name=config["cluster_name"], + file_mounts=config["file_mounts"], + initialization_commands=[], + setup_commands=[], + ray_start_commands=[], + runtime_hash="", + file_mounts_contents_hash="", + is_head_node=True, + rsync_options={ + "rsync_exclude": config.get("rsync_exclude"), + "rsync_filter": config.get("rsync_filter"), + }, + docker_config=config.get("docker"), + ) + if cmd and stop: + cmd = "; ".join( + [ + cmd, + "ray stop", + "ray teardown ~/ray_bootstrap_config.yaml --yes --workers-only", + "sudo shutdown -h now", + ] + ) + + result = _exec( + updater, + cmd, + screen, + tmux, + port_forward=port_forward, + with_output=with_output, + run_env=run_env, + shutdown_after_run=False, + extra_screen_args=extra_screen_args, + ) + if tmux or screen: + attach_command_parts = ["ray attach", config_file] + if override_cluster_name is not None: + attach_command_parts.append( + "--cluster-name={}".format(override_cluster_name) + ) + if tmux: + attach_command_parts.append("--tmux") + elif screen: + attach_command_parts.append("--screen") + + attach_command = " ".join(attach_command_parts) + cli_logger.print("Run `{}` to check command status.", cf.bold(attach_command)) + return result + + +def _exec( + updater: NodeUpdaterThread, + cmd: Optional[str] = None, + screen: bool = False, + tmux: bool = False, + port_forward: Optional[Port_forward] = None, + with_output: bool = False, + run_env: str = "auto", + shutdown_after_run: bool = False, + extra_screen_args: Optional[str] = None, +) -> str: + if cmd: + if screen: + wrapped_cmd = [ + "screen", + "-L", + "-dm", + ] + + if extra_screen_args is not None and len(extra_screen_args) > 0: + wrapped_cmd += [extra_screen_args] + + wrapped_cmd += [ + "bash", + "-c", + quote(cmd + "; exec bash"), + ] + cmd = " ".join(wrapped_cmd) + elif tmux: + # TODO: Consider providing named session functionality + wrapped_cmd = [ + "tmux", + "new", + "-d", + "bash", + "-c", + quote(cmd + "; exec bash"), + ] + cmd = " ".join(wrapped_cmd) + return updater.cmd_runner.run( + cmd, + exit_on_fail=True, + port_forward=port_forward, + with_output=with_output, + run_env=run_env, + shutdown_after_run=shutdown_after_run, + ) + + +def rsync( + config_file: str, + source: Optional[str], + target: Optional[str], + override_cluster_name: Optional[str], + down: bool, + ip_address: Optional[str] = None, + use_internal_ip: bool = False, + no_config_cache: bool = False, + all_nodes: bool = False, + should_bootstrap: bool = True, + _runner: ModuleType = subprocess, +) -> None: + """Rsyncs files. + + Arguments: + config_file: path to the cluster yaml + source: source dir + target: target dir + override_cluster_name: set the name of the cluster + down: whether we're syncing remote -> local + ip_address: Address of node. Raise Exception + if both ip_address and 'all_nodes' are provided. + use_internal_ip: Whether the provided ip_address is + public or private. + all_nodes: whether to sync worker nodes in addition to the head node + should_bootstrap: whether to bootstrap cluster config before syncing + """ + if bool(source) != bool(target): + cli_logger.abort("Expected either both a source and a target, or neither.") + + assert bool(source) == bool( + target + ), "Must either provide both or neither source and target." + + if ip_address and all_nodes: + cli_logger.abort("Cannot provide both ip_address and 'all_nodes'.") + + config = yaml.safe_load(open(config_file).read()) + if override_cluster_name is not None: + config["cluster_name"] = override_cluster_name + if should_bootstrap: + config = _bootstrap_config(config, no_config_cache=no_config_cache) + + is_file_mount = False + if source and target: + for remote_mount in config.get("file_mounts", {}).keys(): + if (source if down else target).startswith(remote_mount): + is_file_mount = True + break + + provider = _get_node_provider(config["provider"], config["cluster_name"]) + + def rsync_to_node(node_id, is_head_node): + updater = NodeUpdaterThread( + node_id=node_id, + provider_config=config["provider"], + provider=provider, + auth_config=config["auth"], + cluster_name=config["cluster_name"], + file_mounts=config["file_mounts"], + initialization_commands=[], + setup_commands=[], + ray_start_commands=[], + runtime_hash="", + use_internal_ip=use_internal_ip, + process_runner=_runner, + file_mounts_contents_hash="", + is_head_node=is_head_node, + rsync_options={ + "rsync_exclude": config.get("rsync_exclude"), + "rsync_filter": config.get("rsync_filter"), + }, + docker_config=config.get("docker"), + ) + if down: + rsync = updater.rsync_down + else: + rsync = updater.rsync_up + + if source and target: + # print rsync progress for single file rsync + if cli_logger.verbosity > 0: + cmd_output_util.set_output_redirected(False) + set_rsync_silent(False) + rsync(source, target, is_file_mount) + else: + updater.sync_file_mounts(rsync) + + nodes = [] + head_node = _get_running_head_node( + config, config_file, override_cluster_name, create_if_needed=False + ) + if ip_address: + nodes = [provider.get_node_id(ip_address, use_internal_ip=use_internal_ip)] + else: + nodes = [head_node] + if all_nodes: + nodes.extend(_get_worker_nodes(config, override_cluster_name)) + + for node_id in nodes: + rsync_to_node(node_id, is_head_node=(node_id == head_node)) + + +def get_head_node_ip( + config_file: str, override_cluster_name: Optional[str] = None +) -> str: + """Returns head node IP for given configuration file if exists.""" + + config = yaml.safe_load(open(config_file).read()) + if override_cluster_name is not None: + config["cluster_name"] = override_cluster_name + + provider = _get_node_provider(config["provider"], config["cluster_name"]) + head_node = _get_running_head_node(config, config_file, override_cluster_name) + provider_cfg = config.get("provider", {}) + # Get internal IP if using internal IPs and + # use_external_head_ip is not specified + if provider_cfg.get("use_internal_ips", False) and not provider_cfg.get( + "use_external_head_ip", False + ): + head_node_ip = provider.internal_ip(head_node) + else: + head_node_ip = provider.external_ip(head_node) + + return head_node_ip + + +def get_worker_node_ips( + config_file: str, override_cluster_name: Optional[str] = None +) -> List[str]: + """Returns worker node IPs for given configuration file.""" + + config = yaml.safe_load(open(config_file).read()) + if override_cluster_name is not None: + config["cluster_name"] = override_cluster_name + + provider = _get_node_provider(config["provider"], config["cluster_name"]) + nodes = provider.non_terminated_nodes({TAG_RAY_NODE_KIND: NODE_KIND_WORKER}) + + if config.get("provider", {}).get("use_internal_ips", False): + return [provider.internal_ip(node) for node in nodes] + else: + return [provider.external_ip(node) for node in nodes] + + +def _get_worker_nodes( + config: Dict[str, Any], override_cluster_name: Optional[str] +) -> List[str]: + """Returns worker node ids for given configuration.""" + # todo: technically could be reused in get_worker_node_ips + if override_cluster_name is not None: + config["cluster_name"] = override_cluster_name + + provider = _get_node_provider(config["provider"], config["cluster_name"]) + return provider.non_terminated_nodes({TAG_RAY_NODE_KIND: NODE_KIND_WORKER}) + + +def _get_running_head_node( + config: Dict[str, Any], + printable_config_file: str, + override_cluster_name: Optional[str], + create_if_needed: bool = False, + _provider: Optional[NodeProvider] = None, + _allow_uninitialized_state: bool = False, +) -> str: + """Get a valid, running head node. + Args: + config (Dict[str, Any]): Cluster Config dictionary + printable_config_file: Used for printing formatted CLI commands. + override_cluster_name: Passed to `get_or_create_head_node` to + override the cluster name present in `config`. + create_if_needed: Create a head node if one is not present. + _provider: [For testing], a Node Provider to use. + _allow_uninitialized_state: Whether to return a head node that + is not 'UP TO DATE'. This is used to allow `ray attach` and + `ray exec` to debug a cluster in a bad state. + + """ + provider = _provider or _get_node_provider( + config["provider"], config["cluster_name"] + ) + head_node_tags = { + TAG_RAY_NODE_KIND: NODE_KIND_HEAD, + } + nodes = provider.non_terminated_nodes(head_node_tags) + head_node = None + _backup_head_node = None + for node in nodes: + node_state = provider.node_tags(node).get(TAG_RAY_NODE_STATUS) + if node_state == STATUS_UP_TO_DATE: + head_node = node + else: + _backup_head_node = node + cli_logger.warning(f"Head node ({node}) is in state {node_state}.") + + if head_node is not None: + return head_node + elif create_if_needed: + get_or_create_head_node( + config, + printable_config_file=printable_config_file, + restart_only=False, + no_restart=False, + yes=True, + override_cluster_name=override_cluster_name, + ) + # NOTE: `_allow_uninitialized_state` is forced to False if + # `create_if_needed` is set to True. This is to ensure that the + # commands executed after creation occur on an actually running + # cluster. + return _get_running_head_node( + config, + printable_config_file, + override_cluster_name, + create_if_needed=False, + _allow_uninitialized_state=False, + ) + else: + if _allow_uninitialized_state and _backup_head_node is not None: + cli_logger.warning( + f"The head node being returned: {_backup_head_node} is not " + "`up-to-date`. If you are not debugging a startup issue " + "it is recommended to restart this head node with: {}", + cf.bold(f" ray down {printable_config_file}"), + ) + + return _backup_head_node + raise RuntimeError( + "Head node of cluster ({}) not found!".format(config["cluster_name"]) + ) + + +def get_local_dump_archive( + stream: bool = False, + output: Optional[str] = None, + logs: bool = True, + debug_state: bool = True, + pip: bool = True, + processes: bool = True, + processes_verbose: bool = False, + tempfile: Optional[str] = None, +) -> Optional[str]: + if stream and output: + raise ValueError( + "You can only use either `--output` or `--stream`, but not both." + ) + + parameters = GetParameters( + logs=logs, + debug_state=debug_state, + pip=pip, + processes=processes, + processes_verbose=processes_verbose, + ) + + with Archive(file=tempfile) as archive: + get_all_local_data(archive, parameters) + + tmp = archive.file + + if stream: + with open(tmp, "rb") as fp: + os.write(1, fp.read()) + os.remove(tmp) + return None + + target = output or os.path.join(os.getcwd(), os.path.basename(tmp)) + shutil.move(tmp, target) + cli_logger.print(f"Created local data archive at {target}") + + return target + + +def get_cluster_dump_archive( + cluster_config_file: Optional[str] = None, + host: Optional[str] = None, + ssh_user: Optional[str] = None, + ssh_key: Optional[str] = None, + docker: Optional[str] = None, + local: Optional[bool] = None, + output: Optional[str] = None, + logs: bool = True, + debug_state: bool = True, + pip: bool = True, + processes: bool = True, + processes_verbose: bool = False, + tempfile: Optional[str] = None, +) -> Optional[str]: + # Inform the user what kind of logs are collected (before actually + # collecting, so they can abort) + content_str = "" + if logs: + content_str += ( + " - The logfiles of your Ray session\n" + " This usually includes Python outputs (stdout/stderr)\n" + ) + + if debug_state: + content_str += ( + " - Debug state information on your Ray cluster \n" + " e.g. number of workers, drivers, objects, etc.\n" + ) + + if pip: + content_str += " - Your installed Python packages (`pip freeze`)\n" + + if processes: + content_str += ( + " - Information on your running Ray processes\n" + " This includes command line arguments\n" + ) + + cli_logger.warning( + "You are about to create a cluster dump. This will collect data from " + "cluster nodes.\n\n" + "The dump will contain this information:\n\n" + f"{content_str}\n" + f"If you are concerned about leaking private information, extract " + f"the archive and inspect its contents before sharing it with " + f"anyone." + ) + + # Parse arguments (e.g. fetch info from cluster config) + ( + cluster_config_file, + hosts, + ssh_user, + ssh_key, + docker, + cluster_name, + ) = _info_from_params(cluster_config_file, host, ssh_user, ssh_key, docker) + + nodes = [ + Node(host=h, ssh_user=ssh_user, ssh_key=ssh_key, docker_container=docker) + for h in hosts + ] + + if not nodes: + cli_logger.error( + "No nodes found. Specify with `--host` or by passing a ray " + "cluster config to `--cluster`." + ) + return None + + if cluster_config_file: + nodes[0].is_head = True + + if local is None: + # If called with a cluster config, this was probably started + # from a laptop + local = not bool(cluster_config_file) + + parameters = GetParameters( + logs=logs, + debug_state=debug_state, + pip=pip, + processes=processes, + processes_verbose=processes_verbose, + ) + + with Archive(file=tempfile) as archive: + if local: + create_archive_for_local_and_remote_nodes( + archive, remote_nodes=nodes, parameters=parameters + ) + else: + create_archive_for_remote_nodes( + archive, remote_nodes=nodes, parameters=parameters + ) + + if not output: + if cluster_name: + filename = ( + f"{cluster_name}_" f"{datetime.datetime.now():%Y-%m-%d_%H-%M-%S}.tar.gz" + ) + else: + filename = ( + f"collected_logs_" f"{datetime.datetime.now():%Y-%m-%d_%H-%M-%S}.tar.gz" + ) + output = os.path.join(os.getcwd(), filename) + else: + output = os.path.expanduser(output) + + shutil.move(archive.file, output) + return output + + +def confirm(msg: str, yes: bool) -> Optional[bool]: + return None if yes else click.confirm(msg, abort=True) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/constants.py b/lib/python3.12/site-packages/ray/autoscaler/_private/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..ca1005e88db20916211684158abd71c9bb70f174 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/constants.py @@ -0,0 +1,144 @@ +import os +import sys + +from ray._private.ray_constants import ( # noqa F401 + AUTOSCALER_RESOURCE_REQUEST_CHANNEL, + DEFAULT_OBJECT_STORE_MEMORY_PROPORTION, + LABELS_ENVIRONMENT_VARIABLE, + LOGGER_FORMAT, + RESOURCES_ENVIRONMENT_VARIABLE, +) + + +def env_integer(key, default): + if key in os.environ: + val = os.environ[key] + if val == "inf": + return sys.maxsize + else: + return int(val) + return default + + +# Whether autoscaler cluster status logging is enabled. Set to 0 disable. +AUTOSCALER_STATUS_LOG = env_integer("RAY_ENABLE_CLUSTER_STATUS_LOG", 1) + +# The name of the environment variable for plugging in a utilization scorer. +AUTOSCALER_UTILIZATION_SCORER_KEY = "RAY_AUTOSCALER_UTILIZATION_SCORER" + +# Whether to avoid launching GPU nodes for CPU only tasks. +AUTOSCALER_CONSERVE_GPU_NODES = env_integer("AUTOSCALER_CONSERVE_GPU_NODES", 1) + +# How long to wait for a node to start and terminate, in seconds. +AUTOSCALER_NODE_START_WAIT_S = env_integer("AUTOSCALER_NODE_START_WAIT_S", 900) +AUTOSCALER_NODE_TERMINATE_WAIT_S = env_integer("AUTOSCALER_NODE_TERMINATE_WAIT_S", 900) + +# Interval at which to check if node SSH became available. +AUTOSCALER_NODE_SSH_INTERVAL_S = env_integer("AUTOSCALER_NODE_SSH_INTERVAL_S", 5) + +# Abort autoscaling if more than this number of errors are encountered. This +# is a safety feature to prevent e.g. runaway node launches. +AUTOSCALER_MAX_NUM_FAILURES = env_integer("AUTOSCALER_MAX_NUM_FAILURES", 5) + +# The maximum number of nodes to launch in a single request. +# Multiple requests may be made for this batch size, up to +# the limit of AUTOSCALER_MAX_CONCURRENT_LAUNCHES. +AUTOSCALER_MAX_LAUNCH_BATCH = env_integer("AUTOSCALER_MAX_LAUNCH_BATCH", 5) + +# Max number of nodes to launch at a time. +AUTOSCALER_MAX_CONCURRENT_LAUNCHES = env_integer( + "AUTOSCALER_MAX_CONCURRENT_LAUNCHES", 10 +) + +# Default upscaling speed for the autoscaler. This specifies how many nodes +# to request at a time, where the desired number to upscale is +# min(1, upscaling_speed * current_num_nodes) +# e.g. 1.0 means to request enough nodes to double +# the cluster size in each round of requests. +# When the upscaling speed is 0.0, the autoscaler will request 1 node. +DEFAULT_UPSCALING_SPEED = 0.0 + +# Interval at which to perform autoscaling updates. +AUTOSCALER_UPDATE_INTERVAL_S = env_integer("AUTOSCALER_UPDATE_INTERVAL_S", 5) + +# The autoscaler will attempt to restart Ray on nodes it hasn't heard from +# in more than this interval. +AUTOSCALER_HEARTBEAT_TIMEOUT_S = env_integer("AUTOSCALER_HEARTBEAT_TIMEOUT_S", 30) +# The maximum number of nodes (including failed nodes) that the autoscaler will +# track for logging purposes. +AUTOSCALER_MAX_NODES_TRACKED = 1500 + +AUTOSCALER_MAX_FAILURES_DISPLAYED = 20 + +AUTOSCALER_NODE_AVAILABILITY_MAX_STALENESS_S = env_integer( + "AUTOSCALER_NODE_AVAILABILITY_MAX_STALENESS_S", 30 * 60 +) + +AUTOSCALER_REPORT_PER_NODE_STATUS = ( + env_integer("AUTOSCALER_REPORT_PER_NODE_STATUS", 1) == 1 +) + +# The maximum allowed resource demand vector size to guarantee the resource +# demand scheduler bin packing algorithm takes a reasonable amount of time +# to run. +AUTOSCALER_MAX_RESOURCE_DEMAND_VECTOR_SIZE = env_integer( + "AUTOSCALER_MAX_RESOURCE_DEMAND_VECTOR_SIZE", 1000 +) + +# Port that autoscaler prometheus metrics will be exported to +AUTOSCALER_METRIC_PORT = env_integer("AUTOSCALER_METRIC_PORT", 44217) + +# The minimum number of nodes to launch concurrently. +AUTOSCALER_UPSCALING_INITIAL_NUM_NODES = 5 + +# Max number of retries to AWS (default is 5, time increases exponentially) +BOTO_MAX_RETRIES = env_integer("BOTO_MAX_RETRIES", 12) +# Max number of retries to create an EC2 node (retry different subnet) +BOTO_CREATE_MAX_RETRIES = env_integer("BOTO_CREATE_MAX_RETRIES", 5) + +# ray home path in the container image +RAY_HOME = "/home/ray" + +# The order of this list matters! `scripts.py` kills the ray processes in order of this +# list. Think twice when you add to this list. +# Invariants: +# RAYLET must be the first in the list. +# GCS SERVER must be the last in the list. +RAY_PROCESSES = [ + # The first element is the substring to filter. + # The second element, if True, is to filter ps results by command name + # (only the first 15 charactors of the executable name on Linux); + # if False, is to filter ps results by command with all its arguments. + # See STANDARD FORMAT SPECIFIERS section of + # http://man7.org/linux/man-pages/man1/ps.1.html + # about comm and args. This can help avoid killing non-ray processes. + # Format: + # Keyword to filter, filter by command (True)/filter by args (False) + ["raylet", True], + ["plasma_store", True], + ["monitor.py", False], + ["ray.util.client.server", False], + ["default_worker.py", False], # Python worker. + ["setup_worker.py", False], # Python environment setup worker. + # For mac osx, setproctitle doesn't change the process name returned + # by psutil but only cmdline. + [ + "ray::", + sys.platform != "darwin", + ], # Python worker. TODO(mehrdadn): Fix for Windows + ["io.ray.runtime.runner.worker.DefaultWorker", False], # Java worker. + ["log_monitor.py", False], + [os.path.join("dashboard", "agent.py"), False], + [os.path.join("dashboard", "dashboard.py"), False], + [os.path.join("runtime_env", "agent", "main.py"), False], + ["ray_process_reaper.py", False], + ["gcs_server", True], +] + +# Max Concurrent SSH Calls to stop Docker +MAX_PARALLEL_SHUTDOWN_WORKERS = env_integer("MAX_PARALLEL_SHUTDOWN_WORKERS", 50) + +DISABLE_NODE_UPDATERS_KEY = "disable_node_updaters" +DISABLE_LAUNCH_CONFIG_CHECK_KEY = "disable_launch_config_check" +FOREGROUND_NODE_LAUNCH_KEY = "foreground_node_launch" +WORKER_LIVENESS_CHECK_KEY = "worker_liveness_check" diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/docker.py b/lib/python3.12/site-packages/ray/autoscaler/_private/docker.py new file mode 100644 index 0000000000000000000000000000000000000000..cf1464d9becb771bf5d3422f7e1e2c8483b7f462 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/docker.py @@ -0,0 +1,130 @@ +from pathlib import Path +from typing import Any, Dict + +from ray.autoscaler._private.cli_logger import cli_logger + +try: # py3 + from shlex import quote +except ImportError: # py2 + from pipes import quote + + +def _check_docker_file_mounts(file_mounts: Dict[str, str]) -> None: + """Checks if files are passed as file_mounts. This is a problem for Docker + based clusters because when a file is bind-mounted in Docker, updates to + the file on the host do not always propagate to the container. Using + directories is recommended. + """ + for remote, local in file_mounts.items(): + if Path(local).is_file(): + cli_logger.warning( + f"File Mount: ({remote}:{local}) refers to a file.\n To ensure" + " this mount updates properly, please use a directory." + ) + + +def validate_docker_config(config: Dict[str, Any]) -> None: + """Checks whether the Docker configuration is valid.""" + if "docker" not in config: + return + + _check_docker_file_mounts(config.get("file_mounts", {})) + + docker_image = config["docker"].get("image") + cname = config["docker"].get("container_name") + + head_docker_image = config["docker"].get("head_image", docker_image) + + worker_docker_image = config["docker"].get("worker_image", docker_image) + + image_present = docker_image or (head_docker_image and worker_docker_image) + if (not cname) and (not image_present): + return + else: + assert cname and image_present, "Must provide a container & image name" + + return None + + +def with_docker_exec( + cmds, container_name, docker_cmd, env_vars=None, with_interactive=False +): + assert docker_cmd, "Must provide docker command" + env_str = "" + if env_vars: + env_str = " ".join(["-e {env}=${env}".format(env=env) for env in env_vars]) + return [ + "{docker_cmd} exec {interactive} {env} {container} /bin/bash -c {cmd} ".format( + docker_cmd=docker_cmd, + interactive="-it" if with_interactive else "", + env=env_str, + container=container_name, + cmd=quote(cmd), + ) + for cmd in cmds + ] + + +def _check_helper(cname, template, docker_cmd): + return " ".join( + [docker_cmd, "inspect", "-f", "'{{" + template + "}}'", cname, "||", "true"] + ) + + +def check_docker_running_cmd(cname, docker_cmd): + return _check_helper(cname, ".State.Running", docker_cmd) + + +def check_bind_mounts_cmd(cname, docker_cmd): + return _check_helper(cname, "json .Mounts", docker_cmd) + + +def check_docker_image(cname, docker_cmd): + return _check_helper(cname, ".Config.Image", docker_cmd) + + +def docker_start_cmds( + user, + image, + mount_dict, + container_name, + user_options, + cluster_name, + home_directory, + docker_cmd, +): + # Imported here due to circular dependency. + from ray.autoscaler.sdk import get_docker_host_mount_location + + docker_mount_prefix = get_docker_host_mount_location(cluster_name) + mount = {f"{docker_mount_prefix}/{dst}": dst for dst in mount_dict} + + mount_flags = " ".join( + [ + "-v {src}:{dest}".format(src=k, dest=v.replace("~/", home_directory + "/")) + for k, v in mount.items() + ] + ) + + # for click, used in ray cli + env_vars = {"LC_ALL": "C.UTF-8", "LANG": "C.UTF-8"} + env_flags = " ".join( + ["-e {name}={val}".format(name=k, val=v) for k, v in env_vars.items()] + ) + + user_options_str = " ".join(user_options) + docker_run = [ + docker_cmd, + "run", + "--rm", + "--name {}".format(container_name), + "-d", + "-it", + mount_flags, + env_flags, + user_options_str, + "--net=host", + image, + "bash", + ] + return " ".join(docker_run) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/event_summarizer.py b/lib/python3.12/site-packages/ray/autoscaler/_private/event_summarizer.py new file mode 100644 index 0000000000000000000000000000000000000000..2f8d3494d191b7b5908b84ccf3481e273ec4e129 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/event_summarizer.py @@ -0,0 +1,75 @@ +import time +from threading import RLock +from typing import Any, Callable, Dict, List + + +class EventSummarizer: + """Utility that aggregates related log messages to reduce log spam.""" + + def __init__(self): + self.events_by_key: Dict[str, int] = {} + # Messages to send in next summary batch. + self.messages_to_send: List[str] = [] + # Tracks TTL of messages. A message will not be re-sent once it is + # added here, until its TTL expires. + self.throttled_messages: Dict[str, float] = {} + + # Event summarizer is used by the main thread and + # by node launcher child threads. + self.lock = RLock() + + def add( + self, template: str, *, quantity: Any, aggregate: Callable[[Any, Any], Any] + ) -> None: + """Add a log message, which will be combined by template. + + Args: + template: Format string with one placeholder for quantity. + quantity: Quantity to aggregate. + aggregate: Aggregation function used to combine the + quantities. The result is inserted into the template to + produce the final log message. + """ + with self.lock: + # Enforce proper sentence structure. + if not template.endswith("."): + template += "." + if template in self.events_by_key: + self.events_by_key[template] = aggregate( + self.events_by_key[template], quantity + ) + else: + self.events_by_key[template] = quantity + + def add_once_per_interval(self, message: str, key: str, interval_s: int): + """Add a log message, which is throttled once per interval by a key. + + Args: + message: The message to log. + key: The key to use to deduplicate the message. + interval_s: Throttling interval in seconds. + """ + with self.lock: + if key not in self.throttled_messages: + self.throttled_messages[key] = time.time() + interval_s + self.messages_to_send.append(message) + + def summary(self) -> List[str]: + """Generate the aggregated log summary of all added events.""" + with self.lock: + out = [] + for template, quantity in self.events_by_key.items(): + out.append(template.format(quantity)) + out.extend(self.messages_to_send) + return out + + def clear(self) -> None: + """Clear the events added.""" + with self.lock: + self.events_by_key.clear() + self.messages_to_send.clear() + # Expire any messages that have reached their TTL. This allows them + # to be sent again. + for k, t in list(self.throttled_messages.items()): + if time.time() > t: + del self.throttled_messages[k] diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/event_system.py b/lib/python3.12/site-packages/ray/autoscaler/_private/event_system.py new file mode 100644 index 0000000000000000000000000000000000000000..db4bfc1ce101e3558a4996ef7fb94e64aee1fbb9 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/event_system.py @@ -0,0 +1,106 @@ +from enum import Enum, auto +from typing import Any, Callable, Dict, List, Optional, Union + +from ray.autoscaler._private.cli_logger import cli_logger + + +class CreateClusterEvent(Enum): + """Events to track in ray.autoscaler.sdk.create_or_update_cluster. + + Attributes: + up_started : Invoked at the beginning of create_or_update_cluster. + ssh_keypair_downloaded : Invoked when the ssh keypair is downloaded. + cluster_booting_started : Invoked when when the cluster booting starts. + acquiring_new_head_node : Invoked before the head node is acquired. + head_node_acquired : Invoked after the head node is acquired. + ssh_control_acquired : Invoked when the node is being updated. + run_initialization_cmd : Invoked before all initialization + commands are called and again before each initialization command. + run_setup_cmd : Invoked before all setup commands are + called and again before each setup command. + start_ray_runtime : Invoked before ray start commands are run. + start_ray_runtime_completed : Invoked after ray start commands + are run. + cluster_booting_completed : Invoked after cluster booting + is completed. + """ + + up_started = auto() + ssh_keypair_downloaded = auto() + cluster_booting_started = auto() + acquiring_new_head_node = auto() + head_node_acquired = auto() + ssh_control_acquired = auto() + run_initialization_cmd = auto() + run_setup_cmd = auto() + start_ray_runtime = auto() + start_ray_runtime_completed = auto() + cluster_booting_completed = auto() + + +class _EventSystem: + """Event system that handles storing and calling callbacks for events. + + Attributes: + callback_map (Dict[str, List[Callable]]) : Stores list of callbacks + for events when registered. + """ + + def __init__(self): + self.callback_map = {} + + def add_callback_handler( + self, + event: str, + callback: Union[Callable[[Dict], None], List[Callable[[Dict], None]]], + ): + """Stores callback handler for event. + + Args: + event: Event that callback should be called on. See + CreateClusterEvent for details on the events available to be + registered against. + callback (Callable[[Dict], None]): Callable object that is invoked + when specified event occurs. + """ + if event not in CreateClusterEvent.__members__.values(): + cli_logger.warning( + f"{event} is not currently tracked, and this" + " callback will not be invoked." + ) + + self.callback_map.setdefault(event, []).extend( + [callback] if type(callback) is not list else callback + ) + + def execute_callback( + self, event: CreateClusterEvent, event_data: Optional[Dict[str, Any]] = None + ): + """Executes all callbacks for event. + + Args: + event: Event that is invoked. See CreateClusterEvent + for details on the available events. + event_data (Dict[str, Any]): Argument that is passed to each + callable object stored for this particular event. + """ + if event_data is None: + event_data = {} + + event_data["event_name"] = event + if event in self.callback_map: + for callback in self.callback_map[event]: + callback(event_data) + + def clear_callbacks_for_event(self, event: str): + """Clears stored callable objects for event. + + Args: + event: Event that has callable objects stored in map. + See CreateClusterEvent for details on the available events. + """ + if event in self.callback_map: + del self.callback_map[event] + + +global_event_system = _EventSystem() diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..473caff245e0090a7e400f1fc9b0b09a7992ea05 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/command_runner.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/command_runner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0cc98b144f51f85d900bf3e5cf69472b6a55a12 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/command_runner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/docker_monitor.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/docker_monitor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26b0d680eb7ebf23f0a27635b2bc9a849a23adb3 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/docker_monitor.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c20fc7fac413ed6ba694e24cd5552f1136a7f5ff Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/test_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/test_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83d294a868457880ee61a6b05da6bad7f9519aba Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/__pycache__/test_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/command_runner.py b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/command_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..e1444eef7ee1b1196de9c2e6b6a2699b4a60eb9c --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/command_runner.py @@ -0,0 +1,91 @@ +import os +import subprocess +from typing import Dict, List, Tuple + +from ray.autoscaler._private.docker import with_docker_exec +from ray.autoscaler.command_runner import CommandRunnerInterface + + +class FakeDockerCommandRunner(CommandRunnerInterface): + """Command runner for the fke docker multinode cluster. + + This command runner uses ``docker exec`` and ``docker cp`` to + run commands and copy files, respectively. + + The regular ``DockerCommandRunner`` is made for use in SSH settings + where Docker runs on a remote hose. In contrast, this command runner + does not wrap the docker commands in ssh calls. + """ + + def __init__(self, docker_config, **common_args): + self.container_name = docker_config["container_name"] + self.docker_config = docker_config + self.home_dir = None + self.initialized = False + # Optionally use 'podman' instead of 'docker' + use_podman = docker_config.get("use_podman", False) + self.docker_cmd = "podman" if use_podman else "docker" + + def _run_shell(self, cmd: str, timeout: int = 120) -> str: + return subprocess.check_output( + cmd, shell=True, timeout=timeout, encoding="utf-8" + ) + + def run( + self, + cmd: str = None, + timeout: int = 120, + exit_on_fail: bool = False, + port_forward: List[Tuple[int, int]] = None, + with_output: bool = False, + environment_variables: Dict[str, object] = None, + run_env: str = "auto", + ssh_options_override_ssh_key: str = "", + shutdown_after_run: bool = False, + ) -> str: + prefix = with_docker_exec( + [cmd], + container_name=self.container_name, + with_interactive=False, + docker_cmd=self.docker_cmd, + )[0] + return self._run_shell(prefix) + + def run_init( + self, *, as_head: bool, file_mounts: Dict[str, str], sync_run_yet: bool + ): + pass + + def remote_shell_command_str(self): + return "{} exec -it {} bash".format(self.docker_cmd, self.container_name) + + def run_rsync_down(self, source, target, options=None): + docker_dir = os.path.dirname(self._docker_expand_user(source)) + + self._run_shell(f"docker cp {self.container_name}:{docker_dir} {target}") + + def run_rsync_up(self, source, target, options=None): + docker_dir = os.path.dirname(self._docker_expand_user(target)) + self.run(cmd=f"mkdir -p {docker_dir}") + + self._run_shell(f"docker cp {source} {self.container_name}:{docker_dir}") + + def _docker_expand_user(self, string, any_char=False): + user_pos = string.find("~") + if user_pos > -1: + if self.home_dir is None: + self.home_dir = self._run_shell( + with_docker_exec( + ["printenv HOME"], + container_name=self.container_name, + docker_cmd=self.docker_cmd, + ) + ).strip() + + if any_char: + return string.replace("~/", self.home_dir + "/") + + elif not any_char and user_pos == 0: + return string.replace("~", self.home_dir, 1) + + return string diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/docker_monitor.py b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/docker_monitor.py new file mode 100644 index 0000000000000000000000000000000000000000..14c5e4646cf4625677259cf82c218de70c5982f6 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/docker_monitor.py @@ -0,0 +1,246 @@ +"""Fake multinode docker monitoring script. + +This script is the "docker compose server" for the fake_multinode +provider using Docker compose. It should be started before running +`RAY_FAKE_CLUSTER=1 ray up `. + +This script reads the volume directory from a supplied fake multinode +docker cluster config file. +It then waits until a docker-compose.yaml file is created in the same +directory, which is done by the `ray up` command. + +It then watches for changes in the docker-compose.yaml file and runs +`docker compose up` whenever changes are detected. This will start docker +containers as requested by the autoscaler. + +Generally, the docker-compose.yaml will be mounted in the head node of the +cluster, which will then continue to change it according to the autoscaler +requirements. + +Additionally, this script monitors the docker container status using +`docker status` and writes it into a `status.json`. This information is +again used by the autoscaler to determine if any nodes have died. +""" +import argparse +import json +import os +import shutil +import subprocess +import time +from typing import Any, Dict, List, Optional + +import yaml + + +def _read_yaml(path: str): + with open(path, "rt") as f: + return yaml.safe_load(f) + + +def _update_docker_compose( + docker_compose_path: str, project_name: str, status: Optional[Dict[str, Any]] +) -> bool: + docker_compose_config = _read_yaml(docker_compose_path) + + if not docker_compose_config: + print("Docker compose currently empty") + return False + + cmd = ["up", "-d"] + if status and len(status) > 0: + cmd += ["--no-recreate"] + + shutdown = False + if not docker_compose_config["services"]: + # If no more nodes, run `down` instead of `up` + print("Shutting down nodes") + cmd = ["down"] + shutdown = True + try: + subprocess.check_call( + ["docker", "compose", "-f", docker_compose_path, "-p", project_name] + + cmd + + [ + "--remove-orphans", + ] + ) + except Exception as e: + print(f"Ran into error when updating docker compose: {e}") + # Ignore error + + return shutdown + + +def _get_ip( + project_name: str, + container_name: str, + override_network: Optional[str] = None, + retry_times: int = 3, +) -> Optional[str]: + network = override_network or f"{project_name}_ray_local" + + cmd = [ + "docker", + "inspect", + "-f", + '"{{ .NetworkSettings.Networks' f".{network}.IPAddress" ' }}"', + f"{container_name}", + ] + for i in range(retry_times): + try: + ip_address = subprocess.check_output(cmd, encoding="utf-8") + except Exception: + time.sleep(1) + else: + return ip_address.strip().strip('"').strip('\\"') + return None + + +def _update_docker_status( + docker_compose_path: str, project_name: str, docker_status_path: str +): + data_str = "" + try: + data_str = ( + subprocess.check_output( + [ + "docker", + "compose", + "-f", + docker_compose_path, + "-p", + project_name, + "ps", + "--format", + "json", + ] + ) + .decode("utf-8") + .strip() + .split("\n") + ) + data: List[Dict[str, str]] = [] + for line in data_str: + line = line.strip() + if line: + data.append(json.loads(line)) + except Exception as e: + print(f"Ran into error when fetching status: {e}") + print(f"docker compose ps output: {data_str}") + return None + + status = {} + for container in data: + node_id = container["Service"] + container_name = container["Name"] + if container["State"] == "running": + ip = _get_ip(project_name, container_name) + else: + ip = "" + container["IP"] = ip + status[node_id] = container + + with open(docker_status_path, "wt") as f: + json.dump(status, f) + + return status + + +def monitor_docker( + docker_compose_path: str, + status_path: str, + project_name: str, + update_interval: float = 1.0, +): + while not os.path.exists(docker_compose_path): + # Wait until cluster is created + time.sleep(0.5) + + print("Docker compose config detected, starting status monitoring") + + # Make sure this is always writeable from inside the containers + os.chmod(docker_compose_path, 0o777) + + docker_config = {"force_update": True} + + # Force update + next_update = time.monotonic() - 1.0 + shutdown = False + status = None + + # Loop: + # If the config changed, update cluster. + # Every `update_interval` seconds, update docker status. + while not shutdown: + new_docker_config = _read_yaml(docker_compose_path) + if new_docker_config != docker_config: + # Update cluster + shutdown = _update_docker_compose(docker_compose_path, project_name, status) + + # Force status update + next_update = time.monotonic() - 1.0 + + if time.monotonic() > next_update: + # Update docker status + status = _update_docker_status( + docker_compose_path, project_name, status_path + ) + next_update = time.monotonic() + update_interval + + docker_config = new_docker_config + time.sleep(0.1) + + print("Cluster shut down, terminating monitoring script.") + + +def start_monitor(config_file: str): + cluster_config = _read_yaml(config_file) + + provider_config = cluster_config["provider"] + assert provider_config["type"] == "fake_multinode_docker", ( + f"The docker monitor only works with providers of type " + f"`fake_multinode_docker`, got `{provider_config['type']}`" + ) + + project_name = provider_config["project_name"] + + volume_dir = provider_config["shared_volume_dir"] + os.makedirs(volume_dir, mode=0o755, exist_ok=True) + + # Create bootstrap config + bootstrap_config_path = os.path.join(volume_dir, "bootstrap_config.yaml") + shutil.copy(config_file, bootstrap_config_path) + + # These two files usually don't exist, yet + docker_compose_config_path = os.path.join(volume_dir, "docker-compose.yaml") + + docker_status_path = os.path.join(volume_dir, "status.json") + + if os.path.exists(docker_compose_config_path): + # We wait until this file exists, so remove it if it exists + # from a previous run. + os.remove(docker_compose_config_path) + + if os.path.exists(docker_status_path): + os.remove(docker_status_path) + # Create empty file so it can be mounted + with open(docker_status_path, "wt") as f: + f.write("{}") + + print( + f"Starting monitor process. Please start Ray cluster with:\n" + f" RAY_FAKE_CLUSTER=1 ray up {config_file}" + ) + monitor_docker(docker_compose_config_path, docker_status_path, project_name) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "config_file", + help="Path to cluster config file containing a fake docker " + "cluster configuration.", + ) + args = parser.parse_args() + + start_monitor(args.config_file) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..270385c37506f2d4244b01863cb79dc5a20192b4 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/node_provider.py @@ -0,0 +1,728 @@ +import copy +import json +import logging +import os +import subprocess +import sys +import time +from threading import RLock +from types import ModuleType +from typing import Any, Dict, Optional + +import yaml + +import ray +import ray._private.ray_constants as ray_constants +from ray._common.network_utils import build_address +from ray.autoscaler._private.fake_multi_node.command_runner import ( + FakeDockerCommandRunner, +) +from ray.autoscaler.command_runner import CommandRunnerInterface +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + NODE_KIND_WORKER, + STATUS_UP_TO_DATE, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_NAME, + TAG_RAY_NODE_STATUS, + TAG_RAY_USER_NODE_TYPE, +) + +logger = logging.getLogger(__name__) + +# We generate the node ids deterministically in the fake node provider, so that +# we can associate launched nodes with their resource reports. IDs increment +# starting with fffff*00000 for the head node, fffff*00001, etc. for workers. +FAKE_HEAD_NODE_ID = "fffffffffffffffffffffffffffffffffffffffffffffffffff00000" +FAKE_HEAD_NODE_TYPE = "ray.head.default" + +FAKE_DOCKER_DEFAULT_GCS_PORT = 16379 +FAKE_DOCKER_DEFAULT_OBJECT_MANAGER_PORT = 18076 +FAKE_DOCKER_DEFAULT_CLIENT_PORT = 10002 + +DOCKER_COMPOSE_SKELETON = { + "services": {}, + "networks": {"ray_local": {}}, +} + +DOCKER_NODE_SKELETON = { + "networks": ["ray_local"], + "mem_limit": "3000m", + "mem_reservation": "3000m", + "shm_size": "1200m", + "volumes": [], +} + +DOCKER_HEAD_CMD = ( + 'bash -c "' + "sudo mkdir -p {volume_dir} && " + "sudo chmod 777 {volume_dir} && " + "touch {volume_dir}/.in_docker && " + "sudo chown -R ray:users /cluster/node && " + "sudo chmod -R 777 /cluster/node && " + "sudo chown -R ray:users /cluster/shared && " + "sudo chmod -R 777 /cluster/shared && " + "sudo chmod 700 ~/.ssh && " + "sudo chmod 600 ~/.ssh/authorized_keys && " + "sudo chmod 600 ~/ray_bootstrap_key.pem && " + "sudo chown ray:users " + "~/.ssh ~/.ssh/authorized_keys ~/ray_bootstrap_key.pem && " + "{ensure_ssh} && " + "sleep 1 && " + "RAY_FAKE_CLUSTER=1 ray start --head " + "--autoscaling-config=~/ray_bootstrap_config.yaml " + "--object-manager-port=8076 " + "--num-cpus {num_cpus} " + "--num-gpus {num_gpus} " + # "--resources='{resources}' " + '--block"' +) + +DOCKER_WORKER_CMD = ( + 'bash -c "' + "sudo mkdir -p {volume_dir} && " + "sudo chmod 777 {volume_dir} && " + "touch {volume_dir}/.in_docker && " + "sudo chown -R ray:users /cluster/node && " + "sudo chmod -R 777 /cluster/node && " + "sudo chmod 700 ~/.ssh && " + "sudo chmod 600 ~/.ssh/authorized_keys && " + "sudo chown ray:users ~/.ssh ~/.ssh/authorized_keys && " + "{ensure_ssh} && " + "sleep 1 && " + f"ray start --address={FAKE_HEAD_NODE_ID}:6379 " + "--object-manager-port=8076 " + "--num-cpus {num_cpus} " + "--num-gpus {num_gpus} " + # "--resources='{resources}' " + '--block"' +) + + +def host_dir(container_dir: str): + """Replace local dir with potentially different host dir. + + E.g. in docker-in-docker environments, the host dir might be + different to the mounted directory in the container. + + This method will do a simple global replace to adjust the paths. + """ + ray_tempdir = os.environ.get("RAY_TEMPDIR", None) + ray_hostdir = os.environ.get("RAY_HOSTDIR", None) + + if not ray_tempdir or not ray_hostdir: + return container_dir + + return container_dir.replace(ray_tempdir, ray_hostdir) + + +def create_node_spec( + head: bool, + docker_image: str, + mounted_cluster_dir: str, + mounted_node_dir: str, + num_cpus: int = 2, + num_gpus: int = 0, + resources: Optional[Dict] = None, + env_vars: Optional[Dict] = None, + host_gcs_port: int = 16379, + host_object_manager_port: int = 18076, + host_client_port: int = 10002, + volume_dir: Optional[str] = None, + node_state_path: Optional[str] = None, + docker_status_path: Optional[str] = None, + docker_compose_path: Optional[str] = None, + bootstrap_config_path: Optional[str] = None, + private_key_path: Optional[str] = None, + public_key_path: Optional[str] = None, +): + node_spec = copy.deepcopy(DOCKER_NODE_SKELETON) + node_spec["image"] = docker_image + + bootstrap_cfg_path_on_container = "/home/ray/ray_bootstrap_config.yaml" + bootstrap_key_path_on_container = "/home/ray/ray_bootstrap_key.pem" + + resources = resources or {} + + ensure_ssh = ( + ( + "((sudo apt update && sudo apt install -y openssh-server && " + "sudo service ssh start) || true)" + ) + if not bool(int(os.environ.get("RAY_HAS_SSH", "0") or "0")) + else "sudo service ssh start" + ) + + cmd_kwargs = dict( + ensure_ssh=ensure_ssh, + num_cpus=num_cpus, + num_gpus=num_gpus, + resources=json.dumps(resources, indent=None), + volume_dir=volume_dir, + autoscaling_config=bootstrap_cfg_path_on_container, + ) + + env_vars = env_vars or {} + + # Set to "auto" to mount current autoscaler directory to nodes for dev + fake_cluster_dev_dir = os.environ.get("FAKE_CLUSTER_DEV", "") + if fake_cluster_dev_dir: + if fake_cluster_dev_dir == "auto": + local_ray_dir = os.path.dirname(ray.__file__) + else: + local_ray_dir = fake_cluster_dev_dir + os.environ["FAKE_CLUSTER_DEV"] = local_ray_dir + + mj = sys.version_info.major + mi = sys.version_info.minor + + fake_modules_str = os.environ.get("FAKE_CLUSTER_DEV_MODULES", "autoscaler") + fake_modules = fake_modules_str.split(",") + + docker_ray_dir = f"/home/ray/anaconda3/lib/python{mj}.{mi}/site-packages/ray" + + node_spec["volumes"] += [ + f"{local_ray_dir}/{module}:{docker_ray_dir}/{module}:ro" + for module in fake_modules + ] + env_vars["FAKE_CLUSTER_DEV"] = local_ray_dir + env_vars["FAKE_CLUSTER_DEV_MODULES"] = fake_modules_str + os.environ["FAKE_CLUSTER_DEV_MODULES"] = fake_modules_str + + if head: + node_spec["command"] = DOCKER_HEAD_CMD.format(**cmd_kwargs) + # Expose ports so we can connect to the cluster from outside + node_spec["ports"] = [ + f"{host_gcs_port}:{ray_constants.DEFAULT_PORT}", + f"{host_object_manager_port}:8076", + f"{host_client_port}:10001", + ] + # Mount status and config files for the head node + node_spec["volumes"] += [ + f"{host_dir(node_state_path)}:{node_state_path}", + f"{host_dir(docker_status_path)}:{docker_status_path}", + f"{host_dir(docker_compose_path)}:{docker_compose_path}", + f"{host_dir(bootstrap_config_path)}:" f"{bootstrap_cfg_path_on_container}", + f"{host_dir(private_key_path)}:{bootstrap_key_path_on_container}", + ] + + # Create file if it does not exist on local filesystem + for filename in [node_state_path, docker_status_path, bootstrap_config_path]: + if not os.path.exists(filename): + with open(filename, "wt") as f: + f.write("{}") + else: + node_spec["command"] = DOCKER_WORKER_CMD.format(**cmd_kwargs) + node_spec["depends_on"] = [FAKE_HEAD_NODE_ID] + + # Mount shared directories and ssh access keys + node_spec["volumes"] += [ + f"{host_dir(mounted_cluster_dir)}:/cluster/shared", + f"{host_dir(mounted_node_dir)}:/cluster/node", + f"{host_dir(public_key_path)}:/home/ray/.ssh/authorized_keys", + ] + + # Pass these environment variables (to the head node) + # These variables are propagated by the `docker compose` command. + env_vars.setdefault("RAY_HAS_SSH", os.environ.get("RAY_HAS_SSH", "")) + env_vars.setdefault("RAY_TEMPDIR", os.environ.get("RAY_TEMPDIR", "")) + env_vars.setdefault("RAY_HOSTDIR", os.environ.get("RAY_HOSTDIR", "")) + + node_spec["environment"] = [f"{k}={v}" for k, v in env_vars.items()] + + return node_spec + + +class FakeMultiNodeProvider(NodeProvider): + """A node provider that implements multi-node on a single machine. + + This is used for laptop mode testing of autoscaling functionality.""" + + def __init__( + self, + provider_config, + cluster_name, + ): + """ + Args: + provider_config: Configuration for the provider. + cluster_name: Name of the cluster. + """ + + NodeProvider.__init__(self, provider_config, cluster_name) + self.lock = RLock() + if "RAY_FAKE_CLUSTER" not in os.environ: + raise RuntimeError( + "FakeMultiNodeProvider requires ray to be started with " + "RAY_FAKE_CLUSTER=1 ray start ..." + ) + # GCS address to use for the cluster + self._gcs_address = provider_config.get("gcs_address", None) + # Head node id + self._head_node_id = provider_config.get("head_node_id", FAKE_HEAD_NODE_ID) + # Whether to launch multiple nodes at once, or one by one regardless of + # the count (default) + self._launch_multiple = provider_config.get("launch_multiple", False) + + # These are injected errors for testing purposes. If not None, + # these will be raised on `create_node_with_resources_and_labels`` and + # `terminate_node``, respectively. + self._creation_error = None + self._termination_errors = None + + self._nodes = { + self._head_node_id: { + "tags": { + TAG_RAY_NODE_KIND: NODE_KIND_HEAD, + TAG_RAY_USER_NODE_TYPE: FAKE_HEAD_NODE_TYPE, + TAG_RAY_NODE_NAME: self._head_node_id, + TAG_RAY_NODE_STATUS: STATUS_UP_TO_DATE, + } + }, + } + self._next_node_id = 0 + + def _next_hex_node_id(self): + self._next_node_id += 1 + base = "fffffffffffffffffffffffffffffffffffffffffffffffffff" + return base + str(self._next_node_id).zfill(5) + + def non_terminated_nodes(self, tag_filters): + with self.lock: + nodes = [] + for node_id in self._nodes: + tags = self.node_tags(node_id) + ok = True + for k, v in tag_filters.items(): + if tags.get(k) != v: + ok = False + if ok: + nodes.append(node_id) + + return nodes + + def is_running(self, node_id): + with self.lock: + return node_id in self._nodes + + def is_terminated(self, node_id): + with self.lock: + return node_id not in self._nodes + + def node_tags(self, node_id): + with self.lock: + return self._nodes[node_id]["tags"] + + def _get_ip(self, node_id: str) -> Optional[str]: + return node_id + + def external_ip(self, node_id): + return self._get_ip(node_id) + + def internal_ip(self, node_id): + return self._get_ip(node_id) + + def set_node_tags(self, node_id, tags): + raise AssertionError("Readonly node provider cannot be updated") + + def create_node_with_resources_and_labels( + self, node_config, tags, count, resources, labels + ): + if self._creation_error: + raise self._creation_error + + if self._launch_multiple: + for _ in range(count): + self._create_node_with_resources_and_labels( + node_config, tags, count, resources, labels + ) + else: + self._create_node_with_resources_and_labels( + node_config, tags, count, resources, labels + ) + + def _create_node_with_resources_and_labels( + self, node_config, tags, count, resources, labels + ): + # This function calls `pop`. To avoid side effects, we make a + # copy of `resources`. + resources = copy.deepcopy(resources) + with self.lock: + node_type = tags[TAG_RAY_USER_NODE_TYPE] + next_id = self._next_hex_node_id() + ray_params = ray._private.parameter.RayParams( + min_worker_port=0, + max_worker_port=0, + dashboard_port=None, + num_cpus=resources.pop("CPU", 0), + num_gpus=resources.pop("GPU", 0), + object_store_memory=resources.pop("object_store_memory", None), + resources=resources, + labels=labels, + redis_address=build_address( + ray._private.services.get_node_ip_address(), 6379 + ) + if not self._gcs_address + else self._gcs_address, + gcs_address=build_address( + ray._private.services.get_node_ip_address(), 6379 + ) + if not self._gcs_address + else self._gcs_address, + env_vars={ + "RAY_OVERRIDE_NODE_ID_FOR_TESTING": next_id, + "RAY_CLOUD_INSTANCE_ID": next_id, + "RAY_NODE_TYPE_NAME": node_type, + ray_constants.RESOURCES_ENVIRONMENT_VARIABLE: json.dumps(resources), + ray_constants.LABELS_ENVIRONMENT_VARIABLE: json.dumps(labels), + }, + ) + node = ray._private.node.Node( + ray_params, head=False, shutdown_at_exit=False, spawn_reaper=False + ) + all_tags = { + TAG_RAY_NODE_KIND: NODE_KIND_WORKER, + TAG_RAY_USER_NODE_TYPE: node_type, + TAG_RAY_NODE_NAME: next_id, + TAG_RAY_NODE_STATUS: STATUS_UP_TO_DATE, + } + all_tags.update(tags) + self._nodes[next_id] = { + "tags": all_tags, + "node": node, + } + + def terminate_node(self, node_id): + with self.lock: + if self._termination_errors: + raise self._termination_errors + + try: + node = self._nodes.pop(node_id) + except Exception as e: + raise e + + self._terminate_node(node) + + def _terminate_node(self, node): + node["node"].kill_all_processes(check_alive=False, allow_graceful=True) + + @staticmethod + def bootstrap_config(cluster_config): + return cluster_config + + ############################ + # Test only methods + ############################ + def _test_set_creation_error(self, e: Exception): + """Set an error that will be raised on + create_node_with_resources_and_labels.""" + self._creation_error = e + + def _test_add_termination_errors(self, e: Exception): + """Set an error that will be raised on terminate_node.""" + self._termination_errors = e + + +class FakeMultiNodeDockerProvider(FakeMultiNodeProvider): + """A node provider that implements multi-node on a single machine. + + This is used for laptop mode testing of multi node functionality + where each node has their own FS and IP.""" + + def __init__(self, provider_config, cluster_name): + super(FakeMultiNodeDockerProvider, self).__init__(provider_config, cluster_name) + + fake_head = copy.deepcopy(self._nodes) + + self._project_name = self.provider_config["project_name"] + self._docker_image = self.provider_config["image"] + + self._host_gcs_port = self.provider_config.get( + "host_gcs_port", FAKE_DOCKER_DEFAULT_GCS_PORT + ) + self._host_object_manager_port = self.provider_config.get( + "host_object_manager_port", FAKE_DOCKER_DEFAULT_OBJECT_MANAGER_PORT + ) + self._host_client_port = self.provider_config.get( + "host_client_port", FAKE_DOCKER_DEFAULT_CLIENT_PORT + ) + + self._head_resources = self.provider_config["head_resources"] + + # subdirs: + # - ./shared (shared filesystem) + # - ./nodes/ (node-specific mounted filesystem) + self._volume_dir = self.provider_config["shared_volume_dir"] + self._mounted_cluster_dir = os.path.join(self._volume_dir, "shared") + + if not self.in_docker_container: + # Only needed on host + os.makedirs(self._mounted_cluster_dir, mode=0o755, exist_ok=True) + + self._boostrap_config_path = os.path.join( + self._volume_dir, "bootstrap_config.yaml" + ) + + self._private_key_path = os.path.join(self._volume_dir, "bootstrap_key.pem") + self._public_key_path = os.path.join(self._volume_dir, "bootstrap_key.pem.pub") + + if not self.in_docker_container: + # Create private key + if not os.path.exists(self._private_key_path): + subprocess.check_call( + f'ssh-keygen -b 2048 -t rsa -q -N "" ' + f"-f {self._private_key_path}", + shell=True, + ) + + # Create public key + if not os.path.exists(self._public_key_path): + subprocess.check_call( + f"ssh-keygen -y " + f"-f {self._private_key_path} " + f"> {self._public_key_path}", + shell=True, + ) + + self._docker_compose_config_path = os.path.join( + self._volume_dir, "docker-compose.yaml" + ) + self._docker_compose_config = None + + self._node_state_path = os.path.join(self._volume_dir, "nodes.json") + self._docker_status_path = os.path.join(self._volume_dir, "status.json") + + self._load_node_state() + if FAKE_HEAD_NODE_ID not in self._nodes: + # Reset + self._nodes = copy.deepcopy(fake_head) + + self._nodes[FAKE_HEAD_NODE_ID][ + "node_spec" + ] = self._create_node_spec_with_resources( + head=True, node_id=FAKE_HEAD_NODE_ID, resources=self._head_resources + ) + self._possibly_terminated_nodes = dict() + + self._cleanup_interval = provider_config.get("cleanup_interval", 9.5) + + self._docker_status = {} + + self._update_docker_compose_config() + self._update_docker_status() + self._save_node_state() + + @property + def in_docker_container(self): + return os.path.exists(os.path.join(self._volume_dir, ".in_docker")) + + def _create_node_spec_with_resources( + self, head: bool, node_id: str, resources: Dict[str, Any] + ): + resources = resources.copy() + + # Create shared directory + node_dir = os.path.join(self._volume_dir, "nodes", node_id) + os.makedirs(node_dir, mode=0o777, exist_ok=True) + + resource_str = json.dumps(resources, indent=None) + + return create_node_spec( + head=head, + docker_image=self._docker_image, + mounted_cluster_dir=self._mounted_cluster_dir, + mounted_node_dir=node_dir, + num_cpus=resources.pop("CPU", 0), + num_gpus=resources.pop("GPU", 0), + host_gcs_port=self._host_gcs_port, + host_object_manager_port=self._host_object_manager_port, + host_client_port=self._host_client_port, + resources=resources, + env_vars={ + "RAY_OVERRIDE_NODE_ID_FOR_TESTING": node_id, + ray_constants.RESOURCES_ENVIRONMENT_VARIABLE: resource_str, + **self.provider_config.get("env_vars", {}), + }, + volume_dir=self._volume_dir, + node_state_path=self._node_state_path, + docker_status_path=self._docker_status_path, + docker_compose_path=self._docker_compose_config_path, + bootstrap_config_path=self._boostrap_config_path, + public_key_path=self._public_key_path, + private_key_path=self._private_key_path, + ) + + def _load_node_state(self) -> bool: + if not os.path.exists(self._node_state_path): + return False + try: + with open(self._node_state_path, "rt") as f: + nodes = json.load(f) + except Exception: + return False + if not nodes: + return False + self._nodes = nodes + return True + + def _save_node_state(self): + with open(self._node_state_path, "wt") as f: + json.dump(self._nodes, f) + + # Make sure this is always writeable from inside the containers + if not self.in_docker_container: + # Only chmod from the outer container + os.chmod(self._node_state_path, 0o777) + + def _update_docker_compose_config(self): + config = copy.deepcopy(DOCKER_COMPOSE_SKELETON) + config["services"] = {} + for node_id, node in self._nodes.items(): + config["services"][node_id] = node["node_spec"] + + with open(self._docker_compose_config_path, "wt") as f: + yaml.safe_dump(config, f) + + def _update_docker_status(self): + if not os.path.exists(self._docker_status_path): + return + with open(self._docker_status_path, "rt") as f: + self._docker_status = json.load(f) + + def _update_nodes(self): + for node_id in list(self._nodes): + if not self._is_docker_running(node_id): + self._possibly_terminated_nodes.setdefault(node_id, time.monotonic()) + else: + self._possibly_terminated_nodes.pop(node_id, None) + self._cleanup_nodes() + + def _cleanup_nodes(self): + for node_id, timestamp in list(self._possibly_terminated_nodes.items()): + if time.monotonic() > timestamp + self._cleanup_interval: + if not self._is_docker_running(node_id): + self._nodes.pop(node_id, None) + self._possibly_terminated_nodes.pop(node_id, None) + self._save_node_state() + + def _container_name(self, node_id): + node_status = self._docker_status.get(node_id, {}) + timeout = time.monotonic() + 60 + while not node_status: + if time.monotonic() > timeout: + raise RuntimeError(f"Container for {node_id} never became available.") + time.sleep(1) + self._update_docker_status() + node_status = self._docker_status.get(node_id, {}) + + return node_status["Name"] + + def _is_docker_running(self, node_id): + self._update_docker_status() + + return self._docker_status.get(node_id, {}).get("State", None) == "running" + + def non_terminated_nodes(self, tag_filters): + self._update_nodes() + return super(FakeMultiNodeDockerProvider, self).non_terminated_nodes( + tag_filters + ) + + def is_running(self, node_id): + with self.lock: + self._update_nodes() + + return node_id in self._nodes and self._is_docker_running(node_id) + + def is_terminated(self, node_id): + with self.lock: + self._update_nodes() + + return node_id not in self._nodes and not self._is_docker_running(node_id) + + def get_command_runner( + self, + log_prefix: str, + node_id: str, + auth_config: Dict[str, Any], + cluster_name: str, + process_runner: ModuleType, + use_internal_ip: bool, + docker_config: Optional[Dict[str, Any]] = None, + ) -> CommandRunnerInterface: + if self.in_docker_container: + return super(FakeMultiNodeProvider, self).get_command_runner( + log_prefix, + node_id, + auth_config, + cluster_name, + process_runner, + use_internal_ip, + ) + + # Else, host command runner: + common_args = { + "log_prefix": log_prefix, + "node_id": node_id, + "provider": self, + "auth_config": auth_config, + "cluster_name": cluster_name, + "process_runner": process_runner, + "use_internal_ip": use_internal_ip, + } + + docker_config["container_name"] = self._container_name(node_id) + docker_config["image"] = self._docker_image + + return FakeDockerCommandRunner(docker_config, **common_args) + + def _get_ip(self, node_id: str) -> Optional[str]: + for i in range(3): + self._update_docker_status() + ip = self._docker_status.get(node_id, {}).get("IP", None) + if ip: + return ip + time.sleep(3) + return None + + def set_node_tags(self, node_id, tags): + assert node_id in self._nodes + self._nodes[node_id]["tags"].update(tags) + + def create_node_with_resources_and_labels( + self, node_config, tags, count, resources, labels + ): + with self.lock: + is_head = tags[TAG_RAY_NODE_KIND] == NODE_KIND_HEAD + + if is_head: + next_id = FAKE_HEAD_NODE_ID + else: + next_id = self._next_hex_node_id() + + self._nodes[next_id] = { + "tags": tags, + "node_spec": self._create_node_spec_with_resources( + head=is_head, node_id=next_id, resources=resources + ), + } + self._update_docker_compose_config() + self._save_node_state() + + def create_node( + self, node_config: Dict[str, Any], tags: Dict[str, str], count: int + ) -> Optional[Dict[str, Any]]: + resources = self._head_resources + return self.create_node_with_resources_and_labels( + node_config, tags, count, resources, {} + ) + + def _terminate_node(self, node): + self._update_docker_compose_config() + self._save_node_state() + + @staticmethod + def bootstrap_config(cluster_config): + return cluster_config diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/test_utils.py b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d541ed5f408a1f0e0665856b4e60d2f20c50d6e3 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/fake_multi_node/test_utils.py @@ -0,0 +1,399 @@ +import json +import logging +import os +import random +import shutil +import subprocess +import sys +import tempfile +import threading +import time +from typing import Any, Dict, Optional + +import yaml + +import ray +from ray._common.network_utils import build_address +from ray._private.dict import deep_update +from ray.autoscaler._private.fake_multi_node.node_provider import ( + FAKE_DOCKER_DEFAULT_CLIENT_PORT, + FAKE_DOCKER_DEFAULT_GCS_PORT, +) +from ray.util.queue import Empty, Queue + +logger = logging.getLogger(__name__) + +DEFAULT_DOCKER_IMAGE = "rayproject/ray:nightly-py{major}{minor}-cpu" + + +class ResourcesNotReadyError(RuntimeError): + pass + + +class DockerCluster: + """Docker cluster wrapper. + + Creates a directory for starting a fake multinode docker cluster. + + Includes APIs to update the cluster config as needed in tests, + and to start and connect to the cluster. + """ + + def __init__(self, config: Optional[Dict[str, Any]] = None): + self._base_config_file = os.path.join( + os.path.dirname(__file__), "example_docker.yaml" + ) + self._tempdir = None + self._config_file = None + self._nodes_file = None + self._nodes = {} + self._status_file = None + self._status = {} + self._partial_config = config + self._cluster_config = None + self._docker_image = None + + self._monitor_script = os.path.join( + os.path.dirname(__file__), "docker_monitor.py" + ) + self._monitor_process = None + + self._execution_thread = None + self._execution_event = threading.Event() + self._execution_queue = None + + @property + def config_file(self): + return self._config_file + + @property + def cluster_config(self): + return self._cluster_config + + @property + def cluster_dir(self): + return self._tempdir + + @property + def gcs_port(self): + return self._cluster_config.get("provider", {}).get( + "host_gcs_port", FAKE_DOCKER_DEFAULT_GCS_PORT + ) + + @property + def client_port(self): + return self._cluster_config.get("provider", {}).get( + "host_client_port", FAKE_DOCKER_DEFAULT_CLIENT_PORT + ) + + def connect(self, client: bool = True, timeout: int = 120, **init_kwargs): + """Connect to the docker-compose Ray cluster. + + Assumes the cluster is at RAY_TESTHOST (defaults to + ``127.0.0.1``). + + Args: + client: If True, uses Ray client to connect to the + cluster. If False, uses GCS to connect to the cluster. + timeout: Connection timeout in seconds. + **init_kwargs: kwargs to pass to ``ray.init()``. + + """ + host = os.environ.get("RAY_TESTHOST", "127.0.0.1") + + if client: + port = self.client_port + address = f"ray://{build_address(host, port)}" + else: + port = self.gcs_port + address = build_address(host, port) + + timeout_at = time.monotonic() + timeout + while time.monotonic() < timeout_at: + try: + ray.init(address, **init_kwargs) + self.wait_for_resources({"CPU": 1}) + except ResourcesNotReadyError: + time.sleep(1) + continue + else: + break + + try: + ray.cluster_resources() + except Exception as e: + raise RuntimeError(f"Timed out connecting to Ray: {e}") + + def remote_execution_api(self) -> "RemoteAPI": + """Create an object to control cluster state from within the cluster.""" + self._execution_queue = Queue(actor_options={"num_cpus": 0}) + stop_event = self._execution_event + + def entrypoint(): + while not stop_event.is_set(): + try: + cmd, kwargs = self._execution_queue.get(timeout=1) + except Empty: + continue + + if cmd == "kill_node": + self.kill_node(**kwargs) + + self._execution_thread = threading.Thread(target=entrypoint) + self._execution_thread.start() + + return RemoteAPI(self._execution_queue) + + @staticmethod + def wait_for_resources(resources: Dict[str, float], timeout: int = 60): + """Wait until Ray cluster resources are available + + Args: + resources: Minimum resources needed before + this function returns. + timeout: Timeout in seconds. + + """ + timeout = time.monotonic() + timeout + + available = ray.cluster_resources() + while any(available.get(k, 0.0) < v for k, v in resources.items()): + if time.monotonic() > timeout: + raise ResourcesNotReadyError( + f"Timed out waiting for resources: {resources}" + ) + time.sleep(1) + available = ray.cluster_resources() + + def update_config(self, config: Optional[Dict[str, Any]] = None): + """Update autoscaling config. + + Does a deep update of the base config with a new configuration. + This can change autoscaling behavior. + + Args: + config: Partial config to update current + config with. + + """ + assert self._tempdir, "Call setup() first" + + config = config or {} + + if config: + self._partial_config = config + + if not config.get("provider", {}).get("image"): + # No image specified, trying to parse from buildkite + docker_image = os.environ.get("RAY_DOCKER_IMAGE", None) + + if not docker_image: + # If still no docker image, use one according to Python version + mj = sys.version_info.major + mi = sys.version_info.minor + + docker_image = DEFAULT_DOCKER_IMAGE.format(major=mj, minor=mi) + + self._docker_image = docker_image + + with open(self._base_config_file, "rt") as f: + cluster_config = yaml.safe_load(f) + + if self._partial_config: + deep_update(cluster_config, self._partial_config, new_keys_allowed=True) + + if self._docker_image: + cluster_config["provider"]["image"] = self._docker_image + + cluster_config["provider"]["shared_volume_dir"] = self._tempdir + + self._cluster_config = cluster_config + + with open(self._config_file, "wt") as f: + yaml.safe_dump(self._cluster_config, f) + + logging.info(f"Updated cluster config to: {self._cluster_config}") + + def maybe_pull_image(self): + if self._docker_image: + try: + images_str = subprocess.check_output( + f"docker image inspect {self._docker_image}", shell=True + ) + images = json.loads(images_str) + except Exception as e: + logger.error(f"Error inspecting image {self._docker_image}: {e}") + return + + if not images: + try: + subprocess.check_call( + f"docker pull {self._docker_image}", shell=True + ) + except Exception as e: + logger.error(f"Error pulling image {self._docker_image}: {e}") + + def setup(self): + """Setup docker compose cluster environment. + + Creates the temporary directory, writes the initial config file, + and pulls the docker image, if required. + """ + self._tempdir = tempfile.mkdtemp(dir=os.environ.get("RAY_TEMPDIR", None)) + os.chmod(self._tempdir, 0o777) + self._config_file = os.path.join(self._tempdir, "cluster.yaml") + self._nodes_file = os.path.join(self._tempdir, "nodes.json") + self._status_file = os.path.join(self._tempdir, "status.json") + self.update_config() + self.maybe_pull_image() + + def teardown(self, keep_dir: bool = False): + """Tear down docker compose cluster environment. + + Args: + keep_dir: If True, cluster directory + will not be removed after termination. + """ + if not keep_dir: + shutil.rmtree(self._tempdir) + self._tempdir = None + self._config_file = None + + def _start_monitor(self): + self._monitor_process = subprocess.Popen( + [sys.executable, self._monitor_script, self.config_file] + ) + time.sleep(2) + + def _stop_monitor(self): + if self._monitor_process: + self._monitor_process.wait(timeout=30) + if self._monitor_process.poll() is None: + self._monitor_process.terminate() + self._monitor_process = None + + def start(self): + """Start docker compose cluster. + + Starts the monitor process and runs ``ray up``. + """ + self._start_monitor() + + subprocess.check_call( + f"RAY_FAKE_CLUSTER=1 ray up -y {self.config_file}", shell=True + ) + + def stop(self): + """Stop docker compose cluster. + + Runs ``ray down`` and stops the monitor process. + """ + if ray.is_initialized: + ray.shutdown() + + subprocess.check_call( + f"RAY_FAKE_CLUSTER=1 ray down -y {self.config_file}", shell=True + ) + + self._stop_monitor() + self._execution_event.set() + + def _update_nodes(self): + with open(self._nodes_file, "rt") as f: + self._nodes = json.load(f) + + def _update_status(self): + with open(self._status_file, "rt") as f: + self._status = json.load(f) + + def _get_node( + self, + node_id: Optional[str] = None, + num: Optional[int] = None, + rand: Optional[str] = None, + ) -> str: + self._update_nodes() + if node_id: + assert ( + not num and not rand + ), "Only provide either `node_id`, `num`, or `random`." + elif num: + assert ( + not node_id and not rand + ), "Only provide either `node_id`, `num`, or `random`." + base = "fffffffffffffffffffffffffffffffffffffffffffffffffff" + node_id = base + str(num).zfill(5) + elif rand: + assert ( + not node_id and not num + ), "Only provide either `node_id`, `num`, or `random`." + assert rand in [ + "worker", + "any", + ], "`random` must be one of ['worker', 'any']" + choices = list(self._nodes.keys()) + if rand == "worker": + choices.remove( + "fffffffffffffffffffffffffffffffffffffffffffffffffff00000" + ) + # Else: any + node_id = random.choice(choices) + + assert node_id in self._nodes, f"Node with ID {node_id} is not in active nodes." + return node_id + + def _get_docker_container(self, node_id: str) -> Optional[str]: + self._update_status() + node_status = self._status.get(node_id) + if not node_status: + return None + + return node_status["Name"] + + def kill_node( + self, + node_id: Optional[str] = None, + num: Optional[int] = None, + rand: Optional[str] = None, + ): + """Kill node. + + If ``node_id`` is given, kill that node. + + If ``num`` is given, construct node_id from this number, and kill + that node. + + If ``rand`` is given (as either ``worker`` or ``any``), kill a random + node. + """ + node_id = self._get_node(node_id=node_id, num=num, rand=rand) + container = self._get_docker_container(node_id=node_id) + subprocess.check_call(f"docker kill {container}", shell=True) + + +class RemoteAPI: + """Remote API to control cluster state from within cluster tasks. + + This API uses a Ray queue to interact with an execution thread on the + host machine that will execute commands passed to the queue. + + Instances of this class can be serialized and passed to Ray remote actors + to interact with cluster state (but they can also be used outside actors). + + The API subset is limited to specific commands. + + Args: + queue: Ray queue to push command instructions to. + + """ + + def __init__(self, queue: Queue): + self._queue = queue + + def kill_node( + self, + node_id: Optional[str] = None, + num: Optional[int] = None, + rand: Optional[str] = None, + ): + self._queue.put(("kill_node", dict(node_id=node_id, num=num, rand=rand))) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8364b284367b7e3cad68f40b3e66826499fd6cfa Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/config.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..069b6a37c09d5209908c63c11d7429207588f904 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/node.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a49480b4589bd8c5494c466b9e5f205d1d6a8d0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/node.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fb6fb5ef3432db857073f40694ac7fe488c582a Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/tpu_command_runner.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/tpu_command_runner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd3dd794ce4e828095b337c7adf153bfe6dce0c1 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/__pycache__/tpu_command_runner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/config.py b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/config.py new file mode 100644 index 0000000000000000000000000000000000000000..2e646526cb34ba1a4bc9ddf1bb2c51fbc23d9e34 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/config.py @@ -0,0 +1,825 @@ +import copy +import json +import logging +import os +import re +import time +from functools import partial, reduce + +import google_auth_httplib2 +import googleapiclient +import httplib2 +from google.oauth2 import service_account +from google.oauth2.credentials import Credentials as OAuthCredentials +from googleapiclient import discovery, errors + +from ray._private.accelerators import TPUAcceleratorManager, tpu +from ray.autoscaler._private.gcp.node import MAX_POLLS, POLL_INTERVAL, GCPNodeType +from ray.autoscaler._private.util import ( + check_legacy_fields, + generate_rsa_key_pair, + generate_ssh_key_name, + generate_ssh_key_paths, +) + +logger = logging.getLogger(__name__) + +VERSION = "v1" +TPU_VERSION = "v2alpha" # change once v2 is stable + +RAY = "ray-autoscaler" +DEFAULT_SERVICE_ACCOUNT_ID = RAY + "-sa-" + VERSION +SERVICE_ACCOUNT_EMAIL_TEMPLATE = "{account_id}@{project_id}.iam.gserviceaccount.com" +DEFAULT_SERVICE_ACCOUNT_CONFIG = { + "displayName": "Ray Autoscaler Service Account ({})".format(VERSION), +} + +# Those roles will be always added. +# NOTE: `serviceAccountUser` allows the head node to create workers with +# a serviceAccount. `roleViewer` allows the head node to run bootstrap_gcp. +DEFAULT_SERVICE_ACCOUNT_ROLES = [ + "roles/storage.objectAdmin", + "roles/compute.admin", + "roles/iam.serviceAccountUser", + "roles/iam.roleViewer", +] +# Those roles will only be added if there are TPU nodes defined in config. +TPU_SERVICE_ACCOUNT_ROLES = ["roles/tpu.admin"] + +# If there are TPU nodes in config, this field will be set +# to True in config["provider"]. +HAS_TPU_PROVIDER_FIELD = "_has_tpus" + +# NOTE: iam.serviceAccountUser allows the Head Node to create worker nodes +# with ServiceAccounts. + + +def tpu_accelerator_config_to_type(accelerator_config: dict) -> str: + """Convert a provided accelerator_config to accelerator_type. + + Args: + accelerator_config: A dictionary defining the spec of a + TPU accelerator. The dictionary should consist of + the keys 'type', indicating the TPU chip type, and + 'topology', indicating the topology of the TPU. + + Returns: + A string, accelerator_type, e.g. "v4-8". + + """ + generation = accelerator_config["type"].lower() + topology = accelerator_config["topology"] + # Reduce e.g. "2x2x2" to 8 + chip_dimensions = [int(chip_count) for chip_count in topology.split("x")] + num_chips = reduce(lambda x, y: x * y, chip_dimensions) + + # V5LitePod is rendered as "V5LITE_POD" in accelerator configuration but + # accelerator type uses a format like "v5litepod-{cores}", so we need + # to manually convert the string here. + if generation == "v5lite_pod": + generation = "v5litepod" + + num_cores = tpu.get_tpu_cores_per_chip(generation) * num_chips + + return f"{generation}-{num_cores}" + + +def _validate_tpu_config(node: dict): + """Validate the provided node with TPU support. + + If the config is malformed, users will run into an error but this function + will raise the error at config parsing time. This only tests very simple assertions. + + Raises: `ValueError` in case the input is malformed. + + """ + if "acceleratorType" in node and "acceleratorConfig" in node: + raise ValueError( + "For TPU usage, acceleratorType and acceleratorConfig " + "cannot both be set." + ) + if "acceleratorType" in node: + accelerator_type = node["acceleratorType"] + if not TPUAcceleratorManager.is_valid_tpu_accelerator_type(accelerator_type): + raise ValueError( + "`acceleratorType` should match v(generation)-(cores/chips). " + f"Got {accelerator_type}." + ) + else: # "acceleratorConfig" in node + accelerator_config = node["acceleratorConfig"] + if "type" not in accelerator_config or "topology" not in accelerator_config: + raise ValueError( + "acceleratorConfig expects 'type' and 'topology'. " + f"Got {accelerator_config}" + ) + generation = node["acceleratorConfig"]["type"] + topology = node["acceleratorConfig"]["topology"] + + generation_pattern = re.compile(r"^V\d+[a-zA-Z]*$") + topology_pattern = re.compile(r"^\d+x\d+(x\d+)?$") + + if generation != "V5LITE_POD" and not generation_pattern.match(generation): + raise ValueError(f"type should match V(generation). Got {generation}.") + if generation == "V2" or generation == "V3": + raise ValueError( + f"acceleratorConfig is not supported on V2/V3 TPUs. Got {generation}." + ) + if not topology_pattern.match(topology): + raise ValueError( + f"topology should be of form axbxc or axb. Got {topology}." + ) + + +def _get_num_tpu_chips(node: dict) -> int: + chips = 0 + if "acceleratorType" in node: + accelerator_type = node["acceleratorType"] + # `acceleratorType` is typically v{generation}-{cores} + cores = int(accelerator_type.split("-")[1]) + chips = cores / tpu.get_tpu_cores_per_chip(accelerator_type) + if "acceleratorConfig" in node: + topology = node["acceleratorConfig"]["topology"] + # `topology` is typically {chips}x{chips}x{chips} + # Multiply all dimensions together to get total number of chips + chips = 1 + for dim in topology.split("x"): + chips *= int(dim) + return chips + + +def _is_single_host_tpu(node: dict) -> bool: + accelerator_type = "" + if "acceleratorType" in node: + accelerator_type = node["acceleratorType"] + else: + accelerator_type = tpu_accelerator_config_to_type(node["acceleratorConfig"]) + return _get_num_tpu_chips(node) <= tpu.get_num_tpu_visible_chips_per_host( + accelerator_type + ) + + +def get_node_type(node: dict) -> GCPNodeType: + """Returns node type based on the keys in ``node``. + + This is a very simple check. If we have a ``machineType`` key, + this is a Compute instance. If we don't have a ``machineType`` key, + but we have ``acceleratorType``, this is a TPU. Otherwise, it's + invalid and an exception is raised. + + This works for both node configs and API returned nodes. + """ + + if ( + "machineType" not in node + and "acceleratorType" not in node + and "acceleratorConfig" not in node + ): + raise ValueError( + "Invalid node. For a Compute instance, 'machineType' is required." + "For a TPU instance, 'acceleratorType' OR 'acceleratorConfig' and " + f"no 'machineType' is required. Got {list(node)}." + ) + + if "machineType" not in node and ( + "acceleratorType" in node or "acceleratorConfig" in node + ): + _validate_tpu_config(node) + if not _is_single_host_tpu(node): + # Remove once proper autoscaling support is added. + logger.warning( + "TPU pod detected. Note that while the cluster launcher can create " + "multiple TPU pods, proper autoscaling will not work as expected, " + "as all hosts in a TPU pod need to execute the same program. " + "Proceed with caution." + ) + return GCPNodeType.TPU + return GCPNodeType.COMPUTE + + +def wait_for_crm_operation(operation, crm): + """Poll for cloud resource manager operation until finished.""" + logger.info( + "wait_for_crm_operation: " + "Waiting for operation {} to finish...".format(operation) + ) + + for _ in range(MAX_POLLS): + result = crm.operations().get(name=operation["name"]).execute() + if "error" in result: + raise Exception(result["error"]) + + if "done" in result and result["done"]: + logger.info("wait_for_crm_operation: Operation done.") + break + + time.sleep(POLL_INTERVAL) + + return result + + +def wait_for_compute_global_operation(project_name, operation, compute): + """Poll for global compute operation until finished.""" + logger.info( + "wait_for_compute_global_operation: " + "Waiting for operation {} to finish...".format(operation["name"]) + ) + + for _ in range(MAX_POLLS): + result = ( + compute.globalOperations() + .get( + project=project_name, + operation=operation["name"], + ) + .execute() + ) + if "error" in result: + raise Exception(result["error"]) + + if result["status"] == "DONE": + logger.info("wait_for_compute_global_operation: Operation done.") + break + + time.sleep(POLL_INTERVAL) + + return result + + +def _has_tpus_in_node_configs(config: dict) -> bool: + """Check if any nodes in config are TPUs.""" + node_configs = [ + node_type["node_config"] + for node_type in config["available_node_types"].values() + ] + return any(get_node_type(node) == GCPNodeType.TPU for node in node_configs) + + +def _is_head_node_a_tpu(config: dict) -> bool: + """Check if the head node is a TPU.""" + node_configs = { + node_id: node_type["node_config"] + for node_id, node_type in config["available_node_types"].items() + } + return get_node_type(node_configs[config["head_node_type"]]) == GCPNodeType.TPU + + +def build_request(http, *args, **kwargs): + new_http = google_auth_httplib2.AuthorizedHttp( + http.credentials, http=httplib2.Http() + ) + return googleapiclient.http.HttpRequest(new_http, *args, **kwargs) + + +def _create_crm(gcp_credentials=None): + return discovery.build( + "cloudresourcemanager", + "v1", + credentials=gcp_credentials, + requestBuilder=build_request, + cache_discovery=False, + ) + + +def _create_iam(gcp_credentials=None): + return discovery.build( + "iam", + "v1", + credentials=gcp_credentials, + requestBuilder=build_request, + cache_discovery=False, + ) + + +def _create_compute(gcp_credentials=None): + return discovery.build( + "compute", + "v1", + credentials=gcp_credentials, + requestBuilder=build_request, + cache_discovery=False, + ) + + +def _create_tpu(gcp_credentials=None): + return discovery.build( + "tpu", + TPU_VERSION, + credentials=gcp_credentials, + requestBuilder=build_request, + cache_discovery=False, + discoveryServiceUrl="https://tpu.googleapis.com/$discovery/rest", + ) + + +def construct_clients_from_provider_config(provider_config): + """ + Attempt to fetch and parse the JSON GCP credentials from the provider + config yaml file. + + tpu resource (the last element of the tuple) will be None if + `_has_tpus` in provider config is not set or False. + """ + gcp_credentials = provider_config.get("gcp_credentials") + if gcp_credentials is None: + logger.debug( + "gcp_credentials not found in cluster yaml file. " + "Falling back to GOOGLE_APPLICATION_CREDENTIALS " + "environment variable." + ) + tpu_resource = ( + _create_tpu() + if provider_config.get(HAS_TPU_PROVIDER_FIELD, False) + else None + ) + # If gcp_credentials is None, then discovery.build will search for + # credentials in the local environment. + return _create_crm(), _create_iam(), _create_compute(), tpu_resource + + assert ( + "type" in gcp_credentials + ), "gcp_credentials cluster yaml field missing 'type' field." + assert ( + "credentials" in gcp_credentials + ), "gcp_credentials cluster yaml field missing 'credentials' field." + + cred_type = gcp_credentials["type"] + credentials_field = gcp_credentials["credentials"] + + if cred_type == "service_account": + # If parsing the gcp_credentials failed, then the user likely made a + # mistake in copying the credentials into the config yaml. + try: + service_account_info = json.loads(credentials_field) + except json.decoder.JSONDecodeError: + raise RuntimeError( + "gcp_credentials found in cluster yaml file but " + "formatted improperly." + ) + credentials = service_account.Credentials.from_service_account_info( + service_account_info + ) + elif cred_type == "credentials_token": + # Otherwise the credentials type must be credentials_token. + credentials = OAuthCredentials(credentials_field) + + tpu_resource = ( + _create_tpu(credentials) + if provider_config.get(HAS_TPU_PROVIDER_FIELD, False) + else None + ) + + return ( + _create_crm(credentials), + _create_iam(credentials), + _create_compute(credentials), + tpu_resource, + ) + + +def bootstrap_gcp(config): + config = copy.deepcopy(config) + check_legacy_fields(config) + # Used internally to store head IAM role. + config["head_node"] = {} + + # Check if we have any TPUs defined, and if so, + # insert that information into the provider config + if _has_tpus_in_node_configs(config): + config["provider"][HAS_TPU_PROVIDER_FIELD] = True + + crm, iam, compute, tpu = construct_clients_from_provider_config(config["provider"]) + + config = _configure_project(config, crm) + config = _configure_iam_role(config, crm, iam) + config = _configure_key_pair(config, compute) + config = _configure_subnet(config, compute) + + return config + + +def _configure_project(config, crm): + """Setup a Google Cloud Platform Project. + + Google Compute Platform organizes all the resources, such as storage + buckets, users, and instances under projects. This is different from + aws ec2 where everything is global. + """ + config = copy.deepcopy(config) + + project_id = config["provider"].get("project_id") + assert config["provider"]["project_id"] is not None, ( + "'project_id' must be set in the 'provider' section of the autoscaler" + " config. Notice that the project id must be globally unique." + ) + project = _get_project(project_id, crm) + + if project is None: + # Project not found, try creating it + _create_project(project_id, crm) + project = _get_project(project_id, crm) + + assert project is not None, "Failed to create project" + assert ( + project["lifecycleState"] == "ACTIVE" + ), "Project status needs to be ACTIVE, got {}".format(project["lifecycleState"]) + + config["provider"]["project_id"] = project["projectId"] + + return config + + +def _configure_iam_role(config, crm, iam): + """Setup a gcp service account with IAM roles. + + Creates a gcp service acconut and binds IAM roles which allow it to control + control storage/compute services. Specifically, the head node needs to have + an IAM role that allows it to create further gce instances and store items + in google cloud storage. + + TODO: Allow the name/id of the service account to be configured + """ + config = copy.deepcopy(config) + + email = SERVICE_ACCOUNT_EMAIL_TEMPLATE.format( + account_id=DEFAULT_SERVICE_ACCOUNT_ID, + project_id=config["provider"]["project_id"], + ) + service_account = _get_service_account(email, config, iam) + + if service_account is None: + logger.info( + "_configure_iam_role: " + "Creating new service account {}".format(DEFAULT_SERVICE_ACCOUNT_ID) + ) + + service_account = _create_service_account( + DEFAULT_SERVICE_ACCOUNT_ID, DEFAULT_SERVICE_ACCOUNT_CONFIG, config, iam + ) + + assert service_account is not None, "Failed to create service account" + + if config["provider"].get(HAS_TPU_PROVIDER_FIELD, False): + roles = DEFAULT_SERVICE_ACCOUNT_ROLES + TPU_SERVICE_ACCOUNT_ROLES + else: + roles = DEFAULT_SERVICE_ACCOUNT_ROLES + + _add_iam_policy_binding(service_account, roles, crm) + + config["head_node"]["serviceAccounts"] = [ + { + "email": service_account["email"], + # NOTE: The amount of access is determined by the scope + IAM + # role of the service account. Even if the cloud-platform scope + # gives (scope) access to the whole cloud-platform, the service + # account is limited by the IAM rights specified below. + "scopes": ["https://www.googleapis.com/auth/cloud-platform"], + } + ] + + return config + + +def _configure_key_pair(config, compute): + """Configure SSH access, using an existing key pair if possible. + + Creates a project-wide ssh key that can be used to access all the instances + unless explicitly prohibited by instance config. + + The ssh-keys created by ray are of format: + + [USERNAME]:ssh-rsa [KEY_VALUE] [USERNAME] + + where: + + [USERNAME] is the user for the SSH key, specified in the config. + [KEY_VALUE] is the public SSH key value. + """ + config = copy.deepcopy(config) + + if "ssh_private_key" in config["auth"]: + return config + + ssh_user = config["auth"]["ssh_user"] + + project = compute.projects().get(project=config["provider"]["project_id"]).execute() + + # Key pairs associated with project meta data. The key pairs are general, + # and not just ssh keys. + ssh_keys_str = next( + ( + item + for item in project["commonInstanceMetadata"].get("items", []) + if item["key"] == "ssh-keys" + ), + {}, + ).get("value", "") + + ssh_keys = ssh_keys_str.split("\n") if ssh_keys_str else [] + + # Try a few times to get or create a good key pair. + key_found = False + for i in range(10): + key_name = generate_ssh_key_name( + "gcp", + i, + config["provider"]["region"], + config["provider"]["project_id"], + ssh_user, + ) + public_key_path, private_key_path = generate_ssh_key_paths(key_name) + + for ssh_key in ssh_keys: + key_parts = ssh_key.split(" ") + if len(key_parts) != 3: + continue + + if key_parts[2] == ssh_user and os.path.exists(private_key_path): + # Found a key + key_found = True + break + + # Writing the new ssh key to the filesystem fails if the ~/.ssh + # directory doesn't already exist. + os.makedirs(os.path.expanduser("~/.ssh"), exist_ok=True) + + # Create a key since it doesn't exist locally or in GCP + if not key_found and not os.path.exists(private_key_path): + logger.info( + "_configure_key_pair: Creating new key pair {}".format(key_name) + ) + public_key, private_key = generate_rsa_key_pair() + + _create_project_ssh_key_pair(project, public_key, ssh_user, compute) + + # Create the directory if it doesn't exists + private_key_dir = os.path.dirname(private_key_path) + os.makedirs(private_key_dir, exist_ok=True) + + # We need to make sure to _create_ the file with the right + # permissions. In order to do that we need to change the default + # os.open behavior to include the mode we want. + with open( + private_key_path, + "w", + opener=partial(os.open, mode=0o600), + ) as f: + f.write(private_key) + + with open(public_key_path, "w") as f: + f.write(public_key) + + key_found = True + + break + + if key_found: + break + + assert key_found, "SSH keypair for user {} not found for {}".format( + ssh_user, private_key_path + ) + assert os.path.exists( + private_key_path + ), "Private key file {} not found for user {}".format(private_key_path, ssh_user) + + logger.info( + "_configure_key_pair: " + "Private key not specified in config, using" + "{}".format(private_key_path) + ) + + config["auth"]["ssh_private_key"] = private_key_path + + return config + + +def _configure_subnet(config, compute): + """Pick a reasonable subnet if not specified by the config.""" + config = copy.deepcopy(config) + + node_configs = [ + node_type["node_config"] + for node_type in config["available_node_types"].values() + ] + # Rationale: avoid subnet lookup if the network is already + # completely manually configured + + # networkInterfaces is compute, networkConfig is TPU + if all( + "networkInterfaces" in node_config or "networkConfig" in node_config + for node_config in node_configs + ): + return config + + subnets = _list_subnets(config, compute) + + if not subnets: + raise NotImplementedError("Should be able to create subnet.") + + # TODO: make sure that we have usable subnet. Maybe call + # compute.subnetworks().listUsable? For some reason it didn't + # work out-of-the-box + default_subnet = subnets[0] + + default_interfaces = [ + { + "subnetwork": default_subnet["selfLink"], + "accessConfigs": [ + { + "name": "External NAT", + "type": "ONE_TO_ONE_NAT", + } + ], + } + ] + + for node_config in node_configs: + # The not applicable key will be removed during node creation + + # compute + if "networkInterfaces" not in node_config: + node_config["networkInterfaces"] = copy.deepcopy(default_interfaces) + # TPU + if "networkConfig" not in node_config: + node_config["networkConfig"] = copy.deepcopy(default_interfaces)[0] + node_config["networkConfig"].pop("accessConfigs") + + return config + + +def _list_subnets(config, compute): + response = ( + compute.subnetworks() + .list( + project=config["provider"]["project_id"], + region=config["provider"]["region"], + ) + .execute() + ) + + return response["items"] + + +def _get_subnet(config, subnet_id, compute): + subnet = ( + compute.subnetworks() + .get( + project=config["provider"]["project_id"], + region=config["provider"]["region"], + subnetwork=subnet_id, + ) + .execute() + ) + + return subnet + + +def _get_project(project_id, crm): + try: + project = crm.projects().get(projectId=project_id).execute() + except errors.HttpError as e: + if e.resp.status != 403: + raise + project = None + + return project + + +def _create_project(project_id, crm): + operation = ( + crm.projects() + .create(body={"projectId": project_id, "name": project_id}) + .execute() + ) + + result = wait_for_crm_operation(operation, crm) + + return result + + +def _get_service_account(account, config, iam): + project_id = config["provider"]["project_id"] + full_name = "projects/{project_id}/serviceAccounts/{account}".format( + project_id=project_id, account=account + ) + try: + service_account = iam.projects().serviceAccounts().get(name=full_name).execute() + except errors.HttpError as e: + if e.resp.status != 404: + raise + service_account = None + + return service_account + + +def _create_service_account(account_id, account_config, config, iam): + project_id = config["provider"]["project_id"] + + service_account = ( + iam.projects() + .serviceAccounts() + .create( + name="projects/{project_id}".format(project_id=project_id), + body={ + "accountId": account_id, + "serviceAccount": account_config, + }, + ) + .execute() + ) + + return service_account + + +def _add_iam_policy_binding(service_account, roles, crm): + """Add new IAM roles for the service account.""" + project_id = service_account["projectId"] + email = service_account["email"] + member_id = "serviceAccount:" + email + + policy = ( + crm.projects() + .getIamPolicy( + resource=project_id, body={"options": {"requestedPolicyVersion": 3}} + ) + .execute() + ) + + already_configured = True + for role in roles: + role_exists = False + for binding in policy["bindings"]: + if binding["role"] == role: + if member_id not in binding["members"]: + binding["members"].append(member_id) + already_configured = False + role_exists = True + + if not role_exists: + already_configured = False + policy["bindings"].append( + { + "members": [member_id], + "role": role, + } + ) + + if already_configured: + # In some managed environments, an admin needs to grant the + # roles, so only call setIamPolicy if needed. + return + + result = ( + crm.projects() + .setIamPolicy( + resource=project_id, + body={ + "policy": policy, + }, + ) + .execute() + ) + + return result + + +def _create_project_ssh_key_pair(project, public_key, ssh_user, compute): + """Inserts an ssh-key into project commonInstanceMetadata""" + + key_parts = public_key.split(" ") + + # Sanity checks to make sure that the generated key matches expectation + assert len(key_parts) == 2, key_parts + assert key_parts[0] == "ssh-rsa", key_parts + + new_ssh_meta = "{ssh_user}:ssh-rsa {key_value} {ssh_user}".format( + ssh_user=ssh_user, key_value=key_parts[1] + ) + + common_instance_metadata = project["commonInstanceMetadata"] + items = common_instance_metadata.get("items", []) + + ssh_keys_i = next( + (i for i, item in enumerate(items) if item["key"] == "ssh-keys"), None + ) + + if ssh_keys_i is None: + items.append({"key": "ssh-keys", "value": new_ssh_meta}) + else: + ssh_keys = items[ssh_keys_i] + ssh_keys["value"] += "\n" + new_ssh_meta + items[ssh_keys_i] = ssh_keys + + common_instance_metadata["items"] = items + + operation = ( + compute.projects() + .setCommonInstanceMetadata( + project=project["name"], body=common_instance_metadata + ) + .execute() + ) + + response = wait_for_compute_global_operation(project["name"], operation, compute) + + return response diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/node.py b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/node.py new file mode 100644 index 0000000000000000000000000000000000000000..56a756a587ce85bb9954337d0674cd8305c8f7a4 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/node.py @@ -0,0 +1,856 @@ +"""Abstractions around GCP resources and nodes. + +The logic has been abstracted away here to allow for different GCP resources +(API endpoints), which can differ widely, making it impossible to use +the same logic for everything. + +Classes inheriting from ``GCPResource`` represent different GCP resources - +API endpoints that allow for nodes to be created, removed, listed and +otherwise managed. Those classes contain methods abstracting GCP REST API +calls. +Each resource has a corresponding node type, represented by a +class inheriting from ``GCPNode``. Those classes are essentially dicts +with some extra methods. The instances of those classes will be created +from API responses. + +The ``GCPNodeType`` enum is a lightweight way to classify nodes. + +Currently, Compute and TPU resources & nodes are supported. + +In order to add support for new resources, create classes inheriting from +``GCPResource`` and ``GCPNode``, update the ``GCPNodeType`` enum, +update the ``_generate_node_name`` method and finally update the +node provider. +""" + +import abc +import logging +import re +import time +from collections import UserDict +from copy import deepcopy +from enum import Enum +from functools import wraps +from typing import Any, Dict, List, Optional, Tuple, Union +from uuid import uuid4 + +import httplib2 +from google_auth_httplib2 import AuthorizedHttp +from googleapiclient.discovery import Resource +from googleapiclient.errors import HttpError + +from ray.autoscaler.tags import TAG_RAY_CLUSTER_NAME, TAG_RAY_NODE_NAME + +logger = logging.getLogger(__name__) + +INSTANCE_NAME_MAX_LEN = 64 +INSTANCE_NAME_UUID_LEN = 8 +MAX_POLLS = 12 +# TPUs take a long while to respond, so we increase the MAX_POLLS +# considerably - this probably could be smaller +# TPU deletion uses MAX_POLLS +MAX_POLLS_TPU = MAX_POLLS * 8 +POLL_INTERVAL = 5 + + +def _retry_on_exception( + exception: Union[Exception, Tuple[Exception]], + regex: Optional[str] = None, + max_retries: int = MAX_POLLS, + retry_interval_s: int = POLL_INTERVAL, +): + """Retry a function call n-times for as long as it throws an exception.""" + + def dec(func): + @wraps(func) + def wrapper(*args, **kwargs): + def try_catch_exc(): + try: + value = func(*args, **kwargs) + return value + except Exception as e: + if not isinstance(e, exception) or ( + regex and not re.search(regex, str(e)) + ): + raise e + return e + + for _ in range(max_retries): + ret = try_catch_exc() + if not isinstance(ret, Exception): + break + time.sleep(retry_interval_s) + if isinstance(ret, Exception): + raise ret + return ret + + return wrapper + + return dec + + +def _generate_node_name(labels: dict, node_suffix: str) -> str: + """Generate node name from labels and suffix. + + This is required so that the correct resource can be selected + when the only information autoscaler has is the name of the node. + + The suffix is expected to be one of 'compute' or 'tpu' + (as in ``GCPNodeType``). + """ + name_label = labels[TAG_RAY_NODE_NAME] + assert len(name_label) <= (INSTANCE_NAME_MAX_LEN - INSTANCE_NAME_UUID_LEN - 1), ( + name_label, + len(name_label), + ) + return f"{name_label}-{uuid4().hex[:INSTANCE_NAME_UUID_LEN]}-{node_suffix}" + + +class GCPNodeType(Enum): + """Enum for GCP node types (compute & tpu)""" + + COMPUTE = "compute" + TPU = "tpu" + + @staticmethod + def from_gcp_node(node: "GCPNode"): + """Return GCPNodeType based on ``node``'s class""" + if isinstance(node, GCPTPUNode): + return GCPNodeType.TPU + if isinstance(node, GCPComputeNode): + return GCPNodeType.COMPUTE + raise TypeError(f"Wrong GCPNode type {type(node)}.") + + @staticmethod + def name_to_type(name: str): + """Provided a node name, determine the type. + + This expects the name to be in format '[NAME]-[UUID]-[TYPE]', + where [TYPE] is either 'compute' or 'tpu'. + """ + return GCPNodeType(name.split("-")[-1]) + + +class GCPNode(UserDict, metaclass=abc.ABCMeta): + """Abstraction around compute and tpu nodes""" + + NON_TERMINATED_STATUSES = None + RUNNING_STATUSES = None + STATUS_FIELD = None + + def __init__(self, base_dict: dict, resource: "GCPResource", **kwargs) -> None: + super().__init__(base_dict, **kwargs) + self.resource = resource + assert isinstance(self.resource, GCPResource) + + def is_running(self) -> bool: + return self.get(self.STATUS_FIELD) in self.RUNNING_STATUSES + + def is_terminated(self) -> bool: + return self.get(self.STATUS_FIELD) not in self.NON_TERMINATED_STATUSES + + @abc.abstractmethod + def get_labels(self) -> dict: + return + + @abc.abstractmethod + def get_external_ip(self) -> str: + return + + @abc.abstractmethod + def get_internal_ip(self) -> str: + return + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}: {self.get('name')}>" + + +class GCPComputeNode(GCPNode): + """Abstraction around compute nodes""" + + # https://cloud.google.com/compute/docs/instances/instance-life-cycle + NON_TERMINATED_STATUSES = {"PROVISIONING", "STAGING", "RUNNING"} + TERMINATED_STATUSES = {"TERMINATED", "SUSPENDED"} + RUNNING_STATUSES = {"RUNNING"} + STATUS_FIELD = "status" + + def get_labels(self) -> dict: + return self.get("labels", {}) + + def get_external_ip(self) -> str: + return ( + self.get("networkInterfaces", [{}])[0] + .get("accessConfigs", [{}])[0] + .get("natIP", None) + ) + + def get_internal_ip(self) -> str: + return self.get("networkInterfaces", [{}])[0].get("networkIP") + + +class GCPTPUNode(GCPNode): + """Abstraction around tpu nodes""" + + # https://cloud.google.com/tpu/docs/reference/rest/v2alpha1/projects.locations.nodes#State + + NON_TERMINATED_STATUSES = {"CREATING", "STARTING", "RESTARTING", "READY"} + RUNNING_STATUSES = {"READY"} + STATUS_FIELD = "state" + + def get_labels(self) -> dict: + return self.get("labels", {}) + + @property + def num_workers(self) -> int: + return len(self.get("networkEndpoints", [{}])) + + def get_external_ips(self) -> List[str]: + return self.get("networkEndpoints", [{}]) + + def get_external_ip(self, worker_index: int = 0) -> str: + return ( + self.get_external_ips()[worker_index] + .get("accessConfig", {}) + .get("externalIp", None) + ) + + def get_internal_ips(self) -> List[str]: + return self.get("networkEndpoints", [{}]) + + def get_internal_ip(self, worker_index: int = 0) -> str: + return self.get_internal_ips()[worker_index].get("ipAddress", None) + + +class GCPResource(metaclass=abc.ABCMeta): + """Abstraction around compute and TPU resources""" + + def __init__( + self, + resource: Resource, + project_id: str, + availability_zone: str, + cluster_name: str, + ) -> None: + self.resource = resource + self.project_id = project_id + self.availability_zone = availability_zone + self.cluster_name = cluster_name + + @abc.abstractmethod + def get_new_authorized_http(self, http: AuthorizedHttp) -> AuthorizedHttp: + """Generate a new AuthorizedHttp object with the given credentials.""" + return + + @abc.abstractmethod + def wait_for_operation( + self, + operation: dict, + max_polls: int = MAX_POLLS, + poll_interval: int = POLL_INTERVAL, + ) -> dict: + """Waits a preset amount of time for operation to complete.""" + return None + + @abc.abstractmethod + def list_instances( + self, + label_filters: Optional[dict] = None, + is_terminated: bool = False, + ) -> List["GCPNode"]: + """Returns a filtered list of all instances. + + The filter removes all terminated instances and, if ``label_filters`` + are provided, all instances which labels are not matching the + ones provided. + """ + return + + @abc.abstractmethod + def get_instance(self, node_id: str) -> "GCPNode": + """Returns a single instance.""" + return + + @abc.abstractmethod + def set_labels( + self, node: GCPNode, labels: dict, wait_for_operation: bool = True + ) -> dict: + """Sets labels on an instance and returns result. + + Completely replaces the labels dictionary.""" + return + + @abc.abstractmethod + def create_instance( + self, base_config: dict, labels: dict, wait_for_operation: bool = True + ) -> Tuple[dict, str]: + """Creates a single instance and returns result. + + Returns a tuple of (result, node_name). + """ + return + + def create_instances( + self, + base_config: dict, + labels: dict, + count: int, + wait_for_operation: bool = True, + ) -> List[Tuple[dict, str]]: + """Creates multiple instances and returns result. + + Returns a list of tuples of (result, node_name). + """ + operations = [ + self.create_instance(base_config, labels, wait_for_operation=False) + for i in range(count) + ] + + if wait_for_operation: + results = [ + (self.wait_for_operation(operation), node_name) + for operation, node_name in operations + ] + else: + results = operations + + return results + + @abc.abstractmethod + def delete_instance(self, node_id: str, wait_for_operation: bool = True) -> dict: + """Deletes an instance and returns result.""" + return + + @abc.abstractmethod + def stop_instance(self, node_id: str, wait_for_operation: bool = True) -> dict: + """Deletes an instance and returns result.""" + return + + @abc.abstractmethod + def start_instance(self, node_id: str, wait_for_operation: bool = True) -> dict: + """Starts a single instance and returns result.""" + return + + +class GCPCompute(GCPResource): + """Abstraction around GCP compute resource""" + + def get_new_authorized_http(self, http: AuthorizedHttp) -> AuthorizedHttp: + """Generate a new AuthorizedHttp object with the given credentials.""" + new_http = AuthorizedHttp(http.credentials, http=httplib2.Http()) + return new_http + + def wait_for_operation( + self, + operation: dict, + max_polls: int = MAX_POLLS, + poll_interval: int = POLL_INTERVAL, + ) -> dict: + """Poll for compute zone operation until finished.""" + logger.info( + "wait_for_compute_zone_operation: " + f"Waiting for operation {operation['name']} to finish..." + ) + + for _ in range(max_polls): + result = ( + self.resource.zoneOperations() + .get( + project=self.project_id, + operation=operation["name"], + zone=self.availability_zone, + ) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + if "error" in result: + raise Exception(result["error"]) + + if result["status"] == "DONE": + logger.info( + "wait_for_compute_zone_operation: " + f"Operation {operation['name']} finished." + ) + break + + time.sleep(poll_interval) + + return result + + def list_instances( + self, + label_filters: Optional[dict] = None, + is_terminated: bool = False, + ) -> List[GCPComputeNode]: + label_filters = label_filters or {} + + if label_filters: + label_filter_expr = ( + "(" + + " AND ".join( + [ + "(labels.{key} = {value})".format(key=key, value=value) + for key, value in label_filters.items() + ] + ) + + ")" + ) + else: + label_filter_expr = "" + + statuses = ( + GCPComputeNode.TERMINATED_STATUSES + if is_terminated + else GCPComputeNode.NON_TERMINATED_STATUSES + ) + + instance_state_filter_expr = ( + "(" + + " OR ".join( + ["(status = {status})".format(status=status) for status in statuses] + ) + + ")" + ) + + cluster_name_filter_expr = "(labels.{key} = {value})".format( + key=TAG_RAY_CLUSTER_NAME, value=self.cluster_name + ) + + # TPU VMs spawn accompanying Compute Instances that must be filtered out, + # else this results in duplicated nodes. + tpu_negation_filter_expr = "(NOT labels.{label}:*)".format(label="tpu_cores") + + not_empty_filters = [ + f + for f in [ + label_filter_expr, + instance_state_filter_expr, + cluster_name_filter_expr, + tpu_negation_filter_expr, + ] + if f + ] + + filter_expr = " AND ".join(not_empty_filters) + + response = ( + self.resource.instances() + .list( + project=self.project_id, + zone=self.availability_zone, + filter=filter_expr, + ) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + instances = response.get("items", []) + return [GCPComputeNode(i, self) for i in instances] + + def get_instance(self, node_id: str) -> GCPComputeNode: + instance = ( + self.resource.instances() + .get( + project=self.project_id, + zone=self.availability_zone, + instance=node_id, + ) + .execute() + ) + + return GCPComputeNode(instance, self) + + def set_labels( + self, node: GCPComputeNode, labels: dict, wait_for_operation: bool = True + ) -> dict: + body = { + "labels": dict(node["labels"], **labels), + "labelFingerprint": node["labelFingerprint"], + } + node_id = node["name"] + operation = ( + self.resource.instances() + .setLabels( + project=self.project_id, + zone=self.availability_zone, + instance=node_id, + body=body, + ) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + if wait_for_operation: + result = self.wait_for_operation(operation) + else: + result = operation + + return result + + def _convert_resources_to_urls( + self, configuration_dict: Dict[str, Any] + ) -> Dict[str, Any]: + """Ensures that resources are in their full URL form. + + GCP expects machineType and acceleratorType to be a full URL (e.g. + `zones/us-west1/machineTypes/n1-standard-2`) instead of just the + type (`n1-standard-2`) + + Args: + configuration_dict: Dict of options that will be passed to GCP + Returns: + Input dictionary, but with possibly expanding `machineType` and + `acceleratorType`. + """ + configuration_dict = deepcopy(configuration_dict) + existing_machine_type = configuration_dict["machineType"] + if not re.search(".*/machineTypes/.*", existing_machine_type): + configuration_dict[ + "machineType" + ] = "zones/{zone}/machineTypes/{machine_type}".format( + zone=self.availability_zone, + machine_type=configuration_dict["machineType"], + ) + + for accelerator in configuration_dict.get("guestAccelerators", []): + gpu_type = accelerator["acceleratorType"] + if not re.search(".*/acceleratorTypes/.*", gpu_type): + accelerator[ + "acceleratorType" + ] = "projects/{project}/zones/{zone}/acceleratorTypes/{accelerator}".format( # noqa: E501 + project=self.project_id, + zone=self.availability_zone, + accelerator=gpu_type, + ) + + return configuration_dict + + def create_instance( + self, base_config: dict, labels: dict, wait_for_operation: bool = True + ) -> Tuple[dict, str]: + + config = self._convert_resources_to_urls(base_config) + # removing TPU-specific default key set in config.py + config.pop("networkConfig", None) + name = _generate_node_name(labels, GCPNodeType.COMPUTE.value) + + labels = dict(config.get("labels", {}), **labels) + + config.update( + { + "labels": dict(labels, **{TAG_RAY_CLUSTER_NAME: self.cluster_name}), + "name": name, + } + ) + + # Allow Google Compute Engine instance templates. + # + # Config example: + # + # ... + # node_config: + # sourceInstanceTemplate: global/instanceTemplates/worker-16 + # machineType: e2-standard-16 + # ... + # + # node_config parameters override matching template parameters, if any. + # + # https://cloud.google.com/compute/docs/instance-templates + # https://cloud.google.com/compute/docs/reference/rest/v1/instances/insert + source_instance_template = config.pop("sourceInstanceTemplate", None) + + operation = ( + self.resource.instances() + .insert( + project=self.project_id, + zone=self.availability_zone, + sourceInstanceTemplate=source_instance_template, + body=config, + ) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + if wait_for_operation: + result = self.wait_for_operation(operation) + else: + result = operation + + return result, name + + def delete_instance(self, node_id: str, wait_for_operation: bool = True) -> dict: + operation = ( + self.resource.instances() + .delete( + project=self.project_id, + zone=self.availability_zone, + instance=node_id, + ) + .execute() + ) + + if wait_for_operation: + result = self.wait_for_operation(operation) + else: + result = operation + + return result + + def stop_instance(self, node_id: str, wait_for_operation: bool = True) -> dict: + operation = ( + self.resource.instances() + .stop( + project=self.project_id, + zone=self.availability_zone, + instance=node_id, + ) + .execute() + ) + + if wait_for_operation: + result = self.wait_for_operation(operation) + else: + result = operation + return result + + def start_instance(self, node_id: str, wait_for_operation: bool = True) -> dict: + + operation = ( + self.resource.instances() + .start( + project=self.project_id, + zone=self.availability_zone, + instance=node_id, + ) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + if wait_for_operation: + result = self.wait_for_operation(operation) + else: + result = operation + return result + + +class GCPTPU(GCPResource): + """Abstraction around GCP TPU resource""" + + # node names already contain the path, but this is required for `parent` + # arguments + @property + def path(self): + return f"projects/{self.project_id}/locations/{self.availability_zone}" + + def get_new_authorized_http(self, http: AuthorizedHttp) -> AuthorizedHttp: + """Generate a new AuthorizedHttp object with the given credentials.""" + new_http = AuthorizedHttp(http.credentials, http=httplib2.Http()) + return new_http + + def wait_for_operation( + self, + operation: dict, + max_polls: int = MAX_POLLS_TPU, + poll_interval: int = POLL_INTERVAL, + ) -> dict: + """Poll for TPU operation until finished.""" + logger.info( + "wait_for_tpu_operation: " + f"Waiting for operation {operation['name']} to finish..." + ) + + for _ in range(max_polls): + result = ( + self.resource.projects() + .locations() + .operations() + .get(name=f"{operation['name']}") + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + if "error" in result: + raise Exception(result["error"]) + + if "response" in result: + logger.info( + "wait_for_tpu_operation: " + f"Operation {operation['name']} finished." + ) + break + + time.sleep(poll_interval) + + return result + + def list_instances( + self, + label_filters: Optional[dict] = None, + is_terminated: bool = False, + ) -> List[GCPTPUNode]: + response = ( + self.resource.projects() + .locations() + .nodes() + .list(parent=self.path) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + instances = response.get("nodes", []) + instances = [GCPTPUNode(i, self) for i in instances] + + # filter_expr cannot be passed directly to API + # so we need to filter the results ourselves + + # same logic as in GCPCompute.list_instances + label_filters = label_filters or {} + label_filters[TAG_RAY_CLUSTER_NAME] = self.cluster_name + + def filter_instance(instance: GCPTPUNode) -> bool: + if instance.is_terminated(): + return False + + labels = instance.get_labels() + if label_filters: + for key, value in label_filters.items(): + if key not in labels: + return False + if value != labels[key]: + return False + + return True + + instances = list(filter(filter_instance, instances)) + + return instances + + def get_instance(self, node_id: str) -> GCPTPUNode: + instance = ( + self.resource.projects() + .locations() + .nodes() + .get(name=node_id) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + return GCPTPUNode(instance, self) + + # this sometimes fails without a clear reason, so we retry it + # MAX_POLLS times + @_retry_on_exception(HttpError, "unable to queue the operation") + def set_labels( + self, node: GCPTPUNode, labels: dict, wait_for_operation: bool = True + ) -> dict: + body = { + "labels": dict(node["labels"], **labels), + } + update_mask = "labels" + + operation = ( + self.resource.projects() + .locations() + .nodes() + .patch( + name=node["name"], + updateMask=update_mask, + body=body, + ) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + if wait_for_operation: + result = self.wait_for_operation(operation) + else: + result = operation + + return result + + def create_instance( + self, base_config: dict, labels: dict, wait_for_operation: bool = True + ) -> Tuple[dict, str]: + config = base_config.copy() + # removing Compute-specific default key set in config.py + config.pop("networkInterfaces", None) + name = _generate_node_name(labels, GCPNodeType.TPU.value) + + labels = dict(config.get("labels", {}), **labels) + + config.update( + { + "labels": dict(labels, **{TAG_RAY_CLUSTER_NAME: self.cluster_name}), + } + ) + + if "networkConfig" not in config: + config["networkConfig"] = {} + if "enableExternalIps" not in config["networkConfig"]: + # this is required for SSH to work, per google documentation + # https://cloud.google.com/tpu/docs/users-guide-tpu-vm#create-curl + config["networkConfig"]["enableExternalIps"] = True + + # replace serviceAccounts with serviceAccount, and scopes with scope + # this is necessary for the head node to work + # see here: https://tpu.googleapis.com/$discovery/rest?version=v2alpha1 + if "serviceAccounts" in config: + config["serviceAccount"] = config.pop("serviceAccounts")[0] + config["serviceAccount"]["scope"] = config["serviceAccount"].pop("scopes") + + operation = ( + self.resource.projects() + .locations() + .nodes() + .create( + parent=self.path, + body=config, + nodeId=name, + ) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + if wait_for_operation: + result = self.wait_for_operation(operation) + else: + result = operation + + return result, name + + def delete_instance(self, node_id: str, wait_for_operation: bool = True) -> dict: + operation = ( + self.resource.projects() + .locations() + .nodes() + .delete(name=node_id) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + # No need to increase MAX_POLLS for deletion + if wait_for_operation: + result = self.wait_for_operation(operation, max_polls=MAX_POLLS) + else: + result = operation + + return result + + def stop_instance(self, node_id: str, wait_for_operation: bool = True) -> dict: + operation = ( + self.resource.projects() + .locations() + .nodes() + .stop(name=node_id) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + if wait_for_operation: + result = self.wait_for_operation(operation, max_polls=MAX_POLLS) + else: + result = operation + + return result + + def start_instance(self, node_id: str, wait_for_operation: bool = True) -> dict: + operation = ( + self.resource.projects() + .locations() + .nodes() + .start(name=node_id) + .execute(http=self.get_new_authorized_http(self.resource._http)) + ) + + if wait_for_operation: + result = self.wait_for_operation(operation, max_polls=MAX_POLLS) + else: + result = operation + + return result diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..56398433f6249efa9dc3624937532e99d75e0e8a --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/node_provider.py @@ -0,0 +1,350 @@ +import copy +import logging +import time +from functools import wraps +from threading import RLock +from types import ModuleType +from typing import Any, Dict, List, Optional, Tuple + +import googleapiclient + +from ray.autoscaler._private.gcp.config import ( + bootstrap_gcp, + construct_clients_from_provider_config, + get_node_type, + tpu_accelerator_config_to_type, +) + +# The logic has been abstracted away here to allow for different GCP resources +# (API endpoints), which can differ widely, making it impossible to use +# the same logic for everything. +from ray.autoscaler._private.gcp.node import ( + GCPTPU, # noqa + GCPCompute, + GCPNode, + GCPNodeType, + GCPResource, +) +from ray.autoscaler._private.gcp.tpu_command_runner import TPUCommandRunner +from ray.autoscaler.command_runner import CommandRunnerInterface +from ray.autoscaler.node_provider import NodeProvider + +logger = logging.getLogger(__name__) + + +def _retry(method, max_tries=5, backoff_s=1): + """Retry decorator for methods of GCPNodeProvider. + + Upon catching BrokenPipeError, API clients are rebuilt and + decorated methods are retried. + + Work-around for https://github.com/ray-project/ray/issues/16072. + Based on https://github.com/kubeflow/pipelines/pull/5250/files. + """ + + @wraps(method) + def method_with_retries(self, *args, **kwargs): + try_count = 0 + while try_count < max_tries: + try: + return method(self, *args, **kwargs) + except BrokenPipeError: + logger.warning("Caught a BrokenPipeError. Retrying.") + try_count += 1 + if try_count < max_tries: + self._construct_clients() + time.sleep(backoff_s) + else: + raise + + return method_with_retries + + +class GCPNodeProvider(NodeProvider): + def __init__(self, provider_config: dict, cluster_name: str): + NodeProvider.__init__(self, provider_config, cluster_name) + self.lock = RLock() + self._construct_clients() + self.cache_stopped_nodes = provider_config.get("cache_stopped_nodes", False) + + # Cache of node objects from the last nodes() call. This avoids + # excessive DescribeInstances requests. + self.cached_nodes: Dict[str, GCPNode] = {} + + def _construct_clients(self): + _, _, compute, tpu = construct_clients_from_provider_config( + self.provider_config + ) + + # Dict of different resources provided by GCP. + # At this moment - Compute and TPUs + self.resources: Dict[GCPNodeType, GCPResource] = {} + + # Compute is always required + self.resources[GCPNodeType.COMPUTE] = GCPCompute( + compute, + self.provider_config["project_id"], + self.provider_config["availability_zone"], + self.cluster_name, + ) + + # if there are no TPU nodes defined in config, tpu will be None. + if tpu is not None: + self.resources[GCPNodeType.TPU] = GCPTPU( + tpu, + self.provider_config["project_id"], + self.provider_config["availability_zone"], + self.cluster_name, + ) + + def _get_resource_depending_on_node_name(self, node_name: str) -> GCPResource: + """Return the resource responsible for the node, based on node_name. + + This expects the name to be in format '[NAME]-[UUID]-[TYPE]', + where [TYPE] is either 'compute' or 'tpu' (see ``GCPNodeType``). + """ + return self.resources[GCPNodeType.name_to_type(node_name)] + + @_retry + def non_terminated_nodes(self, tag_filters: dict): + with self.lock: + instances = [] + + for resource in self.resources.values(): + node_instances = resource.list_instances(tag_filters) + instances += node_instances + + # Note: All the operations use "name" as the unique instance id + self.cached_nodes = {i["name"]: i for i in instances} + return [i["name"] for i in instances] + + def is_running(self, node_id: str): + with self.lock: + node = self._get_cached_node(node_id) + return node.is_running() + + def is_terminated(self, node_id: str): + with self.lock: + node = self._get_cached_node(node_id) + return node.is_terminated() + + def node_tags(self, node_id: str): + with self.lock: + node = self._get_cached_node(node_id) + return node.get_labels() + + @_retry + def set_node_tags(self, node_id: str, tags: dict): + with self.lock: + labels = tags + node = self._get_node(node_id) + + resource = self._get_resource_depending_on_node_name(node_id) + + result = resource.set_labels(node=node, labels=labels) + + return result + + def external_ip(self, node_id: str): + with self.lock: + node = self._get_cached_node(node_id) + + ip = node.get_external_ip() + if ip is None: + node = self._get_node(node_id) + ip = node.get_external_ip() + + return ip + + def internal_ip(self, node_id: str): + with self.lock: + node = self._get_cached_node(node_id) + + ip = node.get_internal_ip() + if ip is None: + node = self._get_node(node_id) + ip = node.get_internal_ip() + + return ip + + @_retry + def create_node(self, base_config: dict, tags: dict, count: int) -> Dict[str, dict]: + """Creates instances. + + Returns dict mapping instance id to each create operation result for the created + instances. + """ + with self.lock: + labels = tags # gcp uses "labels" instead of aws "tags" + + node_type = get_node_type(base_config) + resource = self.resources[node_type] + + all_nodes = {} + if self.cache_stopped_nodes: + filters = { + "ray-node-name": labels["ray-node-name"], + "ray-node-type": labels["ray-node-type"], + "ray-user-node-type": labels["ray-user-node-type"], + } + reuse_nodes = resource.list_instances(filters, True)[:count] + if reuse_nodes: + reused_nodes_dict = { + n["name"]: resource.start_instance(n["name"]) + for n in reuse_nodes + } + all_nodes.update(reused_nodes_dict) + count -= len(reuse_nodes) + + if count > 0: + results: List[Tuple[dict, str]] = resource.create_instances( + base_config, labels, count + ) + + created_nodes_dict = { + instance_id: result for result, instance_id in results + } + all_nodes.update(created_nodes_dict) + + return all_nodes + + def _thread_unsafe_terminate_node(self, node_id: str): + # Assumes the global lock is held for the duration of this operation. + # The lock may be held by a different thread if in `terminate_nodes()` case. + logger.info("NodeProvider: {}: Terminating node".format(node_id)) + resource = self._get_resource_depending_on_node_name(node_id) + try: + result = resource.delete_instance( + node_id=node_id, + ) + except googleapiclient.errors.HttpError as http_error: + if http_error.resp.status == 404: + logger.warning( + f"Tried to delete the node with id {node_id} " + "but it was already gone." + ) + result = None + else: + raise http_error from None + return result + + @_retry + def terminate_node(self, node_id: str): + with self.lock: + resource = self._get_resource_depending_on_node_name(node_id) + try: + if self.cache_stopped_nodes: + node = self._get_cached_node(node_id) + if node.is_running(): + result = resource.stop_instance(node_id=node_id) + else: + result = None + else: + result = resource.delete_instance( + node_id=node_id, + ) + except googleapiclient.errors.HttpError as http_error: + if http_error.resp.status == 404: + logger.warning( + f"Tried to delete the node with id {node_id} " + "but it was already gone." + ) + else: + raise http_error from None + return result + + @_retry + def _get_node(self, node_id: str) -> GCPNode: + self.non_terminated_nodes({}) # Side effect: updates cache + + with self.lock: + if node_id in self.cached_nodes: + return self.cached_nodes[node_id] + + resource = self._get_resource_depending_on_node_name(node_id) + instance = resource.get_instance(node_id=node_id) + + return instance + + def _get_cached_node(self, node_id: str) -> GCPNode: + if node_id in self.cached_nodes: + return self.cached_nodes[node_id] + + return self._get_node(node_id) + + @staticmethod + def bootstrap_config(cluster_config): + return bootstrap_gcp(cluster_config) + + @staticmethod + def fillout_available_node_types_resources( + cluster_config: Dict[str, Any] + ) -> Dict[str, Any]: + """Fill out TPU resources to the cluster config. + + To enable TPU pod autoscaling, we provide the TPU accelerator + type as a resource that only exists on worker 0 of the pod slice. + For instance, a v4-16 should have the resource labels: + worker 0: resources = {"TPU": 4, "TPU-v4-16-head": 1} + worker 1: resources = {"TPU": 4} + + For the autoscaler to correctly process the demands of + creating a new TPU pod, then the autoscaler must know what + a TPU pod is in the form of the TPU accelerator resource. + + Therefore we fill out TPU pods appropriately by providing the + expected resource which we can deduce from the cluster config. + + """ + if "available_node_types" not in cluster_config: + return cluster_config + cluster_config = copy.deepcopy(cluster_config) + available_node_types = cluster_config["available_node_types"] + for node_type in available_node_types: + node_config = available_node_types[node_type]["node_config"] + if get_node_type(node_config) == GCPNodeType.TPU: + autodetected_resources = {} + accelerator_type = "" + if "acceleratorType" in node_config: + accelerator_type = node_config["acceleratorType"] + elif "acceleratorConfig" in node_config: + accelerator_type = tpu_accelerator_config_to_type( + node_config["acceleratorConfig"] + ) + if not accelerator_type: + continue + autodetected_resources[f"TPU-{accelerator_type}-head"] = 1 + available_node_types[node_type]["resources"].update( + autodetected_resources + ) + return cluster_config + + def get_command_runner( + self, + log_prefix: str, + node_id: str, + auth_config: Dict[str, Any], + cluster_name: str, + process_runner: ModuleType, + use_internal_ip: bool, + docker_config: Optional[Dict[str, Any]] = None, + ) -> CommandRunnerInterface: + """Returns a TPU command runner as applicable.""" + resource = self._get_resource_depending_on_node_name(node_id) + instance = resource.get_instance(node_id) + common_args = { + "docker_config": docker_config, + "log_prefix": log_prefix, + "node_id": node_id, + "auth_config": auth_config, + "cluster_name": cluster_name, + "process_runner": process_runner, + "use_internal_ip": use_internal_ip, + } + if ( + GCPNodeType.TPU in self.resources + and resource == self.resources[GCPNodeType.TPU] + ): + return TPUCommandRunner(instance=instance, provider=self, **common_args) + else: + return super().get_command_runner(**common_args) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/tpu_command_runner.py b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/tpu_command_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..b27920863a19f82e217927b903dd4a7c3a085bbd --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/gcp/tpu_command_runner.py @@ -0,0 +1,328 @@ +"""Command runners specific to TPU VM pods. + +TPU VM pods may contain multiple hosts, each including attached TPU chips and +associated internal/external IP addresses. + +To support TPU VM pods, we represent entire TPU pods as "Ray Nodes", meaning +that TPU pods will need to run the operations specified in `CommandRunnerInterface` +N times, where N denotes the number of hosts that comprise a TPU pod. + +To maintain feature completeness, we simply wrap the existing `SSHCommandRunner` and +`DockerCommandRunner` and run them as batched calls. + +""" +import copy +from concurrent.futures import ThreadPoolExecutor +from types import ModuleType +from typing import Any, Dict, Optional + +from ray._private import ray_constants +from ray.autoscaler._private.command_runner import DockerCommandRunner, SSHCommandRunner +from ray.autoscaler._private.gcp.node import GCPTPUNode +from ray.autoscaler.command_runner import CommandRunnerInterface +from ray.autoscaler.node_provider import NodeProvider + + +class TPUVMSSHCommandRunner(SSHCommandRunner): + """An SSH command runner with overwritten IP address calls.""" + + def __init__( + self, + internal_ip: str, + external_ip: str, + worker_id: int, + accelerator_type: str, + *args, + **kwargs, + ): + self._internal_ip = internal_ip + self._external_ip = external_ip + self._worker_id = worker_id + self._accelerator_type = accelerator_type + super().__init__(*args, **kwargs) + + def _get_node_ip(self) -> str: + if self.use_internal_ip: + return self._internal_ip + else: + return self._external_ip + + def run( + self, + cmd, + timeout=120, + exit_on_fail=False, + port_forward=None, + with_output=False, + environment_variables: Dict[str, object] = None, + run_env="auto", # Unused argument. + ssh_options_override_ssh_key="", + shutdown_after_run=False, + ) -> str: + """Override the SSH run for TPU VM pods. + + Main functionality here we need to inject is to intercept the resources + provided by the node_provider TPU node type fillout. + + node_provider will provide a resource "TPU-{TPU_POD_TYPE}-head" which: + 1) allows application developers to target worker 0 of an arbitary TPU pod, and + 2) signals to the autoscaler how to address the demand for more TPU pods. + + Without this intercept, then all workers of a TPU pod will have the + "TPU-{TPU_POD_TYPE}-head" resource which will violate functionality (1) + above. + + """ + + if environment_variables: + environment_variables = _maybe_remove_head_resource( + environment_variables, self._worker_id, self._accelerator_type + ) + + return super().run( + cmd=cmd, + timeout=timeout, + exit_on_fail=exit_on_fail, + port_forward=port_forward, + with_output=with_output, + environment_variables=environment_variables, + run_env=run_env, + ssh_options_override_ssh_key=ssh_options_override_ssh_key, + shutdown_after_run=shutdown_after_run, + ) + + +class TPUVMDockerCommandRunner(DockerCommandRunner): + """A Docker command runner with overwritten IP addresses.""" + + def __init__( + self, + docker_config: Dict[str, Any], + internal_ip: str, + external_ip: str, + worker_id: int, + accelerator_type: str, + **common_args, + ): + super().__init__(docker_config=docker_config, **common_args) + self._worker_id = worker_id + self._accelerator_type = accelerator_type + + self.ssh_command_runner = TPUVMSSHCommandRunner( + internal_ip=internal_ip, + external_ip=external_ip, + worker_id=worker_id, + accelerator_type=accelerator_type, + **common_args, + ) + + def run( + self, + cmd, + timeout=120, + exit_on_fail=False, + port_forward=None, + with_output=False, + environment_variables: Optional[Dict[str, object]] = None, + run_env="auto", + ssh_options_override_ssh_key="", + shutdown_after_run=False, + ): + if environment_variables: + environment_variables = _maybe_remove_head_resource( + environment_variables, self._worker_id, self._accelerator_type + ) + return super().run( + cmd, + timeout, + exit_on_fail, + port_forward, + with_output, + environment_variables, + run_env, + ssh_options_override_ssh_key, + shutdown_after_run, + ) + + +class TPUCommandRunner(CommandRunnerInterface): + """A TPU pod command runner.""" + + def __init__( + self, + instance: GCPTPUNode, + log_prefix: str, + node_id: str, + auth_config: Dict[str, Any], + provider: NodeProvider, + cluster_name: str, + process_runner: ModuleType, + use_internal_ip: bool, + docker_config: Optional[Dict[str, Any]] = None, + ): + def create_command_runner( + worker_id: int, accelerator_type: str, internal_ip: str, external_ip: str + ) -> CommandRunnerInterface: + """Returns the correct base command runner.""" + + common_args = { + "internal_ip": internal_ip, + "external_ip": external_ip, + "worker_id": worker_id, + "accelerator_type": accelerator_type, + "log_prefix": "[tpu_worker_{}] ".format(worker_id) + log_prefix, + "node_id": node_id, + "provider": provider, + "auth_config": auth_config, + "cluster_name": cluster_name, + "process_runner": process_runner, + "use_internal_ip": use_internal_ip, + } + if docker_config and docker_config["container_name"] != "": + return TPUVMDockerCommandRunner( + docker_config=docker_config, **common_args + ) + else: + return TPUVMSSHCommandRunner(**common_args) + + self._command_runners = [] + self._num_workers = instance.num_workers + for i in range(self._num_workers): + self._command_runners.append( + create_command_runner( + worker_id=i, + accelerator_type=instance.get("acceleratorType"), + internal_ip=instance.get_internal_ip(i), + external_ip=instance.get_external_ip(i), + ) + ) + + @property + def num_connections(self) -> int: + """Return the number of active connections allowed at a time. + + We occasionally see issues where too many concurrent connections may lead to + failed SSH connections when there are too many TPU hosts. + + We utilize this property to cap the maximum number of active connections + at a time until a proper fix is found. + + """ + num_max_concurrent_active_connections = ray_constants.env_integer( + ray_constants.RAY_TPU_MAX_CONCURRENT_CONNECTIONS_ENV_VAR, default=16 + ) + return min(self._num_workers, num_max_concurrent_active_connections) + + def run( + self, + cmd, + timeout=120, + exit_on_fail=False, + port_forward=None, + with_output=False, + environment_variables: Dict[str, object] = None, + run_env="auto", # Unused argument. + ssh_options_override_ssh_key="", + shutdown_after_run=False, + ) -> str: + with ThreadPoolExecutor(self.num_connections) as executor: + results = executor.map( + lambda i: self._command_runners[i].run( + cmd=cmd, + timeout=timeout, + exit_on_fail=exit_on_fail, + port_forward=port_forward, + with_output=with_output, + environment_variables=copy.deepcopy(environment_variables), + run_env=run_env, + ssh_options_override_ssh_key=ssh_options_override_ssh_key, + shutdown_after_run=shutdown_after_run, + ), + range(self._num_workers), + ) + # Note: the `run` abstract function may return a string representing + # representing command output, but this result is rarely used - especially + # if the node is a worker (which a TPU pod is). + # We return only the results from worker 0 which may not always be expected. + return list(results)[0] + + def run_rsync_up(self, *args, **kwargs) -> None: + with ThreadPoolExecutor(self.num_connections) as executor: + executor.map( + lambda i: self._command_runners[i].run_rsync_up(*args, **kwargs), + range(self._num_workers), + ) + + def run_rsync_down(self, *args, **kwargs) -> None: + """Rsync files down from the cluster node. + + Args: + source: The (remote) source directory or file. + target: The (local) destination path. + """ + with ThreadPoolExecutor(self.num_connections) as executor: + executor.map( + lambda i: self._command_runners[i].run_rsync_down(*args, **kwargs), + range(self._num_workers), + ) + + def remote_shell_command_str(self) -> str: + """Return the command the user can use to open a shell.""" + # Note: this function is rarely used if the node is a worker. + # We return only the results from worker 0 which may not always be expected. + return self._command_runners[0].remote_shell_command_str() + + def run_init(self, *args, **kwargs) -> Optional[bool]: + """Used to run extra initialization commands. + + Args: + as_head: Run as head image or worker. + file_mounts: Files to copy to the head and worker nodes. + sync_run_yet: Whether sync has been run yet. + + Returns: + optional: Whether initialization is necessary. + """ + with ThreadPoolExecutor(self.num_connections) as executor: + results = executor.map( + lambda i: self._command_runners[i].run_init(*args, **kwargs), + range(self._num_workers), + ) + # Note: the `run_init` abstract function may return a bool representing + # whether initialization is necessary, but this result is rarely used - + # especially if the node is a worker (which a TPU pod is). + # Here we return whether any workers require initialization, which may not be + # the expected result. + return any(results) + + +def _maybe_remove_head_resource( + environment_variables: Dict[str, Any], worker_id: int, accelerator_type: str +): + """ + node_provider will provide a resource "TPU-{TPU_POD_TYPE}-head" which: + 1) allows application developers to target worker 0 of an arbitary TPU pod, and + 2) signals to the autoscaler how to address the demand for more TPU pods. + + Without this intercept, then all workers of a TPU pod will have the + "TPU-{TPU_POD_TYPE}-head" resource which will violate functionality (1) + above. + """ + resources = environment_variables.get( + ray_constants.RESOURCES_ENVIRONMENT_VARIABLE, None + ) + + if resources: + # For TPU pod support, we need to ensure that the + # tpu pod resource type only propagates to worker 0. + if worker_id != 0: + tpu_pod_resource_type = f"TPU-{accelerator_type}-head" + if tpu_pod_resource_type in resources: + resources = copy.copy(resources) + resources.pop(tpu_pod_resource_type, None) + environment_variables = { + **environment_variables, + ray_constants.RESOURCES_ENVIRONMENT_VARIABLE: resources, + } + + return environment_variables diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f688d19a7bfed62092d35d47a95551db1e8d1e9b Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/autoscaling_config.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/autoscaling_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e696af52e43c48af1a3ef382f52d7090a469663 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/autoscaling_config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa4044f78c2d1c2e55c484ef1b8e8df584813888 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/run_autoscaler.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/run_autoscaler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa21491fbc100e8e995d4e75d98d60516bc165db Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/run_autoscaler.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..11c498fae186becdb15eab611eef0d9c290fc79b Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/autoscaling_config.py b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/autoscaling_config.py new file mode 100644 index 0000000000000000000000000000000000000000..e61f453d87c825c266aa762d7e1176b518b12d33 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/autoscaling_config.py @@ -0,0 +1,528 @@ +import decimal +import json +import logging +import time +from itertools import chain +from typing import Any, Dict, Optional + +import requests + +from ray._private.label_utils import ( + validate_node_label_syntax, +) +from ray.autoscaler._private.constants import ( + DISABLE_LAUNCH_CONFIG_CHECK_KEY, + DISABLE_NODE_UPDATERS_KEY, + FOREGROUND_NODE_LAUNCH_KEY, + WORKER_LIVENESS_CHECK_KEY, +) +from ray.autoscaler._private.kuberay import node_provider, utils +from ray.autoscaler._private.util import validate_config + +logger = logging.getLogger(__name__) + +AUTOSCALER_OPTIONS_KEY = "autoscalerOptions" +IDLE_SECONDS_KEY = "idleTimeoutSeconds" +UPSCALING_KEY = "upscalingMode" +UPSCALING_VALUE_AGGRESSIVE = "Aggressive" +UPSCALING_VALUE_DEFAULT = "Default" +UPSCALING_VALUE_CONSERVATIVE = "Conservative" + +MAX_RAYCLUSTER_FETCH_TRIES = 5 +RAYCLUSTER_FETCH_RETRY_S = 5 + +GKE_TPU_TOPOLOGY_LABEL = "cloud.google.com/gke-tpu-topology" +GKE_TPU_ACCELERATOR_LABEL = "cloud.google.com/gke-tpu-accelerator" + +# Logical group name for the KubeRay head group. +# Used as the name of the "head node type" by the autoscaler. +_HEAD_GROUP_NAME = "headgroup" + + +class AutoscalingConfigProducer: + """Produces an autoscaling config by reading data from the RayCluster CR. + + Used to fetch the autoscaling config at the beginning of each autoscaler iteration. + + In the context of Ray deployment on Kubernetes, the autoscaling config is an + internal interface. + + The autoscaling config carries the strict subset of RayCluster CR data required by + the autoscaler to make scaling decisions; in particular, the autoscaling config does + not carry pod configuration data. + + This class is the only public object in this file. + """ + + def __init__(self, ray_cluster_name, ray_cluster_namespace): + self.kubernetes_api_client = node_provider.KubernetesHttpApiClient( + namespace=ray_cluster_namespace + ) + self._ray_cr_path = f"rayclusters/{ray_cluster_name}" + + def __call__(self): + ray_cr = self._fetch_ray_cr_from_k8s_with_retries() + autoscaling_config = _derive_autoscaling_config_from_ray_cr(ray_cr) + return autoscaling_config + + def _fetch_ray_cr_from_k8s_with_retries(self) -> Dict[str, Any]: + """Fetch the RayCluster CR by querying the K8s API server. + + Retry on HTTPError for robustness, in particular to protect autoscaler + initialization. + """ + for i in range(1, MAX_RAYCLUSTER_FETCH_TRIES + 1): + try: + return self.kubernetes_api_client.get(self._ray_cr_path) + except requests.HTTPError as e: + if i < MAX_RAYCLUSTER_FETCH_TRIES: + logger.exception( + "Failed to fetch RayCluster CR from K8s. Retrying." + ) + time.sleep(RAYCLUSTER_FETCH_RETRY_S) + else: + raise e from None + + # This branch is inaccessible. Raise to satisfy mypy. + raise AssertionError + + +def _derive_autoscaling_config_from_ray_cr(ray_cr: Dict[str, Any]) -> Dict[str, Any]: + provider_config = _generate_provider_config(ray_cr["metadata"]["namespace"]) + + available_node_types = _generate_available_node_types_from_ray_cr_spec( + ray_cr["spec"] + ) + + # The autoscaler expects a global max workers field. We set it to the sum of + # node type max workers. + global_max_workers = sum( + node_type["max_workers"] for node_type in available_node_types.values() + ) + + # Legacy autoscaling fields carry no information but are required for compatibility. + legacy_autoscaling_fields = _generate_legacy_autoscaling_config_fields() + + # Process autoscaler options. + autoscaler_options = ray_cr["spec"].get(AUTOSCALER_OPTIONS_KEY, {}) + if IDLE_SECONDS_KEY in autoscaler_options: + idle_timeout_minutes = autoscaler_options[IDLE_SECONDS_KEY] / 60.0 + else: + idle_timeout_minutes = 1.0 + + if autoscaler_options.get(UPSCALING_KEY) == UPSCALING_VALUE_CONSERVATIVE: + upscaling_speed = 1 # Rate-limit upscaling if "Conservative" is set by user. + # This elif is redudant but included for clarity. + elif autoscaler_options.get(UPSCALING_KEY) == UPSCALING_VALUE_DEFAULT: + upscaling_speed = 1000 # i.e. big, no rate-limiting by default + # This elif is redudant but included for clarity. + elif autoscaler_options.get(UPSCALING_KEY) == UPSCALING_VALUE_AGGRESSIVE: + upscaling_speed = 1000 + else: + upscaling_speed = 1000 + + autoscaling_config = { + "provider": provider_config, + "cluster_name": ray_cr["metadata"]["name"], + "head_node_type": _HEAD_GROUP_NAME, + "available_node_types": available_node_types, + "max_workers": global_max_workers, + # Should consider exposing `idleTimeoutMinutes` in the RayCluster CRD, + # under an `autoscaling` field. + "idle_timeout_minutes": idle_timeout_minutes, + # Should consider exposing `upscalingSpeed` in the RayCluster CRD, + # under an `autoscaling` field. + "upscaling_speed": upscaling_speed, + **legacy_autoscaling_fields, + } + + # Make sure the config is readable by the autoscaler. + validate_config(autoscaling_config) + + return autoscaling_config + + +def _generate_provider_config(ray_cluster_namespace: str) -> Dict[str, Any]: + """Generates the `provider` field of the autoscaling config, which carries data + required to instantiate the KubeRay node provider. + """ + return { + "type": "kuberay", + "namespace": ray_cluster_namespace, + DISABLE_NODE_UPDATERS_KEY: True, + DISABLE_LAUNCH_CONFIG_CHECK_KEY: True, + FOREGROUND_NODE_LAUNCH_KEY: True, + WORKER_LIVENESS_CHECK_KEY: False, + } + + +def _generate_legacy_autoscaling_config_fields() -> Dict[str, Any]: + """Generates legacy autoscaling config fields required for compatibiliy.""" + return { + "file_mounts": {}, + "cluster_synced_files": [], + "file_mounts_sync_continuously": False, + "initialization_commands": [], + "setup_commands": [], + "head_setup_commands": [], + "worker_setup_commands": [], + "head_start_ray_commands": [], + "worker_start_ray_commands": [], + "auth": {}, + } + + +def _generate_available_node_types_from_ray_cr_spec( + ray_cr_spec: Dict[str, Any], +) -> Dict[str, Any]: + """Formats autoscaler "available_node_types" field based on the Ray CR's group + specs. + """ + headGroupSpec = ray_cr_spec["headGroupSpec"] + return { + _HEAD_GROUP_NAME: _node_type_from_group_spec(headGroupSpec, is_head=True), + **{ + worker_group_spec["groupName"]: _node_type_from_group_spec( + worker_group_spec, is_head=False + ) + for worker_group_spec in ray_cr_spec["workerGroupSpecs"] + }, + } + + +def _node_type_from_group_spec( + group_spec: Dict[str, Any], is_head: bool +) -> Dict[str, Any]: + """Converts CR group spec to autoscaler node type.""" + if is_head: + # The head node type has no workers because the head is not a worker. + min_workers = max_workers = 0 + else: + # `minReplicas` and `maxReplicas` are required fields for each workerGroupSpec. + # numOfHosts specifies the number of workers per replica in KubeRay v1.1+. + min_workers = group_spec["minReplicas"] * group_spec.get("numOfHosts", 1) + max_workers = group_spec["maxReplicas"] * group_spec.get("numOfHosts", 1) + + resources = _get_ray_resources_from_group_spec(group_spec, is_head) + labels = _get_labels_from_group_spec(group_spec) + + node_type = { + "min_workers": min_workers, + "max_workers": max_workers, + # `node_config` is a legacy field required for compatibility. + # Pod config data is required by the operator but not by the autoscaler. + "node_config": {}, + "resources": resources, + "labels": labels, + } + + idle_timeout_s = group_spec.get(IDLE_SECONDS_KEY) + if idle_timeout_s is not None: + node_type["idle_timeout_s"] = float(idle_timeout_s) + + return node_type + + +def _get_ray_resources_from_group_spec( + group_spec: Dict[str, Any], is_head: bool +) -> Dict[str, int]: + """ + Infers Ray resources from group `Resources` field, rayStartCommands, or K8s limits. + The resources extracted are used in autoscaling calculations. + """ + # Set resources from top-level group 'Resources' field if it exists. + group_resources = group_spec.get("resources", {}) + + ray_start_params = group_spec.get("rayStartParams", {}) + # In KubeRay, Ray container is always the first application container of a Ray Pod. + k8s_resources = group_spec["template"]["spec"]["containers"][0].get("resources", {}) + group_name = _HEAD_GROUP_NAME if is_head else group_spec["groupName"] + + num_cpus = _get_num_cpus( + group_resources, ray_start_params, k8s_resources, group_name + ) + num_gpus = _get_num_gpus( + group_resources, ray_start_params, k8s_resources, group_name + ) + custom_resource_dict = _get_custom_resources( + group_resources, ray_start_params, group_name + ) + num_tpus = _get_num_tpus(group_resources, custom_resource_dict, k8s_resources) + memory = _get_memory(group_resources, ray_start_params, k8s_resources) + + # It's not allowed to use object store memory as a resource request, so we don't + # add that to the autoscaler's resources annotations. + + resources = {} + + assert isinstance(num_cpus, int) + resources["CPU"] = num_cpus + + if num_gpus is not None: + resources["GPU"] = num_gpus + + if num_tpus is not None: + # Add TPU Ray resource if not already added by ray_start_params, + # but specified in k8s_resource_limits. + if "TPU" not in custom_resource_dict: + resources["TPU"] = num_tpus + + """Add TPU head resource, similar to the GCP node_provider. + Sets the Ray resource TPU-{...}-head to ensure the Ray autoscaler + has sufficient resources to make scaling decisions. + TPU worker groups treat each TPU podslice as a replica, with `NumOfHosts` + specifying the number of workers per slice. Each replica of a TPU worker + group has one TPU head. + + For example, a v4-16 worker group with 2 replicas should have the following + resource labels on worker 0 of each replica: + worker 0: resources = {"TPU": 4, "TPU-v4-16-head": 1} + """ + if ( + "nodeSelector" in group_spec["template"]["spec"] + and GKE_TPU_TOPOLOGY_LABEL in group_spec["template"]["spec"]["nodeSelector"] + and GKE_TPU_ACCELERATOR_LABEL + in group_spec["template"]["spec"]["nodeSelector"] + ): + topology = group_spec["template"]["spec"]["nodeSelector"][ + GKE_TPU_TOPOLOGY_LABEL + ] + accelerator = group_spec["template"]["spec"]["nodeSelector"][ + GKE_TPU_ACCELERATOR_LABEL + ] + accelerator_type = utils.tpu_node_selectors_to_type(topology, accelerator) + if accelerator_type: + resources[f"TPU-{accelerator_type}-head"] = 1 + else: + logger.error( + f"Pods using TPUs require both `{GKE_TPU_TOPOLOGY_LABEL}` and `{GKE_TPU_ACCELERATOR_LABEL}` node selectors. " + "See https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/tpu.html#configuring-ray-pods-for-tpu-usage " + "and https://cloud.google.com/kubernetes-engine/docs/how-to/tpus." + ) + + if memory is not None: + resources["memory"] = memory + + resources.update(custom_resource_dict) + + return resources + + +def _get_labels_from_group_spec(group_spec: Dict[str, Any]) -> Dict[str, str]: + """ + Parses Ray node labels for the autoscaling config based on the following + priority: + 1. Top-level `labels` field in the group spec. + 2. `labels` field in `rayStartParams`. + """ + labels_dict = {} + + ray_start_params = group_spec.get("rayStartParams", {}) + labels_str = ray_start_params.get("labels") + if labels_str: + logger.warning( + f"Ignoring labels: {labels_str} set in rayStartParams. Group labels are supported in the top-level Labels field starting in KubeRay v1.5" + ) + + # Check for top-level structured Labels field. + if "labels" in group_spec and isinstance(group_spec.get("labels"), dict): + labels_dict = group_spec.get("labels") + # Validate node labels follow expected Kubernetes label syntax. + validate_node_label_syntax(labels_dict) + + return labels_dict + + +def _get_num_cpus( + group_resources: Dict[str, str], + ray_start_params: Dict[str, str], + k8s_resources: Dict[str, Dict[str, str]], + group_name: str, +) -> int: + """Get CPU annotation from `resources` field, ray_start_params or k8s_resources, + with priority for `resources` field. + """ + if "CPU" in group_resources: + if "num-cpus" in ray_start_params: + logger.warning( + f"'CPU' specified in both the top-level 'resources' field and in 'rayStartParams'. " + f"Using the value from 'resources': {group_resources['CPU']}." + ) + return _round_up_k8s_quantity(group_resources["CPU"]) + if "num-cpus" in ray_start_params: + return int(ray_start_params["num-cpus"]) + elif "cpu" in k8s_resources.get("limits", {}): + cpu_quantity: str = k8s_resources["limits"]["cpu"] + return _round_up_k8s_quantity(cpu_quantity) + elif "cpu" in k8s_resources.get("requests", {}): + cpu_quantity: str = k8s_resources["requests"]["cpu"] + return _round_up_k8s_quantity(cpu_quantity) + else: + # Getting the number of CPUs is important, so raise an error if we can't do it. + raise ValueError( + f"Autoscaler failed to detect `CPU` resources for group {group_name}." + "\nSet the `--num-cpus` rayStartParam and/or " + "the CPU resource limit for the Ray container." + ) + + +def _get_memory( + group_resources: Dict[str, str], + ray_start_params: Dict[str, str], + k8s_resources: Dict[str, Dict[str, str]], +) -> Optional[int]: + """Get memory resource annotation from `resources` field, ray_start_params or k8s_resources, + with priority for `resources` field. + """ + if "memory" in group_resources: + if "memory" in ray_start_params: + logger.warning( + f"'memory' specified in both the top-level 'resources' field and in 'rayStartParams'. " + f"Using the value from 'resources': {group_resources['memory']}." + ) + return _round_up_k8s_quantity(group_resources["memory"]) + if "memory" in ray_start_params: + return int(ray_start_params["memory"]) + elif "memory" in k8s_resources.get("limits", {}): + memory_quantity: str = k8s_resources["limits"]["memory"] + return _round_up_k8s_quantity(memory_quantity) + elif "memory" in k8s_resources.get("requests", {}): + memory_quantity: str = k8s_resources["requests"]["memory"] + return _round_up_k8s_quantity(memory_quantity) + return None + + +def _get_num_gpus( + group_resources: Dict[str, str], + ray_start_params: Dict[str, str], + k8s_resources: Dict[str, Dict[str, str]], + group_name: str, +) -> Optional[int]: + """Get GPU resource annotation from `resources` field, ray_start_params or k8s_resources, + with priority for `resources` field. + """ + if "GPU" in group_resources: + if "num-gpus" in ray_start_params: + logger.warning( + f"'GPU' specified in both the top-level 'resources' field and in 'rayStartParams'. " + f"Using the value from 'resources': {group_resources['GPU']}." + ) + return _round_up_k8s_quantity(group_resources["GPU"]) + elif "num-gpus" in ray_start_params: + return int(ray_start_params["num-gpus"]) + else: + for key, resource_quantity in chain( + k8s_resources.get("limits", {}).items(), + k8s_resources.get("requests", {}).items(), + ): + # e.g. nvidia.com/gpu + if key.endswith("gpu"): + # Typically, this is a string representing an interger, e.g. "1". + # Convert to int, making no assumptions on the resource_quantity, + # besides that it's valid as a K8s resource quantity. + num_gpus = _round_up_k8s_quantity(resource_quantity) + if num_gpus > 0: + # Only one GPU type supported for now, break out on first + # "/gpu" match. + return num_gpus + return None + + +def _get_num_tpus( + group_resources: Dict[str, str], + custom_resource_dict: Dict[str, int], + k8s_resources: Dict[str, Dict[str, str]], +) -> Optional[int]: + """Get TPU custom resource annotation from `resources` field, custom_resource_dict in ray_start_params, + or k8s_resources, with priority for `resources` field. + """ + if "TPU" in group_resources: + return _round_up_k8s_quantity(group_resources["TPU"]) + elif "TPU" in custom_resource_dict: + return custom_resource_dict["TPU"] + else: + for typ in ["limits", "requests"]: + tpu_resource_quantity = k8s_resources.get(typ, {}).get("google.com/tpu") + if tpu_resource_quantity is not None: + # Typically, this is a string representing an integer, e.g. "1". + # Convert to int, making no assumptions on the tpu_resource_quantity, + # besides that it's valid as a K8s resource quantity. + num_tpus = _round_up_k8s_quantity(tpu_resource_quantity) + if num_tpus > 0: + return num_tpus + return None + + +def _round_up_k8s_quantity(quantity: str) -> int: + """Rounds a Kubernetes resource quantity up to the nearest integer. + + Args: + quantity: Resource quantity as a string in the canonical K8s form. + + Returns: + The quantity, rounded up, as an integer. + """ + resource_decimal: decimal.Decimal = utils.parse_quantity(quantity) + rounded = resource_decimal.to_integral_value(rounding=decimal.ROUND_UP) + return int(rounded) + + +def _get_custom_resources( + group_resources: Dict[str, str], ray_start_params: Dict[str, Any], group_name: str +) -> Dict[str, int]: + """Format custom resources based on the group `resources` field or `resources` Ray start param. + + Currently, the value of the rayStartParam `resources` field must + be formatted as follows: + '"{\"Custom1\": 1, \"Custom2\": 5}"'. + + This method first converts the input to a correctly formatted + json string and then loads that json string to a dict. + """ + # If the top-level `resources` field is defined, use it as the exclusive source. + if group_resources: + if "resources" in ray_start_params: + logger.warning( + f"custom resources specified in both the top-level 'resources' field and in 'rayStartParams'. " + f"Using the values from 'resources': {group_resources}." + ) + standard_keys = {"CPU", "GPU", "TPU", "memory"} + try: + custom_resources = { + k: _round_up_k8s_quantity(v) + for k, v in group_resources.items() + if k not in standard_keys + } + except Exception as e: + logger.error( + f"Error reading `resource` for group {group_name}." + " For the correct format, refer to example configuration at " + "https://github.com/ray-project/ray/blob/master/python/" + "ray/autoscaler/kuberay/ray-cluster.complete.yaml." + ) + raise e + return custom_resources + + # Otherwise, check rayStartParams. + if "resources" not in ray_start_params: + return {} + resources_string = ray_start_params["resources"] + try: + # Drop the extra pair of quotes and remove the backslash escapes. + # resources_json should be a json string. + resources_json = resources_string[1:-1].replace("\\", "") + # Load a dict from the json string. + resources = json.loads(resources_json) + assert isinstance(resources, dict) + for key, value in resources.items(): + assert isinstance(key, str) + assert isinstance(value, int) + except Exception as e: + logger.error( + f"Error reading `resource` rayStartParam for group {group_name}." + " For the correct format, refer to example configuration at " + "https://github.com/ray-project/ray/blob/master/python/" + "ray/autoscaler/kuberay/ray-cluster.complete.yaml." + ) + raise e + return resources diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..986df93fb4a1bc1685efba4e72676ce6fd826b9b --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/node_provider.py @@ -0,0 +1,543 @@ +import datetime +import json +import logging +import os +from abc import ABC, abstractmethod +from collections import defaultdict +from typing import Any, Dict, List, Optional, Tuple + +import requests + +from ray._common.network_utils import build_address +from ray.autoscaler._private.constants import WORKER_LIVENESS_CHECK_KEY +from ray.autoscaler._private.util import NodeID, NodeIP, NodeKind, NodeStatus, NodeType +from ray.autoscaler.batching_node_provider import ( + BatchingNodeProvider, + NodeData, + ScaleRequest, +) +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + NODE_KIND_WORKER, + STATUS_UP_TO_DATE, + STATUS_UPDATE_FAILED, + TAG_RAY_USER_NODE_TYPE, +) + +# Key for KubeRay label that identifies a Ray pod as head or worker. +KUBERAY_LABEL_KEY_KIND = "ray.io/node-type" +# Key for KubeRay label that identifies the worker group (autoscaler node type) of a +# Ray pod. +KUBERAY_LABEL_KEY_TYPE = "ray.io/group" + +# These should be synced with: +# https://github.com/ray-project/kuberay/blob/f2d94ffe213dd8f69481b09c474047cb899fa73b/ray-operator/apis/ray/v1/raycluster_types.go#L165-L171 # noqa +# Kind label value indicating the pod is the head. +KUBERAY_KIND_HEAD = "head" +# Kind label value indicating the pod is the worker. +KUBERAY_KIND_WORKER = "worker" + +# KubeRay CRD version +KUBERAY_CRD_VER = os.getenv("KUBERAY_CRD_VER", "v1alpha1") + +KUBERAY_REQUEST_TIMEOUT_S = int(os.getenv("KUBERAY_REQUEST_TIMEOUT_S", 60)) + +RAY_HEAD_POD_NAME = os.getenv("RAY_HEAD_POD_NAME") + +# https://kubernetes.io/docs/tasks/run-application/access-api-from-pod +# While running in a Pod, your container can create an HTTPS URL for the +# Kubernetes API server by fetching the KUBERNETES_SERVICE_HOST and +# KUBERNETES_SERVICE_PORT_HTTPS environment variables. +KUBERNETES_SERVICE_HOST = os.getenv( + "KUBERNETES_SERVICE_HOST", "https://kubernetes.default" +) +KUBERNETES_SERVICE_PORT = os.getenv("KUBERNETES_SERVICE_PORT_HTTPS", "443") +KUBERNETES_HOST = build_address(KUBERNETES_SERVICE_HOST, KUBERNETES_SERVICE_PORT) +# Key for GKE label that identifies which multi-host replica a pod belongs to +REPLICA_INDEX_KEY = "replicaIndex" + +TOKEN_REFRESH_PERIOD = datetime.timedelta(minutes=1) + +# Design: + +# Each modification the autoscaler wants to make is posted to the API server goal state +# (e.g. if the autoscaler wants to scale up, it increases the number of +# replicas of the worker group it wants to scale, if it wants to scale down +# it decreases the number of replicas and adds the exact pods that should be +# terminated to the scaleStrategy). + +# KubeRayNodeProvider inherits from BatchingNodeProvider. +# Thus, the autoscaler's create and terminate requests are batched into a single +# Scale Request object which is submitted at the end of autoscaler update. +# KubeRay node provider converts the ScaleRequest into a RayCluster CR patch +# and applies the patch in the submit_scale_request method. + +# To reduce potential for race conditions, KubeRayNodeProvider +# aborts the autoscaler update if the operator has not yet processed workersToDelete - +# see KubeRayNodeProvider.safe_to_scale(). +# Once it is confirmed that workersToDelete have been cleaned up, KubeRayNodeProvider +# clears the workersToDelete list. + + +# Note: Log handlers set up in autoscaling monitor entrypoint. +logger = logging.getLogger(__name__) + + +def node_data_from_pod(pod: Dict[str, Any]) -> NodeData: + """Converts a Ray pod extracted from K8s into Ray NodeData. + NodeData is processed by BatchingNodeProvider. + """ + kind, type = kind_and_type(pod) + status = status_tag(pod) + ip = pod_ip(pod) + replica_index = _replica_index_label(pod) + return NodeData( + kind=kind, type=type, replica_index=replica_index, status=status, ip=ip + ) + + +def kind_and_type(pod: Dict[str, Any]) -> Tuple[NodeKind, NodeType]: + """Determine Ray node kind (head or workers) and node type (worker group name) + from a Ray pod's labels. + """ + labels = pod["metadata"]["labels"] + kind = ( + NODE_KIND_HEAD + if labels[KUBERAY_LABEL_KEY_KIND] == KUBERAY_KIND_HEAD + else NODE_KIND_WORKER + ) + type = labels[KUBERAY_LABEL_KEY_TYPE] + return kind, type + + +def _replica_index_label(pod: Dict[str, Any]) -> Optional[str]: + """Returns the replicaIndex label for a Pod in a multi-host TPU worker group. + The replicaIndex label is set by the GKE TPU Ray webhook and is of + the form {$WORKER_GROUP_NAME-$REPLICA_INDEX} where $REPLICA_INDEX + is an integer from 0 to Replicas-1. + """ + labels = pod["metadata"]["labels"] + return labels.get(REPLICA_INDEX_KEY, None) + + +def pod_ip(pod: Dict[str, Any]) -> NodeIP: + return pod["status"].get("podIP", "IP not yet assigned") + + +def status_tag(pod: Dict[str, Any]) -> NodeStatus: + """Convert pod state to Ray autoscaler node status. + + See the doc string of the class + batching_node_provider.NodeData for the semantics of node status. + """ + if ( + "containerStatuses" not in pod["status"] + or not pod["status"]["containerStatuses"] + ): + return "pending" + + state = pod["status"]["containerStatuses"][0]["state"] + + if "pending" in state: + return "pending" + if "running" in state: + return STATUS_UP_TO_DATE + if "waiting" in state: + return "waiting" + if "terminated" in state: + return STATUS_UPDATE_FAILED + raise ValueError("Unexpected container state.") + + +def worker_delete_patch(group_index: str, workers_to_delete: List[NodeID]): + path = f"/spec/workerGroupSpecs/{group_index}/scaleStrategy" + value = {"workersToDelete": workers_to_delete} + return replace_patch(path, value) + + +def worker_replica_patch(group_index: str, target_replicas: int): + path = f"/spec/workerGroupSpecs/{group_index}/replicas" + value = target_replicas + return replace_patch(path, value) + + +def replace_patch(path: str, value: Any) -> Dict[str, Any]: + return {"op": "replace", "path": path, "value": value} + + +def load_k8s_secrets() -> Tuple[Dict[str, str], str]: + """ + Loads secrets needed to access K8s resources. + + Returns: + headers: Headers with K8s access token + verify: Path to certificate + """ + with open("/var/run/secrets/kubernetes.io/serviceaccount/token") as secret: + token = secret.read() + + headers = { + "Authorization": "Bearer " + token, + } + verify = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + + return headers, verify + + +def url_from_resource( + namespace: str, + path: str, + kuberay_crd_version: str = KUBERAY_CRD_VER, + kubernetes_host: str = KUBERNETES_HOST, +) -> str: + """Convert resource path to REST URL for Kubernetes API server. + + Args: + namespace: The K8s namespace of the resource + path: The part of the resource path that starts with the resource type. + Supported resource types are "pods" and "rayclusters". + kuberay_crd_version: The API version of the KubeRay CRD. + Looks like "v1alpha1", "v1". + kubernetes_host: The host of the Kubernetes API server. + Uses $KUBERNETES_SERVICE_HOST and + $KUBERNETES_SERVICE_PORT to construct the kubernetes_host if not provided. + + When set by Kubernetes, + $KUBERNETES_SERVICE_HOST could be an IP address. That's why the https + scheme is added here. + + Defaults to "https://kubernetes.default:443". + """ + if kubernetes_host.startswith("http://"): + raise ValueError("Kubernetes host must be accessed over HTTPS.") + if not kubernetes_host.startswith("https://"): + kubernetes_host = "https://" + kubernetes_host + if path.startswith("pods"): + api_group = "/api/v1" + elif path.startswith("rayclusters"): + api_group = "/apis/ray.io/" + kuberay_crd_version + else: + raise NotImplementedError("Tried to access unknown entity at {}".format(path)) + return kubernetes_host + api_group + "/namespaces/" + namespace + "/" + path + + +def _worker_group_index(raycluster: Dict[str, Any], group_name: str) -> int: + """Extract worker group index from RayCluster.""" + group_names = [ + spec["groupName"] for spec in raycluster["spec"].get("workerGroupSpecs", []) + ] + return group_names.index(group_name) + + +def _worker_group_max_replicas( + raycluster: Dict[str, Any], group_index: int +) -> Optional[int]: + """Extract the maxReplicas of a worker group. + + If maxReplicas is unset, return None, to be interpreted as "no constraint". + At time of writing, it should be impossible for maxReplicas to be unset, but it's + better to handle this anyway. + """ + return raycluster["spec"]["workerGroupSpecs"][group_index].get("maxReplicas") + + +def _worker_group_replicas(raycluster: Dict[str, Any], group_index: int): + # 1 is the default replicas value used by the KubeRay operator + return raycluster["spec"]["workerGroupSpecs"][group_index].get("replicas", 1) + + +def _worker_group_num_of_hosts(raycluster: Dict[str, Any], group_index: int): + # 1 is the default numOfHosts value used by the KubeRay operator + return raycluster["spec"]["workerGroupSpecs"][group_index].get("numOfHosts", 1) + + +class IKubernetesHttpApiClient(ABC): + """ + An interface for a Kubernetes HTTP API client. + + This interface could be used to mock the Kubernetes API client in tests. + """ + + @abstractmethod + def get(self, path: str) -> Dict[str, Any]: + """Wrapper for REST GET of resource with proper headers.""" + pass + + @abstractmethod + def patch(self, path: str, payload: List[Dict[str, Any]]) -> Dict[str, Any]: + """Wrapper for REST PATCH of resource with proper headers.""" + pass + + +class KubernetesHttpApiClient(IKubernetesHttpApiClient): + def __init__(self, namespace: str, kuberay_crd_version: str = KUBERAY_CRD_VER): + self._kuberay_crd_version = kuberay_crd_version + self._namespace = namespace + self._token_expires_at = datetime.datetime.now() + TOKEN_REFRESH_PERIOD + self._headers, self._verify = None, None + + def _get_refreshed_headers_and_verify(self): + if (datetime.datetime.now() >= self._token_expires_at) or ( + self._headers is None or self._verify is None + ): + logger.info("Refreshing K8s API client token and certs.") + self._headers, self._verify = load_k8s_secrets() + self._token_expires_at = datetime.datetime.now() + TOKEN_REFRESH_PERIOD + return self._headers, self._verify + else: + return self._headers, self._verify + + def get(self, path: str) -> Dict[str, Any]: + """Wrapper for REST GET of resource with proper headers. + + Args: + path: The part of the resource path that starts with the resource type. + + Returns: + The JSON response of the GET request. + + Raises: + HTTPError: If the GET request fails. + """ + url = url_from_resource( + namespace=self._namespace, + path=path, + kuberay_crd_version=self._kuberay_crd_version, + ) + + headers, verify = self._get_refreshed_headers_and_verify() + result = requests.get( + url, + headers=headers, + timeout=KUBERAY_REQUEST_TIMEOUT_S, + verify=verify, + ) + if not result.status_code == 200: + result.raise_for_status() + return result.json() + + def patch(self, path: str, payload: List[Dict[str, Any]]) -> Dict[str, Any]: + """Wrapper for REST PATCH of resource with proper headers + + Args: + path: The part of the resource path that starts with the resource type. + payload: The JSON patch payload. + + Returns: + The JSON response of the PATCH request. + + Raises: + HTTPError: If the PATCH request fails. + """ + url = url_from_resource( + namespace=self._namespace, + path=path, + kuberay_crd_version=self._kuberay_crd_version, + ) + headers, verify = self._get_refreshed_headers_and_verify() + result = requests.patch( + url, + json.dumps(payload), + headers={**headers, "Content-type": "application/json-patch+json"}, + timeout=KUBERAY_REQUEST_TIMEOUT_S, + verify=verify, + ) + if not result.status_code == 200: + result.raise_for_status() + return result.json() + + +class KubeRayNodeProvider(BatchingNodeProvider): # type: ignore + def __init__( + self, + provider_config: Dict[str, Any], + cluster_name: str, + ): + logger.info("Creating KubeRayNodeProvider.") + self.namespace = provider_config["namespace"] + self.cluster_name = cluster_name + + self.k8s_api_client = KubernetesHttpApiClient(self.namespace) + + assert ( + provider_config.get(WORKER_LIVENESS_CHECK_KEY, True) is False + ), f"To use KubeRayNodeProvider, must set `{WORKER_LIVENESS_CHECK_KEY}:False`." + BatchingNodeProvider.__init__(self, provider_config, cluster_name) + + def get_node_data(self) -> Dict[NodeID, NodeData]: + """Queries K8s for pods in the RayCluster. Converts that pod data into a + map of pod name to Ray NodeData, as required by BatchingNodeProvider. + """ + # Store the raycluster CR + self._raycluster = self._get(f"rayclusters/{self.cluster_name}") + + # Get the pods resource version. + # Specifying a resource version in list requests is important for scalability: + # https://kubernetes.io/docs/reference/using-api/api-concepts/#semantics-for-get-and-list + resource_version = self._get_pods_resource_version() + if resource_version: + logger.info( + f"Listing pods for RayCluster {self.cluster_name}" + f" in namespace {self.namespace}" + f" at pods resource version >= {resource_version}." + ) + + # Filter pods by cluster_name. + label_selector = requests.utils.quote(f"ray.io/cluster={self.cluster_name}") + + resource_path = f"pods?labelSelector={label_selector}" + if resource_version: + resource_path += ( + f"&resourceVersion={resource_version}" + + "&resourceVersionMatch=NotOlderThan" + ) + + pod_list = self._get(resource_path) + fetched_resource_version = pod_list["metadata"]["resourceVersion"] + logger.info( + f"Fetched pod data at resource version" f" {fetched_resource_version}." + ) + + # Extract node data from the pod list. + node_data_dict = {} + for pod in pod_list["items"]: + # Kubernetes sets metadata.deletionTimestamp immediately after admitting a + # request to delete an object. Full removal of the object may take some time + # after the deletion timestamp is set. See link for details: + # https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-deletion + if "deletionTimestamp" in pod["metadata"]: + # Ignore pods marked for termination. + continue + pod_name = pod["metadata"]["name"] + node_data_dict[pod_name] = node_data_from_pod(pod) + return node_data_dict + + def submit_scale_request(self, scale_request: ScaleRequest): + """Converts the scale request generated by BatchingNodeProvider into + a patch that modifies the RayCluster CR's replicas and/or workersToDelete + fields. Then submits the patch to the K8s API server. + """ + # Transform the scale request into a patch payload. + patch_payload = self._scale_request_to_patch_payload( + scale_request, self._raycluster + ) + + # Submit the patch to K8s. + logger.info( + "Autoscaler is submitting the following patch to RayCluster " + f"{self.cluster_name} in namespace {self.namespace}." + ) + logger.info(patch_payload) + self._submit_raycluster_patch(patch_payload) + + def safe_to_scale(self) -> bool: + """Returns False iff non_terminated_nodes contains any pods in the RayCluster's + workersToDelete lists. + + Explanation: + If there are any workersToDelete which are non-terminated, + we should wait for the operator to do its job and delete those + pods. Therefore, we back off the autoscaler update. + + If, on the other hand, all of the workersToDelete have already been cleaned up, + then we patch away the workersToDelete lists and return True. + In the future, we may consider having the operator clean up workersToDelete + on it own: + https://github.com/ray-project/kuberay/issues/733 + + Note (Dmitri): + It is stylistically bad that this function has a side effect. + """ + # Get the list of nodes. + node_set = set(self.node_data_dict.keys()) + worker_groups = self._raycluster["spec"].get("workerGroupSpecs", []) + + # Accumulates the indices of worker groups with non-empty workersToDelete + non_empty_worker_group_indices = [] + + for group_index, worker_group in enumerate(worker_groups): + workersToDelete = worker_group.get("scaleStrategy", {}).get( + "workersToDelete", [] + ) + if workersToDelete: + non_empty_worker_group_indices.append(group_index) + for worker in workersToDelete: + if worker in node_set: + # The operator hasn't removed this worker yet. Abort + # the autoscaler update. + logger.warning(f"Waiting for operator to remove worker {worker}.") + return False + + # All required workersToDelete have been removed. + # Clean up the workersToDelete field. + patch_payload = [] + for group_index in non_empty_worker_group_indices: + patch = worker_delete_patch(group_index, workers_to_delete=[]) + patch_payload.append(patch) + if patch_payload: + logger.info("Cleaning up workers to delete.") + logger.info(f"Submitting patch {patch_payload}.") + self._submit_raycluster_patch(patch_payload) + + # It's safe to proceed with the autoscaler update. + return True + + def _get_pods_resource_version(self) -> str: + """ + Extract a recent pods resource version by reading the head pod's + metadata.resourceVersion of the response. + """ + if not RAY_HEAD_POD_NAME: + return None + pod_resp = self._get(f"pods/{RAY_HEAD_POD_NAME}") + return pod_resp["metadata"]["resourceVersion"] + + def _scale_request_to_patch_payload( + self, scale_request: ScaleRequest, raycluster: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """Converts autoscaler scale request into a RayCluster CR patch payload.""" + patch_payload = [] + # Collect patches for replica counts. + for node_type, target_replicas in scale_request.desired_num_workers.items(): + group_index = _worker_group_index(raycluster, node_type) + group_max_replicas = _worker_group_max_replicas(raycluster, group_index) + # Cap the replica count to maxReplicas. + if group_max_replicas is not None and group_max_replicas < target_replicas: + logger.warning( + "Autoscaler attempted to create " + + "more than maxReplicas pods of type {}.".format(node_type) + ) + target_replicas = group_max_replicas + # Check if we need to change the target count. + if target_replicas == _worker_group_replicas(raycluster, group_index): + # No patch required. + continue + # Need to patch replica count. Format the patch and add it to the payload. + patch = worker_replica_patch(group_index, target_replicas) + patch_payload.append(patch) + + # Maps node_type to nodes to delete for that group. + deletion_groups = defaultdict(list) + for worker in scale_request.workers_to_delete: + node_type = self.node_tags(worker)[TAG_RAY_USER_NODE_TYPE] + deletion_groups[node_type].append(worker) + + for node_type, workers_to_delete in deletion_groups.items(): + group_index = _worker_group_index(raycluster, node_type) + patch = worker_delete_patch(group_index, workers_to_delete) + patch_payload.append(patch) + + return patch_payload + + def _submit_raycluster_patch(self, patch_payload: List[Dict[str, Any]]): + """Submits a patch to modify a RayCluster CR.""" + path = "rayclusters/{}".format(self.cluster_name) + self._patch(path, patch_payload) + + def _get(self, path: str) -> Dict[str, Any]: + """Wrapper for REST GET of resource with proper headers.""" + return self.k8s_api_client.get(path) + + def _patch(self, path: str, payload: List[Dict[str, Any]]) -> Dict[str, Any]: + """Wrapper for REST PATCH of resource with proper headers.""" + return self.k8s_api_client.patch(path, payload) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/run_autoscaler.py b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/run_autoscaler.py new file mode 100644 index 0000000000000000000000000000000000000000..37b09db1f46a726c9889c75417476722b40da9e7 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/run_autoscaler.py @@ -0,0 +1,124 @@ +import logging +import os +import subprocess +import time + +import ray +from ray._common.network_utils import build_address +from ray._common.ray_constants import ( + LOGGING_ROTATE_BACKUP_COUNT, + LOGGING_ROTATE_BYTES, +) +from ray._common.utils import try_to_create_directory +from ray._private import ray_constants +from ray._private.ray_logging import setup_component_logger +from ray._private.services import get_node_ip_address +from ray._raylet import GcsClient +from ray.autoscaler._private.kuberay.autoscaling_config import AutoscalingConfigProducer +from ray.autoscaler._private.monitor import Monitor +from ray.autoscaler.v2.instance_manager.config import KubeRayConfigReader +from ray.autoscaler.v2.utils import is_autoscaler_v2 + +logger = logging.getLogger(__name__) + +BACKOFF_S = 5 + + +def _get_log_dir() -> str: + return os.path.join( + ray._common.utils.get_ray_temp_dir(), + ray._private.ray_constants.SESSION_LATEST, + "logs", + ) + + +def run_kuberay_autoscaler(cluster_name: str, cluster_namespace: str): + """Wait until the Ray head container is ready. Then start the autoscaler.""" + head_ip = get_node_ip_address() + ray_address = build_address(head_ip, 6379) + while True: + try: + # Autoscaler Ray version might not exactly match GCS version, so skip the + # version check when checking GCS status. + subprocess.check_call( + [ + "ray", + "health-check", + "--address", + ray_address, + "--skip-version-check", + ] + ) + logger.info("The Ray head is ready. Starting the autoscaler.") + break + except subprocess.CalledProcessError: + logger.warning( + f"The Ray head is not ready. Will check again in {BACKOFF_S} seconds." + ) + time.sleep(BACKOFF_S) + + # The Ray head container sets up the log directory. Thus, we set up logging + # only after the Ray head is ready. + _setup_logging() + + # autoscaling_config_producer reads the RayCluster CR from K8s and uses the CR + # to output an autoscaling config. + autoscaling_config_producer = AutoscalingConfigProducer( + cluster_name, cluster_namespace + ) + + gcs_client = GcsClient(ray_address) + if is_autoscaler_v2(fetch_from_server=True, gcs_client=gcs_client): + from ray.autoscaler.v2.monitor import AutoscalerMonitor as MonitorV2 + + MonitorV2( + address=gcs_client.address, + config_reader=KubeRayConfigReader(autoscaling_config_producer), + log_dir=_get_log_dir(), + monitor_ip=head_ip, + ).run() + else: + Monitor( + address=gcs_client.address, + # The `autoscaling_config` arg can be a dict or a `Callable: () -> dict`. + # In this case, it's a callable. + autoscaling_config=autoscaling_config_producer, + monitor_ip=head_ip, + # Let the autoscaler process exit after it hits 5 exceptions. + # (See ray.autoscaler._private.constants.AUTOSCALER_MAX_NUM_FAILURES.) + # Kubernetes will then restart the autoscaler container. + retry_on_failure=False, + ).run() + + +def _setup_logging() -> None: + """Log to autoscaler log file + (typically, /tmp/ray/session_latest/logs/monitor.*) + + Also log to pod stdout (logs viewable with `kubectl logs -c autoscaler`). + """ + log_dir = _get_log_dir() + # The director should already exist, but try (safely) to create it just in case. + try_to_create_directory(log_dir) + + # Write logs at info level to monitor.log. + setup_component_logger( + logging_level=ray_constants.LOGGER_LEVEL, + logging_format=ray_constants.LOGGER_FORMAT, + log_dir=log_dir, + filename=ray_constants.MONITOR_LOG_FILE_NAME, # monitor.log + max_bytes=LOGGING_ROTATE_BYTES, + backup_count=LOGGING_ROTATE_BACKUP_COUNT, + ) + + # For the autoscaler, the root logger _also_ needs to write to stderr, not just + # ray_constants.MONITOR_LOG_FILE_NAME. + level = logging.getLevelName(ray_constants.LOGGER_LEVEL.upper()) + stderr_handler = logging._StderrHandler() + stderr_handler.setFormatter(logging.Formatter(ray_constants.LOGGER_FORMAT)) + stderr_handler.setLevel(level) + logging.root.setLevel(level) + logging.root.addHandler(stderr_handler) + + # The stdout handler was set up in the Ray CLI entry point. + # See ray.scripts.scripts::cli(). diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/utils.py b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..24ec131dbea54ae59b655089611a14318db9c09a --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/kuberay/utils.py @@ -0,0 +1,110 @@ +# Source: +# https://github.com/kubernetes-client/python/blob/master/kubernetes/utils/quantity.py +from decimal import Decimal, InvalidOperation +from functools import reduce +from typing import Optional + +# Mapping used to get generation for TPU-{accelerator}-head resource +# https://cloud.google.com/kubernetes-engine/docs/how-to/tpus#run +gke_tpu_accelerator_to_generation = { + "tpu-v4-podslice": "v4", + "tpu-v5-lite-device": "v5e", + "tpu-v5-lite-podslice": "v5e", + "tpu-v5p-slice": "v5p", + "tpu-v6e-slice": "v6e", +} + + +def parse_quantity(quantity): + """Parse kubernetes canonical form quantity like 200Mi to a decimal number. + Supported SI suffixes: + base1024: Ki | Mi | Gi | Ti | Pi | Ei + base1000: n | u | m | "" | k | M | G | T | P | E + + See + https://github.com/kubernetes/apimachinery/blob/master/pkg/api/resource/quantity.go + + Args: + quantity: string. kubernetes canonical form quantity + + Returns: + Decimal: The parsed quantity as a decimal number + + Raises: + ValueError: On invalid or unknown input + """ + if isinstance(quantity, (int, float, Decimal)): + return Decimal(quantity) + + exponents = { + "n": -3, + "u": -2, + "m": -1, + "K": 1, + "k": 1, + "M": 2, + "G": 3, + "T": 4, + "P": 5, + "E": 6, + } + + quantity = str(quantity) + number = quantity + suffix = None + if len(quantity) >= 2 and quantity[-1] == "i": + if quantity[-2] in exponents: + number = quantity[:-2] + suffix = quantity[-2:] + elif len(quantity) >= 1 and quantity[-1] in exponents: + number = quantity[:-1] + suffix = quantity[-1:] + + try: + number = Decimal(number) + except InvalidOperation: + raise ValueError("Invalid number format: {}".format(number)) + + if suffix is None: + return number + + if suffix.endswith("i"): + base = 1024 + elif len(suffix) == 1: + base = 1000 + else: + raise ValueError("{} has unknown suffix".format(quantity)) + + # handle SI inconsistency + if suffix == "ki": + raise ValueError("{} has unknown suffix".format(quantity)) + + if suffix[0] not in exponents: + raise ValueError("{} has unknown suffix".format(quantity)) + + exponent = Decimal(exponents[suffix[0]]) + return number * (base**exponent) + + +def tpu_node_selectors_to_type(topology: str, accelerator: str) -> Optional[str]: + """Convert Kubernetes gke-tpu nodeSelectors to TPU accelerator_type + for a kuberay TPU worker group. + Args: + topology: value of the cloud.google.com/gke-tpu-topology Kubernetes + nodeSelector, describes the physical topology of the TPU podslice. + accelerator: value of the cloud.google.com/gke-tpu-accelerator nodeSelector, + the name of the TPU accelerator, e.g. tpu-v4-podslice + Returns: + A string, accelerator_type, e.g. "v4-8". + """ + if topology and accelerator: + generation = gke_tpu_accelerator_to_generation[accelerator] + # Reduce e.g. "2x2x2" to 8 + chip_dimensions = [int(chip_count) for chip_count in topology.split("x")] + num_chips = reduce(lambda x, y: x * y, chip_dimensions) + default_num_cores_per_chip = 1 + if generation == "v4" or generation == "v5p": + default_num_cores_per_chip = 2 + num_cores = num_chips * default_num_cores_per_chip + return f"{generation}-{num_cores}" + return None diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/legacy_info_string.py b/lib/python3.12/site-packages/ray/autoscaler/_private/legacy_info_string.py new file mode 100644 index 0000000000000000000000000000000000000000..830078345455218a005f7cbf734d0f7c88d93b98 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/legacy_info_string.py @@ -0,0 +1,37 @@ +import logging + +from ray._private.ray_constants import DEBUG_AUTOSCALING_STATUS_LEGACY +from ray.experimental.internal_kv import _internal_kv_initialized, _internal_kv_put + +"""This file provides legacy support for the old info string in order to +ensure the dashboard's `api/cluster_status` does not break backwards +compatibilty. +""" + +logger = logging.getLogger(__name__) + + +def legacy_log_info_string(autoscaler, nodes): + tmp = "Cluster status: " + tmp += info_string(autoscaler, nodes) + tmp += "\n" + tmp += autoscaler.load_metrics.info_string() + tmp += "\n" + tmp += autoscaler.resource_demand_scheduler.debug_string( + nodes, + autoscaler.pending_launches.breakdown(), + autoscaler.load_metrics.get_resource_utilization(), + ) + if _internal_kv_initialized(): + _internal_kv_put(DEBUG_AUTOSCALING_STATUS_LEGACY, tmp, overwrite=True) + logger.debug(tmp) + + +def info_string(autoscaler, nodes): + suffix = "" + if autoscaler.updaters: + suffix += " ({} updating)".format(len(autoscaler.updaters)) + if autoscaler.num_failed_updates: + suffix += " ({} failed to update)".format(len(autoscaler.num_failed_updates)) + + return "{} nodes{}".format(len(nodes), suffix) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/load_metrics.py b/lib/python3.12/site-packages/ray/autoscaler/_private/load_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..ec94647bda9c7666ad30a4ef3372b30b68eed609 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/load_metrics.py @@ -0,0 +1,372 @@ +import logging +import time +from collections import Counter +from functools import reduce +from typing import Dict, List + +from ray._private.gcs_utils import PlacementGroupTableData +from ray.autoscaler._private.constants import ( + AUTOSCALER_MAX_RESOURCE_DEMAND_VECTOR_SIZE, + AUTOSCALER_REPORT_PER_NODE_STATUS, +) +from ray.autoscaler._private.util import ( + DictCount, + LoadMetricsSummary, + NodeIP, + ResourceDict, +) +from ray.core.generated.common_pb2 import PlacementStrategy + +logger = logging.getLogger(__name__) + + +def add_resources(dict1: Dict[str, float], dict2: Dict[str, float]) -> Dict[str, float]: + """Add the values in two dictionaries. + + Returns: + dict: A new dictionary (inputs remain unmodified). + """ + new_dict = dict1.copy() + for k, v in dict2.items(): + new_dict[k] = v + new_dict.get(k, 0) + return new_dict + + +def freq_of_dicts(dicts: List[Dict], serializer=None, deserializer=dict) -> DictCount: + """Count a list of dictionaries (or unhashable types). + + This is somewhat annoying because mutable data structures aren't hashable, + and set/dict keys must be hashable. + + Args: + dicts (List[D]): A list of dictionaries to be counted. + serializer (D -> S): A custom serialization function. The output type S + must be hashable. The default serializer converts a dictionary into + a frozenset of KV pairs. + deserializer (S -> U): A custom deserialization function. See the + serializer for information about type S. For dictionaries U := D. + + Returns: + List[Tuple[U, int]]: Returns a list of tuples. Each entry in the list + is a tuple containing a unique entry from `dicts` and its + corresponding frequency count. + """ + if serializer is None: + serializer = lambda d: frozenset(d.items()) # noqa: E731 + + freqs = Counter(serializer(d) for d in dicts) + as_list = [] + for as_set, count in freqs.items(): + as_list.append((deserializer(as_set), count)) + return as_list + + +class LoadMetrics: + """Container for cluster load metrics. + + Metrics here are updated from raylet heartbeats. The autoscaler + queries these metrics to determine when to scale up, and which nodes + can be removed. + """ + + def __init__(self): + self.last_heartbeat_time_by_ip = {} + self.static_resources_by_ip = {} + self.dynamic_resources_by_ip = {} + self.node_id_by_ip = {} + self.waiting_bundles = [] + self.infeasible_bundles = [] + self.pending_placement_groups = [] + self.resource_requests = [] + self.cluster_full_of_actors_detected = False + self.ray_nodes_last_used_time_by_ip = {} + + def __bool__(self): + """A load metrics instance is Falsey iff the autoscaler process + has not received a resource message from the GCS. + """ + return bool(self.node_id_by_ip) + + def update( + self, + ip: str, + node_id: bytes, + static_resources: Dict[str, Dict], + dynamic_resources: Dict[str, Dict], + node_idle_duration_s: float, + waiting_bundles: List[Dict[str, float]] = None, + infeasible_bundles: List[Dict[str, float]] = None, + pending_placement_groups: List[PlacementGroupTableData] = None, + cluster_full_of_actors_detected: bool = False, + ): + self.static_resources_by_ip[ip] = static_resources + self.node_id_by_ip[ip] = node_id + self.cluster_full_of_actors_detected = cluster_full_of_actors_detected + + if not waiting_bundles: + waiting_bundles = [] + if not infeasible_bundles: + infeasible_bundles = [] + if not pending_placement_groups: + pending_placement_groups = [] + + # We are not guaranteed to have a corresponding dynamic resource + # for every static resource because dynamic resources are based on + # the available resources in the heartbeat, which does not exist + # if it is zero. Thus, we have to update dynamic resources here. + dynamic_resources_update = dynamic_resources.copy() + for resource_name, capacity in self.static_resources_by_ip[ip].items(): + if resource_name not in dynamic_resources_update: + dynamic_resources_update[resource_name] = 0.0 + self.dynamic_resources_by_ip[ip] = dynamic_resources_update + + now = time.time() + self.ray_nodes_last_used_time_by_ip[ip] = now - node_idle_duration_s + self.last_heartbeat_time_by_ip[ip] = now + self.waiting_bundles = waiting_bundles + self.infeasible_bundles = infeasible_bundles + self.pending_placement_groups = pending_placement_groups + + def mark_active(self, ip): + assert ip is not None, "IP should be known at this time" + logger.debug("Node {} is newly setup, treating as active".format(ip)) + self.last_heartbeat_time_by_ip[ip] = time.time() + + def prune_active_ips(self, active_ips: List[str]): + """The Raylet ips stored by LoadMetrics are obtained by polling + the GCS in Monitor.update_load_metrics(). + + On the other hand, the autoscaler gets a list of node ips from + its NodeProvider. + + This method removes from LoadMetrics the ips unknown to the autoscaler. + + Args: + active_ips (List[str]): The node ips known to the autoscaler. + """ + active_ips = set(active_ips) + + def prune(mapping, should_log): + unwanted_ips = set(mapping) - active_ips + for unwanted_ip in unwanted_ips: + if should_log: + logger.info("LoadMetrics: " f"Removed ip: {unwanted_ip}.") + del mapping[unwanted_ip] + if unwanted_ips and should_log: + logger.info( + "LoadMetrics: " + "Removed {} stale ip mappings: {} not in {}".format( + len(unwanted_ips), unwanted_ips, active_ips + ) + ) + assert not (unwanted_ips & set(mapping)) + + prune(self.ray_nodes_last_used_time_by_ip, should_log=True) + prune(self.static_resources_by_ip, should_log=False) + prune(self.node_id_by_ip, should_log=False) + prune(self.dynamic_resources_by_ip, should_log=False) + prune(self.last_heartbeat_time_by_ip, should_log=False) + + def get_node_resources(self): + """Return a list of node resources (static resource sizes). + + Example: + >>> from ray.autoscaler._private.load_metrics import LoadMetrics + >>> metrics = LoadMetrics(...) # doctest: +SKIP + >>> metrics.get_node_resources() # doctest: +SKIP + [{"CPU": 1}, {"CPU": 4, "GPU": 8}] # for two different nodes + """ + return self.static_resources_by_ip.values() + + def get_static_node_resources_by_ip(self) -> Dict[NodeIP, ResourceDict]: + """Return a dict of node resources for every node ip. + + Example: + >>> from ray.autoscaler._private.load_metrics import LoadMetrics + >>> metrics = LoadMetrics(...) # doctest: +SKIP + >>> metrics.get_static_node_resources_by_ip() # doctest: +SKIP + {127.0.0.1: {"CPU": 1}, 127.0.0.2: {"CPU": 4, "GPU": 8}} + """ + return self.static_resources_by_ip + + def get_resource_utilization(self): + return self.dynamic_resources_by_ip + + def _get_resource_usage(self): + resources_used = {} + resources_total = {} + for ip, max_resources in self.static_resources_by_ip.items(): + avail_resources = self.dynamic_resources_by_ip[ip] + for resource_id, amount in max_resources.items(): + used = amount - avail_resources[resource_id] + if resource_id not in resources_used: + resources_used[resource_id] = 0.0 + resources_total[resource_id] = 0.0 + resources_used[resource_id] += used + resources_total[resource_id] += amount + used = max(0, used) + + return resources_used, resources_total + + def get_resource_demand_vector(self, clip=True): + if clip: + # Bound the total number of bundles to + # 2xMAX_RESOURCE_DEMAND_VECTOR_SIZE. This guarantees the resource + # demand scheduler bin packing algorithm takes a reasonable amount + # of time to run. + return ( + self.waiting_bundles[:AUTOSCALER_MAX_RESOURCE_DEMAND_VECTOR_SIZE] + + self.infeasible_bundles[:AUTOSCALER_MAX_RESOURCE_DEMAND_VECTOR_SIZE] + ) + else: + return self.waiting_bundles + self.infeasible_bundles + + def get_resource_requests(self): + return self.resource_requests + + def get_pending_placement_groups(self): + return self.pending_placement_groups + + def resources_avail_summary(self) -> str: + """Return a concise string of cluster size to report to event logs. + + For example, "3 CPUs, 4 GPUs". + """ + total_resources = ( + reduce(add_resources, self.static_resources_by_ip.values()) + if self.static_resources_by_ip + else {} + ) + out = "{} CPUs".format(int(total_resources.get("CPU", 0))) + if "GPU" in total_resources: + out += ", {} GPUs".format(int(total_resources["GPU"])) + if "TPU" in total_resources: + out += ", {} TPUs".format(int(total_resources["TPU"])) + return out + + def summary(self): + available_resources = ( + reduce(add_resources, self.dynamic_resources_by_ip.values()) + if self.dynamic_resources_by_ip + else {} + ) + total_resources = ( + reduce(add_resources, self.static_resources_by_ip.values()) + if self.static_resources_by_ip + else {} + ) + usage_dict = {} + for key in total_resources: + if key in ["memory", "object_store_memory"]: + total = total_resources[key] + available = available_resources[key] + usage_dict[key] = (total - available, total) + else: + total = total_resources[key] + usage_dict[key] = (total - available_resources[key], total) + + summarized_demand_vector = freq_of_dicts( + self.get_resource_demand_vector(clip=False) + ) + summarized_resource_requests = freq_of_dicts(self.get_resource_requests()) + + def placement_group_serializer(pg): + bundles = tuple( + frozenset(bundle.unit_resources.items()) for bundle in pg.bundles + ) + return (bundles, pg.strategy) + + def placement_group_deserializer(pg_tuple): + # We marshal this as a dictionary so that we can easily json.dumps + # it later. + # TODO (Alex): Would there be a benefit to properly + # marshalling this (into a protobuf)? + bundles = list(map(dict, pg_tuple[0])) + return { + "bundles": freq_of_dicts(bundles), + "strategy": PlacementStrategy.Name(pg_tuple[1]), + } + + summarized_placement_groups = freq_of_dicts( + self.get_pending_placement_groups(), + serializer=placement_group_serializer, + deserializer=placement_group_deserializer, + ) + nodes_summary = freq_of_dicts(self.static_resources_by_ip.values()) + + usage_by_node = None + if AUTOSCALER_REPORT_PER_NODE_STATUS: + usage_by_node = {} + for ip, totals in self.static_resources_by_ip.items(): + available = self.dynamic_resources_by_ip.get(ip, {}) + usage_by_node[ip] = {} + for resource, total in totals.items(): + usage_by_node[ip][resource] = ( + total - available.get(resource, 0), + total, + ) + + return LoadMetricsSummary( + usage=usage_dict, + resource_demand=summarized_demand_vector, + pg_demand=summarized_placement_groups, + request_demand=summarized_resource_requests, + node_types=nodes_summary, + usage_by_node=usage_by_node, + ) + + def set_resource_requests(self, requested_resources): + if requested_resources is not None: + assert isinstance(requested_resources, list), requested_resources + self.resource_requests = [ + request for request in requested_resources if len(request) > 0 + ] + + def info_string(self): + return " - " + "\n - ".join( + ["{}: {}".format(k, v) for k, v in sorted(self._info().items())] + ) + + def _info(self): + resources_used, resources_total = self._get_resource_usage() + + now = time.time() + idle_times = [now - t for t in self.ray_nodes_last_used_time_by_ip.values()] + heartbeat_times = [now - t for t in self.last_heartbeat_time_by_ip.values()] + most_delayed_heartbeats = sorted( + self.last_heartbeat_time_by_ip.items(), key=lambda pair: pair[1] + )[:5] + most_delayed_heartbeats = {ip: (now - t) for ip, t in most_delayed_heartbeats} + + def format_resource(key, value): + if key in ["object_store_memory", "memory"]: + return "{} GiB".format(round(value / (1024 * 1024 * 1024), 2)) + else: + return round(value, 2) + + return { + "ResourceUsage": ", ".join( + [ + "{}/{} {}".format( + format_resource(rid, resources_used[rid]), + format_resource(rid, resources_total[rid]), + rid, + ) + for rid in sorted(resources_used) + if not rid.startswith("node:") + ] + ), + "NodeIdleSeconds": "Min={} Mean={} Max={}".format( + int(min(idle_times)) if idle_times else -1, + int(float(sum(idle_times)) / len(idle_times)) if idle_times else -1, + int(max(idle_times)) if idle_times else -1, + ), + "TimeSinceLastHeartbeat": "Min={} Mean={} Max={}".format( + int(min(heartbeat_times)) if heartbeat_times else -1, + int(float(sum(heartbeat_times)) / len(heartbeat_times)) + if heartbeat_times + else -1, + int(max(heartbeat_times)) if heartbeat_times else -1, + ), + "MostDelayedHeartbeats": most_delayed_heartbeats, + } diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/loader.py b/lib/python3.12/site-packages/ray/autoscaler/_private/loader.py new file mode 100644 index 0000000000000000000000000000000000000000..a46c40dd341f4f1e7ff09d728747db98a2a4ce70 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/loader.py @@ -0,0 +1,15 @@ +import importlib + + +def load_function_or_class(path): + """Load a function or class at runtime given a full path. + + Example of the path: mypkg.mysubpkg.myclass + """ + class_data = path.split(".") + if len(class_data) < 2: + raise ValueError("You need to pass a valid path like mymodule.provider_class") + module_path = ".".join(class_data[:-1]) + fn_or_class_str = class_data[-1] + module = importlib.import_module(module_path) + return getattr(module, fn_or_class_str) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/local/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/local/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c5ff864646b817e9afae70c93a4c561098420e0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/config.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fcd7909e2287df7857b761c087e455a122f29e8 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/coordinator_node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/coordinator_node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9d045a9895e4337e2a492a2be642f1261947dd4 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/coordinator_node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72c43010b512325a3a71aea57af902f07b6f0637 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/local/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/local/config.py b/lib/python3.12/site-packages/ray/autoscaler/_private/local/config.py new file mode 100644 index 0000000000000000000000000000000000000000..1d44bb3e3369c92964c1ac6ba9a14466d15b13d6 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/local/config.py @@ -0,0 +1,121 @@ +import copy +import os +from typing import Any, Dict + +from ray._common.utils import get_ray_temp_dir +from ray.autoscaler._private.cli_logger import cli_logger + +unsupported_field_message = "The field {} is not supported for on-premise clusters." + +LOCAL_CLUSTER_NODE_TYPE = "local.cluster.node" + + +def prepare_local(config: Dict[str, Any]) -> Dict[str, Any]: + """ + Prepare local cluster config for ingestion by cluster launcher and + autoscaler. + """ + config = copy.deepcopy(config) + for field in "head_node", "worker_nodes", "available_node_types": + if config.get(field): + err_msg = unsupported_field_message.format(field) + cli_logger.abort(err_msg) + # We use a config with a single node type for on-prem clusters. + # Resources internally detected by Ray are not overridden by the autoscaler + # (see NodeProvider.do_update) + config["available_node_types"] = { + LOCAL_CLUSTER_NODE_TYPE: {"node_config": {}, "resources": {}} + } + config["head_node_type"] = LOCAL_CLUSTER_NODE_TYPE + if "coordinator_address" in config["provider"]: + config = prepare_coordinator(config) + else: + config = prepare_manual(config) + return config + + +def prepare_coordinator(config: Dict[str, Any]) -> Dict[str, Any]: + config = copy.deepcopy(config) + # User should explicitly set the max number of workers for the coordinator + # to allocate. + if "max_workers" not in config: + cli_logger.abort( + "The field `max_workers` is required when using an " + "automatically managed on-premise cluster." + ) + node_type = config["available_node_types"][LOCAL_CLUSTER_NODE_TYPE] + # The autoscaler no longer uses global `min_workers`. + # Move `min_workers` to the node_type config. + node_type["min_workers"] = config.pop("min_workers", 0) + node_type["max_workers"] = config["max_workers"] + return config + + +def prepare_manual(config: Dict[str, Any]) -> Dict[str, Any]: + """Validates and sets defaults for configs of manually managed on-prem + clusters. + + - Checks for presence of required `worker_ips` and `head_ips` fields. + - Defaults min and max workers to the number of `worker_ips`. + - Caps min and max workers at the number of `worker_ips`. + - Writes min and max worker info into the single worker node type. + """ + config = copy.deepcopy(config) + if ("worker_ips" not in config["provider"]) or ( + "head_ip" not in config["provider"] + ): + cli_logger.abort( + "Please supply a `head_ip` and list of `worker_ips`. " + "Alternatively, supply a `coordinator_address`." + ) + num_ips = len(config["provider"]["worker_ips"]) + node_type = config["available_node_types"][LOCAL_CLUSTER_NODE_TYPE] + # Default to keeping all provided ips in the cluster. + config.setdefault("max_workers", num_ips) + + # The autoscaler no longer uses global `min_workers`. + # We will move `min_workers` to the node_type config. + min_workers = config.pop("min_workers", num_ips) + max_workers = config["max_workers"] + + if min_workers > num_ips: + cli_logger.warning( + f"The value of `min_workers` supplied ({min_workers}) is greater" + f" than the number of available worker ips ({num_ips})." + f" Setting `min_workers={num_ips}`." + ) + node_type["min_workers"] = num_ips + else: + node_type["min_workers"] = min_workers + + if max_workers > num_ips: + cli_logger.warning( + f"The value of `max_workers` supplied ({max_workers}) is greater" + f" than the number of available worker ips ({num_ips})." + f" Setting `max_workers={num_ips}`." + ) + node_type["max_workers"] = num_ips + config["max_workers"] = num_ips + else: + node_type["max_workers"] = max_workers + + if max_workers < num_ips: + cli_logger.warning( + f"The value of `max_workers` supplied ({max_workers}) is less" + f" than the number of available worker ips ({num_ips})." + f" At most {max_workers} Ray worker nodes will connect to the cluster." + ) + + return config + + +def get_lock_path(cluster_name: str) -> str: + return os.path.join(get_ray_temp_dir(), "cluster-{}.lock".format(cluster_name)) + + +def get_state_path(cluster_name: str) -> str: + return os.path.join(get_ray_temp_dir(), "cluster-{}.state".format(cluster_name)) + + +def bootstrap_local(config: Dict[str, Any]) -> Dict[str, Any]: + return config diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/local/coordinator_node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/local/coordinator_node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..7d55c394e8629124ea2a7a025813cc00def416ea --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/local/coordinator_node_provider.py @@ -0,0 +1,110 @@ +import json +import logging +from http.client import RemoteDisconnected + +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import TAG_RAY_CLUSTER_NAME + +logger = logging.getLogger(__name__) + + +class CoordinatorSenderNodeProvider(NodeProvider): + """NodeProvider for automatically managed private/local clusters. + + The cluster management is handled by a remote coordinating server. + The server listens on , therefore, the address + should be provided in the provider section in the cluster config. + The server receieves HTTP requests from this class and uses + LocalNodeProvider to get their responses. + """ + + def __init__(self, provider_config, cluster_name): + NodeProvider.__init__(self, provider_config, cluster_name) + self.coordinator_address = provider_config["coordinator_address"] + + def _get_http_response(self, request): + headers = { + "Content-Type": "application/json", + } + request_message = json.dumps(request).encode() + http_coordinator_address = "http://" + self.coordinator_address + + try: + import requests # `requests` is not part of stdlib. + from requests.exceptions import ConnectionError + + r = requests.get( + http_coordinator_address, + data=request_message, + headers=headers, + timeout=None, + ) + except (RemoteDisconnected, ConnectionError): + logger.exception( + "Could not connect to: " + + http_coordinator_address + + ". Did you run python coordinator_server.py" + + " --ips --host --port ?" + ) + raise + except ImportError: + logger.exception( + "Not all Ray Autoscaler dependencies were found. " + "In Ray 1.4+, the Ray CLI, autoscaler, and dashboard will " + 'only be usable via `pip install "ray[default]"`. Please ' + "update your install command." + ) + raise + + response = r.json() + return response + + def non_terminated_nodes(self, tag_filters): + # Only get the non terminated nodes associated with this cluster name. + tag_filters[TAG_RAY_CLUSTER_NAME] = self.cluster_name + request = {"type": "non_terminated_nodes", "args": (tag_filters,)} + return self._get_http_response(request) + + def is_running(self, node_id): + request = {"type": "is_running", "args": (node_id,)} + return self._get_http_response(request) + + def is_terminated(self, node_id): + request = {"type": "is_terminated", "args": (node_id,)} + return self._get_http_response(request) + + def node_tags(self, node_id): + request = {"type": "node_tags", "args": (node_id,)} + return self._get_http_response(request) + + def external_ip(self, node_id): + request = {"type": "external_ip", "args": (node_id,)} + response = self._get_http_response(request) + return response + + def internal_ip(self, node_id): + request = {"type": "internal_ip", "args": (node_id,)} + response = self._get_http_response(request) + return response + + def create_node(self, node_config, tags, count): + # Tag the newly created node with this cluster name. Helps to get + # the right nodes when calling non_terminated_nodes. + tags[TAG_RAY_CLUSTER_NAME] = self.cluster_name + request = { + "type": "create_node", + "args": (node_config, tags, count), + } + self._get_http_response(request) + + def set_node_tags(self, node_id, tags): + request = {"type": "set_node_tags", "args": (node_id, tags)} + self._get_http_response(request) + + def terminate_node(self, node_id): + request = {"type": "terminate_node", "args": (node_id,)} + self._get_http_response(request) + + def terminate_nodes(self, node_ids): + request = {"type": "terminate_nodes", "args": (node_ids,)} + self._get_http_response(request) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/local/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/local/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..4ac9fede40766994e89575af1744c968116faec5 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/local/node_provider.py @@ -0,0 +1,307 @@ +import json +import logging +import os +import socket +from threading import RLock + +from filelock import FileLock + +from ray.autoscaler._private.local.config import ( + LOCAL_CLUSTER_NODE_TYPE, + bootstrap_local, + get_lock_path, + get_state_path, +) +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + NODE_KIND_WORKER, + STATUS_UP_TO_DATE, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_NAME, + TAG_RAY_NODE_STATUS, + TAG_RAY_USER_NODE_TYPE, +) + +logger = logging.getLogger(__name__) + +filelock_logger = logging.getLogger("filelock") +filelock_logger.setLevel(logging.WARNING) + + +class ClusterState: + def __init__(self, lock_path, save_path, provider_config): + self.lock = RLock() + os.makedirs(os.path.dirname(lock_path), exist_ok=True) + self.file_lock = FileLock(lock_path) + self.save_path = save_path + + with self.lock: + with self.file_lock: + if os.path.exists(self.save_path): + workers = json.loads(open(self.save_path).read()) + head_config = workers.get(provider_config["head_ip"]) + if ( + not head_config + or head_config.get("tags", {}).get(TAG_RAY_NODE_KIND) + != NODE_KIND_HEAD + ): + workers = {} + logger.info("Head IP changed - recreating cluster.") + else: + workers = {} + logger.info( + "ClusterState: Loaded cluster state: {}".format(list(workers)) + ) + for worker_ip in provider_config["worker_ips"]: + if worker_ip not in workers: + workers[worker_ip] = { + "tags": {TAG_RAY_NODE_KIND: NODE_KIND_WORKER}, + "state": "terminated", + } + else: + assert ( + workers[worker_ip]["tags"][TAG_RAY_NODE_KIND] + == NODE_KIND_WORKER + ) + if provider_config["head_ip"] not in workers: + workers[provider_config["head_ip"]] = { + "tags": {TAG_RAY_NODE_KIND: NODE_KIND_HEAD}, + "state": "terminated", + } + else: + assert ( + workers[provider_config["head_ip"]]["tags"][TAG_RAY_NODE_KIND] + == NODE_KIND_HEAD + ) + # Relevant when a user reduces the number of workers + # without changing the headnode. + list_of_node_ips = list(provider_config["worker_ips"]) + list_of_node_ips.append(provider_config["head_ip"]) + for worker_ip in list(workers): + if worker_ip not in list_of_node_ips: + del workers[worker_ip] + + # Set external head ip, if provided by user. + # Necessary if calling `ray up` from outside the network. + # Refer to LocalNodeProvider.external_ip function. + external_head_ip = provider_config.get("external_head_ip") + if external_head_ip: + head = workers[provider_config["head_ip"]] + head["external_ip"] = external_head_ip + + assert len(workers) == len(provider_config["worker_ips"]) + 1 + with open(self.save_path, "w") as f: + logger.debug( + "ClusterState: Writing cluster state: {}".format(workers) + ) + f.write(json.dumps(workers)) + + def get(self): + with self.lock: + with self.file_lock: + workers = json.loads(open(self.save_path).read()) + return workers + + def put(self, worker_id, info): + assert "tags" in info + assert "state" in info + with self.lock: + with self.file_lock: + workers = self.get() + workers[worker_id] = info + with open(self.save_path, "w") as f: + logger.info( + "ClusterState: " + "Writing cluster state: {}".format(list(workers)) + ) + f.write(json.dumps(workers)) + + +class OnPremCoordinatorState(ClusterState): + """Generates & updates the state file of CoordinatorSenderNodeProvider. + + Unlike ClusterState, which generates a cluster specific file with + predefined head and worker ips, OnPremCoordinatorState overwrites + ClusterState's __init__ function to generate and manage a unified + file of the status of all the nodes for multiple clusters. + """ + + def __init__(self, lock_path, save_path, list_of_node_ips): + self.lock = RLock() + self.file_lock = FileLock(lock_path) + self.save_path = save_path + + with self.lock: + with self.file_lock: + if os.path.exists(self.save_path): + nodes = json.loads(open(self.save_path).read()) + else: + nodes = {} + logger.info( + "OnPremCoordinatorState: " + "Loaded on prem coordinator state: {}".format(nodes) + ) + + # Filter removed node ips. + for node_ip in list(nodes): + if node_ip not in list_of_node_ips: + del nodes[node_ip] + + for node_ip in list_of_node_ips: + if node_ip not in nodes: + nodes[node_ip] = { + "tags": {}, + "state": "terminated", + } + assert len(nodes) == len(list_of_node_ips) + with open(self.save_path, "w") as f: + logger.info( + "OnPremCoordinatorState: " + "Writing on prem coordinator state: {}".format(nodes) + ) + f.write(json.dumps(nodes)) + + +class LocalNodeProvider(NodeProvider): + """NodeProvider for private/local clusters. + + `node_id` is overloaded to also be `node_ip` in this class. + + When `cluster_name` is provided, it manages a single cluster in a cluster + specific state file. But when `cluster_name` is None, it manages multiple + clusters in a unified state file that requires each node to be tagged with + TAG_RAY_CLUSTER_NAME in create and non_terminated_nodes function calls to + associate each node with the right cluster. + + The current use case of managing multiple clusters is by + OnPremCoordinatorServer which receives node provider HTTP requests + from CoordinatorSenderNodeProvider and uses LocalNodeProvider to get + the responses. + """ + + def __init__(self, provider_config, cluster_name): + NodeProvider.__init__(self, provider_config, cluster_name) + + if cluster_name: + lock_path = get_lock_path(cluster_name) + state_path = get_state_path(cluster_name) + self.state = ClusterState( + lock_path, + state_path, + provider_config, + ) + self.use_coordinator = False + else: + # LocalNodeProvider with a coordinator server. + self.state = OnPremCoordinatorState( + "/tmp/coordinator.lock", + "/tmp/coordinator.state", + provider_config["list_of_node_ips"], + ) + self.use_coordinator = True + + def non_terminated_nodes(self, tag_filters): + workers = self.state.get() + matching_ips = [] + for worker_ip, info in workers.items(): + if info["state"] == "terminated": + continue + ok = True + for k, v in tag_filters.items(): + if info["tags"].get(k) != v: + ok = False + break + if ok: + matching_ips.append(worker_ip) + return matching_ips + + def is_running(self, node_id): + return self.state.get()[node_id]["state"] == "running" + + def is_terminated(self, node_id): + return not self.is_running(node_id) + + def node_tags(self, node_id): + return self.state.get()[node_id]["tags"] + + def external_ip(self, node_id): + """Returns an external ip if the user has supplied one. + Otherwise, use the same logic as internal_ip below. + + This can be used to call ray up from outside the network, for example + if the Ray cluster exists in an AWS VPC and we're interacting with + the cluster from a laptop (where using an internal_ip will not work). + + Useful for debugging the local node provider with cloud VMs.""" + + node_state = self.state.get()[node_id] + ext_ip = node_state.get("external_ip") + if ext_ip: + return ext_ip + else: + return socket.gethostbyname(node_id) + + def internal_ip(self, node_id): + return socket.gethostbyname(node_id) + + def set_node_tags(self, node_id, tags): + with self.state.lock: + with self.state.file_lock: + info = self.state.get()[node_id] + info["tags"].update(tags) + self.state.put(node_id, info) + + def create_node(self, node_config, tags, count): + """Creates min(count, currently available) nodes.""" + node_type = tags[TAG_RAY_NODE_KIND] + with self.state.lock: + with self.state.file_lock: + workers = self.state.get() + for node_id, info in workers.items(): + if info["state"] == "terminated" and ( + self.use_coordinator + or info["tags"][TAG_RAY_NODE_KIND] == node_type + ): + info["tags"] = tags + info["state"] = "running" + self.state.put(node_id, info) + count = count - 1 + if count == 0: + return + + def terminate_node(self, node_id): + workers = self.state.get() + info = workers[node_id] + info["state"] = "terminated" + self.state.put(node_id, info) + + @staticmethod + def bootstrap_config(cluster_config): + return bootstrap_local(cluster_config) + + +def record_local_head_state_if_needed(local_provider: LocalNodeProvider) -> None: + """This function is called on the Ray head from StandardAutoscaler.reset + to record the head node's own existence in the cluster state file. + + This is necessary because `provider.create_node` in + `commands.get_or_create_head_node` records the head state on the + cluster-launching machine but not on the head. + """ + head_ip = local_provider.provider_config["head_ip"] + cluster_name = local_provider.cluster_name + # If the head node is not marked as created in the cluster state file, + if head_ip not in local_provider.non_terminated_nodes({}): + # These tags are based on the ones in commands.get_or_create_head_node; + # keep in sync. + head_tags = { + TAG_RAY_NODE_KIND: NODE_KIND_HEAD, + TAG_RAY_USER_NODE_TYPE: LOCAL_CLUSTER_NODE_TYPE, + TAG_RAY_NODE_NAME: "ray-{}-head".format(cluster_name), + TAG_RAY_NODE_STATUS: STATUS_UP_TO_DATE, + } + # Mark the head node as created in the cluster state file. + local_provider.create_node(node_config={}, tags=head_tags, count=1) + + assert head_ip in local_provider.non_terminated_nodes({}) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/log_timer.py b/lib/python3.12/site-packages/ray/autoscaler/_private/log_timer.py new file mode 100644 index 0000000000000000000000000000000000000000..9e43d4692201c257c9971634c64633908a228f6e --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/log_timer.py @@ -0,0 +1,33 @@ +import datetime +import logging + +from ray.autoscaler._private.cli_logger import cli_logger + +logger = logging.getLogger(__name__) + + +class LogTimer: + def __init__(self, message, show_status=False): + self._message = message + self._show_status = show_status + + def __enter__(self): + self._start_time = datetime.datetime.utcnow() + + def __exit__(self, *error_vals): + if cli_logger.log_style != "record": + return + + td = datetime.datetime.utcnow() - self._start_time + status = "" + if self._show_status: + status = "failed" if any(error_vals) else "succeeded" + cli_logger.print( + " ".join( + [ + self._message, + status, + "[LogTimer={:.0f}ms]".format(td.total_seconds() * 1000), + ] + ) + ) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/monitor.py b/lib/python3.12/site-packages/ray/autoscaler/_private/monitor.py new file mode 100644 index 0000000000000000000000000000000000000000..a1ac85942b85ca009fb68ddc5b6b5f028ac916f7 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/monitor.py @@ -0,0 +1,750 @@ +"""Autoscaler monitoring loop daemon.""" + +import argparse +import json +import logging +import os +import signal +import sys +import time +import traceback +from collections import Counter +from dataclasses import asdict +from typing import Any, Callable, Dict, Optional, Union + +import ray +import ray._private.ray_constants as ray_constants +from ray._common.network_utils import build_address, parse_address +from ray._common.ray_constants import ( + LOGGING_ROTATE_BACKUP_COUNT, + LOGGING_ROTATE_BYTES, +) +from ray._private import logging_utils +from ray._private.event.event_logger import get_event_logger +from ray._private.ray_logging import setup_component_logger +from ray._raylet import GcsClient +from ray.autoscaler._private.autoscaler import StandardAutoscaler +from ray.autoscaler._private.commands import teardown_cluster +from ray.autoscaler._private.constants import ( + AUTOSCALER_MAX_RESOURCE_DEMAND_VECTOR_SIZE, + AUTOSCALER_METRIC_PORT, + AUTOSCALER_UPDATE_INTERVAL_S, + DISABLE_LAUNCH_CONFIG_CHECK_KEY, +) +from ray.autoscaler._private.event_summarizer import EventSummarizer +from ray.autoscaler._private.load_metrics import LoadMetrics +from ray.autoscaler._private.prom_metrics import AutoscalerPrometheusMetrics +from ray.autoscaler._private.util import format_readonly_node_type +from ray.autoscaler.v2.sdk import get_cluster_resource_state +from ray.core.generated import gcs_pb2 +from ray.core.generated.event_pb2 import Event as RayEvent +from ray.experimental.internal_kv import ( + _initialize_internal_kv, + _internal_kv_del, + _internal_kv_get, + _internal_kv_initialized, + _internal_kv_put, +) + +try: + import prometheus_client +except ImportError: + prometheus_client = None + + +logger = logging.getLogger(__name__) + + +def parse_resource_demands(resource_load_by_shape): + """Handle the message.resource_load_by_shape protobuf for the demand + based autoscaling. Catch and log all exceptions so this doesn't + interfere with the utilization based autoscaler until we're confident + this is stable. Worker queue backlogs are added to the appropriate + resource demand vector. + + Args: + resource_load_by_shape (pb2.gcs.ResourceLoad): The resource demands + in protobuf form or None. + + Returns: + List[ResourceDict]: Waiting bundles (ready and feasible). + List[ResourceDict]: Infeasible bundles. + """ + waiting_bundles, infeasible_bundles = [], [] + try: + for resource_demand_pb in list(resource_load_by_shape.resource_demands): + request_shape = dict(resource_demand_pb.shape) + for _ in range(resource_demand_pb.num_ready_requests_queued): + waiting_bundles.append(request_shape) + for _ in range(resource_demand_pb.num_infeasible_requests_queued): + infeasible_bundles.append(request_shape) + + # Infeasible and ready states for tasks are (logically) + # mutually exclusive. + if resource_demand_pb.num_infeasible_requests_queued > 0: + backlog_queue = infeasible_bundles + else: + backlog_queue = waiting_bundles + for _ in range(resource_demand_pb.backlog_size): + backlog_queue.append(request_shape) + if ( + len(waiting_bundles + infeasible_bundles) + > AUTOSCALER_MAX_RESOURCE_DEMAND_VECTOR_SIZE + ): + break + except Exception: + logger.exception("Failed to parse resource demands.") + + return waiting_bundles, infeasible_bundles + + +# Readonly provider config (e.g., for laptop mode, manually setup clusters). +BASE_READONLY_CONFIG = { + "cluster_name": "default", + "max_workers": 0, + "upscaling_speed": 1.0, + "docker": {}, + "idle_timeout_minutes": 0, + "provider": { + "type": "readonly", + "use_node_id_as_ip": True, # For emulated multi-node on laptop. + DISABLE_LAUNCH_CONFIG_CHECK_KEY: True, # No launch check. + }, + "auth": {}, + "available_node_types": { + "ray.head.default": {"resources": {}, "node_config": {}, "max_workers": 0} + }, + "head_node_type": "ray.head.default", + "file_mounts": {}, + "cluster_synced_files": [], + "file_mounts_sync_continuously": False, + "rsync_exclude": [], + "rsync_filter": [], + "initialization_commands": [], + "setup_commands": [], + "head_setup_commands": [], + "worker_setup_commands": [], + "head_start_ray_commands": [], + "worker_start_ray_commands": [], +} + + +class Monitor: + """Autoscaling monitor. + + This process periodically collects stats from the GCS and triggers + autoscaler updates. + """ + + def __init__( + self, + address: str, + autoscaling_config: Union[str, Callable[[], Dict[str, Any]]], + log_dir: str = None, + prefix_cluster_info: bool = False, + monitor_ip: Optional[str] = None, + retry_on_failure: bool = True, + ): + self.gcs_address = address + worker = ray._private.worker.global_worker + # TODO: eventually plumb ClusterID through to here + self.gcs_client = GcsClient(address=self.gcs_address) + + _initialize_internal_kv(self.gcs_client) + + if monitor_ip: + monitor_addr = build_address(monitor_ip, AUTOSCALER_METRIC_PORT) + self.gcs_client.internal_kv_put( + b"AutoscalerMetricsAddress", monitor_addr.encode(), True, None + ) + self._session_name = self.get_session_name(self.gcs_client) + logger.info(f"session_name: {self._session_name}") + worker.mode = 0 + head_node_ip = parse_address(self.gcs_address)[0] + + self.load_metrics = LoadMetrics() + self.last_avail_resources = None + self.event_summarizer = EventSummarizer() + self.prefix_cluster_info = prefix_cluster_info + self.retry_on_failure = retry_on_failure + self.autoscaling_config = autoscaling_config + self.autoscaler = None + # If set, we are in a manually created cluster (non-autoscaling) and + # simply mirroring what the GCS tells us the cluster node types are. + self.readonly_config = None + + if log_dir: + try: + self.event_logger = get_event_logger( + RayEvent.SourceType.AUTOSCALER, log_dir + ) + except Exception: + self.event_logger = None + else: + self.event_logger = None + + self.prom_metrics = AutoscalerPrometheusMetrics(session_name=self._session_name) + + if monitor_ip and prometheus_client: + # If monitor_ip wasn't passed in, then don't attempt to start the + # metric server to keep behavior identical to before metrics were + # introduced + try: + logger.info( + "Starting autoscaler metrics server on port {}".format( + AUTOSCALER_METRIC_PORT + ) + ) + kwargs = {"addr": "127.0.0.1"} if head_node_ip == "127.0.0.1" else {} + prometheus_client.start_http_server( + port=AUTOSCALER_METRIC_PORT, + registry=self.prom_metrics.registry, + **kwargs, + ) + + # Reset some gauges, since we don't know which labels have + # leaked if the autoscaler was restarted. + self.prom_metrics.pending_nodes.clear() + self.prom_metrics.active_nodes.clear() + except Exception: + logger.exception( + "An exception occurred while starting the metrics server." + ) + elif not prometheus_client: + logger.warning( + "`prometheus_client` not found, so metrics will not be exported." + ) + + logger.info("Monitor: Started") + + def _initialize_autoscaler(self): + if self.autoscaling_config: + autoscaling_config = self.autoscaling_config + else: + # This config mirrors the current setup of the manually created + # cluster. Each node gets its own unique node type. + self.readonly_config = BASE_READONLY_CONFIG + + # Note that the "available_node_types" of the config can change. + def get_latest_readonly_config(): + return self.readonly_config + + autoscaling_config = get_latest_readonly_config + self.autoscaler = StandardAutoscaler( + autoscaling_config, + self.load_metrics, + self.gcs_client, + self._session_name, + prefix_cluster_info=self.prefix_cluster_info, + event_summarizer=self.event_summarizer, + prom_metrics=self.prom_metrics, + ) + + def update_load_metrics(self): + """Fetches resource usage data from GCS and updates load metrics.""" + + # TODO(jinbum-kim): Still needed since some fields aren't in cluster_resource_state. + # Remove after v1 autoscaler fully migrates to get_cluster_resource_state(). + # ref: https://github.com/ray-project/ray/pull/57130 + response = self.gcs_client.get_all_resource_usage(timeout=60) + resources_batch_data = response.resource_usage_data + log_resource_batch_data_if_desired(resources_batch_data) + + # This is a workaround to get correct idle_duration_ms + # from "get_cluster_resource_state" + # ref: https://github.com/ray-project/ray/pull/48519#issuecomment-2481659346 + cluster_resource_state = get_cluster_resource_state(self.gcs_client) + ray_node_states = cluster_resource_state.node_states + ray_nodes_idle_duration_ms_by_id = { + node.node_id: node.idle_duration_ms for node in ray_node_states + } + + # Tell the readonly node provider what nodes to report. + if self.readonly_config: + new_nodes = [] + for msg in list(cluster_resource_state.node_states): + node_id = msg.node_id.hex() + new_nodes.append((node_id, msg.node_ip_address)) + self.autoscaler.provider._set_nodes(new_nodes) + + mirror_node_types = {} + legacy_cluster_full_detected = any( + getattr(entry, "cluster_full_of_actors_detected", False) + for entry in resources_batch_data.batch + ) + cluster_full = legacy_cluster_full_detected or getattr( + response, "cluster_full_of_actors_detected_by_gcs", False + ) + if ( + hasattr(response, "cluster_full_of_actors_detected_by_gcs") + and response.cluster_full_of_actors_detected_by_gcs + ): + # GCS has detected the cluster full of actors. + cluster_full = True + for resource_message in cluster_resource_state.node_states: + node_id = resource_message.node_id + # Generate node type config based on GCS reported node list. + if self.readonly_config: + # Keep prefix in sync with ReadonlyNodeProvider. + node_type = format_readonly_node_type(node_id.hex()) + resources = {} + for k, v in resource_message.total_resources.items(): + resources[k] = v + mirror_node_types[node_type] = { + "resources": resources, + "node_config": {}, + "max_workers": 1, + } + total_resources = dict(resource_message.total_resources) + available_resources = dict(resource_message.available_resources) + + waiting_bundles, infeasible_bundles = parse_resource_demands( + resources_batch_data.resource_load_by_shape + ) + + pending_placement_groups = list( + resources_batch_data.placement_group_load.placement_group_data + ) + + use_node_id_as_ip = self.autoscaler is not None and self.autoscaler.config[ + "provider" + ].get("use_node_id_as_ip", False) + + # "use_node_id_as_ip" is a hack meant to address situations in + # which there's more than one Ray node residing at a given ip. + # TODO (Dmitri): Stop using ips as node identifiers. + # https://github.com/ray-project/ray/issues/19086 + if use_node_id_as_ip: + peloton_id = total_resources.get("NODE_ID_AS_RESOURCE") + # Legacy support https://github.com/ray-project/ray/pull/17312 + if peloton_id is not None: + ip = str(int(peloton_id)) + else: + ip = node_id.hex() + else: + ip = resource_message.node_ip_address + + idle_duration_s = 0.0 + if node_id in ray_nodes_idle_duration_ms_by_id: + idle_duration_s = ray_nodes_idle_duration_ms_by_id[node_id] / 1000 + else: + logger.warning( + f"node_id {node_id} not found in ray_nodes_idle_duration_ms_by_id" + ) + + self.load_metrics.update( + ip, + node_id, + total_resources, + available_resources, + idle_duration_s, + waiting_bundles, + infeasible_bundles, + pending_placement_groups, + cluster_full, + ) + if self.readonly_config: + self.readonly_config["available_node_types"].update(mirror_node_types) + + def get_session_name(self, gcs_client: GcsClient) -> Optional[str]: + """Obtain the session name from the GCS. + + If the GCS doesn't respond, session name is considered None. + In this case, the metrics reported from the monitor won't have + the correct session name. + """ + if not _internal_kv_initialized(): + return None + + session_name = gcs_client.internal_kv_get( + b"session_name", + ray_constants.KV_NAMESPACE_SESSION, + timeout=10, + ) + + if session_name: + session_name = session_name.decode() + + return session_name + + def update_resource_requests(self): + """Fetches resource requests from the internal KV and updates load.""" + if not _internal_kv_initialized(): + return + data = _internal_kv_get( + ray._private.ray_constants.AUTOSCALER_RESOURCE_REQUEST_CHANNEL + ) + if data: + try: + resource_request = json.loads(data) + self.load_metrics.set_resource_requests(resource_request) + except Exception: + logger.exception("Error parsing resource requests") + + def _run(self): + """Run the monitor loop.""" + + while True: + try: + gcs_request_start_time = time.time() + self.update_load_metrics() + gcs_request_time = time.time() - gcs_request_start_time + self.update_resource_requests() + self.update_event_summary() + load_metrics_summary = self.load_metrics.summary() + status = { + "gcs_request_time": gcs_request_time, + "time": time.time(), + "monitor_pid": os.getpid(), + } + + if self.autoscaler and not self.load_metrics: + # load_metrics is Falsey iff we haven't collected any + # resource messages from the GCS, which can happen at startup if + # the GCS hasn't yet received data from the Raylets. + # In this case, do not do an autoscaler update. + # Wait to get load metrics. + logger.info( + "Autoscaler has not yet received load metrics. Waiting." + ) + elif self.autoscaler: + # Process autoscaling actions + update_start_time = time.time() + self.autoscaler.update() + status["autoscaler_update_time"] = time.time() - update_start_time + autoscaler_summary = self.autoscaler.summary() + try: + self.emit_metrics( + load_metrics_summary, + autoscaler_summary, + self.autoscaler.all_node_types, + ) + except Exception: + logger.exception("Error emitting metrics") + + if autoscaler_summary: + status["autoscaler_report"] = asdict(autoscaler_summary) + status[ + "non_terminated_nodes_time" + ] = ( + self.autoscaler.non_terminated_nodes.non_terminated_nodes_time # noqa: E501 + ) + + for msg in self.event_summarizer.summary(): + # Need to prefix each line of the message for the lines to + # get pushed to the driver logs. + for line in msg.split("\n"): + logger.info( + "{}{}".format( + ray_constants.LOG_PREFIX_EVENT_SUMMARY, line + ) + ) + if self.event_logger: + self.event_logger.info(line) + + self.event_summarizer.clear() + + status["load_metrics_report"] = asdict(load_metrics_summary) + as_json = json.dumps(status) + if _internal_kv_initialized(): + _internal_kv_put( + ray_constants.DEBUG_AUTOSCALING_STATUS, as_json, overwrite=True + ) + except Exception: + # By default, do not exit the monitor on failure. + if self.retry_on_failure: + logger.exception("Monitor: Execution exception. Trying again...") + else: + raise + + # Wait for a autoscaler update interval before processing the next + # round of messages. + time.sleep(AUTOSCALER_UPDATE_INTERVAL_S) + + def emit_metrics(self, load_metrics_summary, autoscaler_summary, node_types): + if autoscaler_summary is None: + return None + + for resource_name in ["CPU", "GPU", "TPU"]: + _, total = load_metrics_summary.usage.get(resource_name, (0, 0)) + pending = autoscaler_summary.pending_resources.get(resource_name, 0) + self.prom_metrics.cluster_resources.labels( + resource=resource_name, + SessionName=self.prom_metrics.session_name, + ).set(total) + self.prom_metrics.pending_resources.labels( + resource=resource_name, + SessionName=self.prom_metrics.session_name, + ).set(pending) + + pending_node_count = Counter() + for _, node_type, _ in autoscaler_summary.pending_nodes: + pending_node_count[node_type] += 1 + + for node_type, count in autoscaler_summary.pending_launches.items(): + pending_node_count[node_type] += count + + for node_type in node_types: + count = pending_node_count[node_type] + self.prom_metrics.pending_nodes.labels( + SessionName=self.prom_metrics.session_name, + NodeType=node_type, + ).set(count) + + for node_type in node_types: + count = autoscaler_summary.active_nodes.get(node_type, 0) + self.prom_metrics.active_nodes.labels( + SessionName=self.prom_metrics.session_name, + NodeType=node_type, + ).set(count) + + failed_node_counts = Counter() + for _, node_type in autoscaler_summary.failed_nodes: + failed_node_counts[node_type] += 1 + + # NOTE: This metric isn't reset with monitor resets. This means it will + # only be updated when the autoscaler' node tracker remembers failed + # nodes. If the node type failure is evicted from the autoscaler, the + # metric may not update for a while. + for node_type, count in failed_node_counts.items(): + self.prom_metrics.recently_failed_nodes.labels( + SessionName=self.prom_metrics.session_name, + NodeType=node_type, + ).set(count) + + def update_event_summary(self): + """Report the current size of the cluster. + + To avoid log spam, only cluster size changes (CPU, GPU or TPU count change) + are reported to the event summarizer. The event summarizer will report + only the latest cluster size per batch. + """ + avail_resources = self.load_metrics.resources_avail_summary() + if not self.readonly_config and avail_resources != self.last_avail_resources: + self.event_summarizer.add( + "Resized to {}.", # e.g., Resized to 100 CPUs, 4 GPUs, 4 TPUs. + quantity=avail_resources, + aggregate=lambda old, new: new, + ) + self.last_avail_resources = avail_resources + + def destroy_autoscaler_workers(self): + """Cleanup the autoscaler, in case of an exception in the run() method. + + We kill the worker nodes, but retain the head node in order to keep + logs around, keeping costs minimal. This monitor process runs on the + head node anyway, so this is more reliable.""" + + if self.autoscaler is None: + return # Nothing to clean up. + + if self.autoscaling_config is None: + # This is a logic error in the program. Can't do anything. + logger.error("Monitor: Cleanup failed due to lack of autoscaler config.") + return + + logger.info("Monitor: Exception caught. Taking down workers...") + clean = False + while not clean: + try: + teardown_cluster( + config_file=self.autoscaling_config, + yes=True, # Non-interactive. + workers_only=True, # Retain head node for logs. + override_cluster_name=None, + keep_min_workers=True, # Retain minimal amount of workers. + ) + clean = True + logger.info("Monitor: Workers taken down.") + except Exception: + logger.error("Monitor: Cleanup exception. Trying again...") + time.sleep(2) + + def _handle_failure(self, error): + if ( + self.autoscaler is not None + and os.environ.get("RAY_AUTOSCALER_FATESHARE_WORKERS", "") == "1" + ): + self.autoscaler.kill_workers() + # Take down autoscaler workers if necessary. + self.destroy_autoscaler_workers() + + # Something went wrong, so push an error to all current and future + # drivers. + message = f"The autoscaler failed with the following error:\n{error}" + if _internal_kv_initialized(): + _internal_kv_put( + ray_constants.DEBUG_AUTOSCALING_ERROR, message, overwrite=True + ) + from ray._private.utils import publish_error_to_driver + + publish_error_to_driver( + ray_constants.MONITOR_DIED_ERROR, + message, + gcs_client=self.gcs_client, + ) + + def _signal_handler(self, sig, frame): + try: + self._handle_failure( + f"Terminated with signal {sig}\n" + + "".join(traceback.format_stack(frame)) + ) + except Exception: + logger.exception("Monitor: Failure in signal handler.") + sys.exit(sig + 128) + + def run(self): + # Register signal handlers for autoscaler termination. + # Signals will not be received on windows + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGTERM, self._signal_handler) + try: + if _internal_kv_initialized(): + # Delete any previous autoscaling errors. + _internal_kv_del(ray_constants.DEBUG_AUTOSCALING_ERROR) + self._initialize_autoscaler() + self._run() + except Exception: + logger.exception("Error in monitor loop") + self._handle_failure(traceback.format_exc()) + raise + + +def log_resource_batch_data_if_desired( + resources_batch_data: gcs_pb2.ResourceUsageBatchData, +) -> None: + if os.getenv("AUTOSCALER_LOG_RESOURCE_BATCH_DATA") == "1": + logger.info("Logging raw resource message pulled from GCS.") + logger.info(resources_batch_data) + logger.info("Done logging raw resource message.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=("Parse GCS server for the monitor to connect to.") + ) + parser.add_argument( + "--gcs-address", required=False, type=str, help="The address (ip:port) of GCS." + ) + parser.add_argument( + "--autoscaling-config", + required=False, + type=str, + help="the path to the autoscaling config file", + ) + parser.add_argument( + "--logging-level", + required=False, + type=str, + default=ray_constants.LOGGER_LEVEL, + choices=ray_constants.LOGGER_LEVEL_CHOICES, + help=ray_constants.LOGGER_LEVEL_HELP, + ) + parser.add_argument( + "--logging-format", + required=False, + type=str, + default=ray_constants.LOGGER_FORMAT, + help=ray_constants.LOGGER_FORMAT_HELP, + ) + parser.add_argument( + "--logging-filename", + required=False, + type=str, + default=ray_constants.MONITOR_LOG_FILE_NAME, + help="Specify the name of log file, " + "log to stdout if set empty, default is " + f'"{ray_constants.MONITOR_LOG_FILE_NAME}"', + ) + parser.add_argument( + "--logs-dir", + required=True, + type=str, + help="Specify the path of the temporary directory used by Ray processes.", + ) + parser.add_argument( + "--logging-rotate-bytes", + required=False, + type=int, + default=LOGGING_ROTATE_BYTES, + help="Specify the max bytes for rotating " + "log file, default is " + f"{LOGGING_ROTATE_BYTES} bytes.", + ) + parser.add_argument( + "--logging-rotate-backup-count", + required=False, + type=int, + default=LOGGING_ROTATE_BACKUP_COUNT, + help="Specify the backup count of rotated log file, default is " + f"{LOGGING_ROTATE_BACKUP_COUNT}.", + ) + parser.add_argument( + "--monitor-ip", + required=False, + type=str, + default=None, + help="The IP address of the machine hosting the monitor process.", + ) + parser.add_argument( + "--stdout-filepath", + required=False, + type=str, + default="", + help="The filepath to dump monitor stdout.", + ) + parser.add_argument( + "--stderr-filepath", + required=False, + type=str, + default="", + help="The filepath to dump monitor stderr.", + ) + + args = parser.parse_args() + + # Disable log rotation for windows, because NTFS doesn't allow file deletion when there're multiple owners or borrowers, which happens to be how ray accesses log files. + logging_rotation_bytes = args.logging_rotate_bytes if sys.platform != "win32" else 0 + logging_rotation_backup_count = ( + args.logging_rotate_backup_count if sys.platform != "win32" else 1 + ) + setup_component_logger( + logging_level=args.logging_level, + logging_format=args.logging_format, + log_dir=args.logs_dir, + filename=args.logging_filename, + max_bytes=logging_rotation_bytes, + backup_count=logging_rotation_backup_count, + ) + + # Setup stdout/stderr redirect files if redirection enabled. + logging_utils.redirect_stdout_stderr_if_needed( + args.stdout_filepath, + args.stderr_filepath, + logging_rotation_bytes, + logging_rotation_backup_count, + ) + + logger.info(f"Starting monitor using ray installation: {ray.__file__}") + logger.info(f"Ray version: {ray.__version__}") + logger.info(f"Ray commit: {ray.__commit__}") + logger.info(f"Monitor started with command: {sys.argv}") + + if args.autoscaling_config: + autoscaling_config = os.path.expanduser(args.autoscaling_config) + else: + autoscaling_config = None + + bootstrap_address = args.gcs_address + if bootstrap_address is None: + raise ValueError("--gcs-address must be set!") + + monitor = Monitor( + bootstrap_address, + autoscaling_config, + log_dir=args.logs_dir, + monitor_ip=args.monitor_ip, + ) + + monitor.run() diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/node_launcher.py b/lib/python3.12/site-packages/ray/autoscaler/_private/node_launcher.py new file mode 100644 index 0000000000000000000000000000000000000000..a457a7703c1c43ff65feba155044bf83b7b71ae0 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/node_launcher.py @@ -0,0 +1,221 @@ +import copy +import logging +import operator +import threading +import time +import traceback +from typing import Any, Dict, Optional + +from ray.autoscaler._private.node_provider_availability_tracker import ( + NodeProviderAvailabilityTracker, +) +from ray.autoscaler._private.prom_metrics import AutoscalerPrometheusMetrics +from ray.autoscaler._private.util import hash_launch_conf +from ray.autoscaler.node_launch_exception import NodeLaunchException +from ray.autoscaler.tags import ( + NODE_KIND_WORKER, + STATUS_UNINITIALIZED, + TAG_RAY_LAUNCH_CONFIG, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_NAME, + TAG_RAY_NODE_STATUS, + TAG_RAY_USER_NODE_TYPE, +) + +logger = logging.getLogger(__name__) + + +class BaseNodeLauncher: + """Launches Ray nodes in the main thread using + `BaseNodeLauncher.launch_node()`. + + This is a superclass of NodeLauncher, which launches nodes asynchronously + in the background. + + By default, the subclass NodeLauncher is used to launch nodes in subthreads. + That behavior can be flagged off in the provider config by setting + `foreground_node_launch: True`; the autoscaler will then makes blocking calls to + BaseNodeLauncher.launch_node() in the main thread. + """ + + def __init__( + self, + provider, + pending, + event_summarizer, + node_provider_availability_tracker: NodeProviderAvailabilityTracker, + session_name: Optional[str] = None, + prom_metrics=None, + node_types=None, + index=None, + *args, + **kwargs, + ): + self.pending = pending + self.event_summarizer = event_summarizer + self.node_provider_availability_tracker = node_provider_availability_tracker + self.prom_metrics = prom_metrics or AutoscalerPrometheusMetrics( + session_name=session_name + ) + self.provider = provider + self.node_types = node_types + self.index = str(index) if index is not None else "" + + def launch_node( + self, config: Dict[str, Any], count: int, node_type: str + ) -> Optional[Dict]: + self.log("Got {} nodes to launch.".format(count)) + created_nodes = self._launch_node(config, count, node_type) + self.pending.dec(node_type, count) + return created_nodes + + def _launch_node( + self, config: Dict[str, Any], count: int, node_type: str + ) -> Optional[Dict]: + if self.node_types: + assert node_type, node_type + + # The `worker_nodes` field is deprecated in favor of per-node-type + # node_configs. We allow it for backwards-compatibility. + launch_config = copy.deepcopy(config.get("worker_nodes", {})) + if node_type: + launch_config.update( + config["available_node_types"][node_type]["node_config"] + ) + resources = copy.deepcopy( + config["available_node_types"][node_type]["resources"] + ) + labels = copy.deepcopy( + config["available_node_types"][node_type].get("labels", {}) + ) + launch_hash = hash_launch_conf(launch_config, config["auth"]) + node_config = copy.deepcopy(config.get("worker_nodes", {})) + node_tags = { + TAG_RAY_NODE_NAME: "ray-{}-worker".format(config["cluster_name"]), + TAG_RAY_NODE_KIND: NODE_KIND_WORKER, + TAG_RAY_NODE_STATUS: STATUS_UNINITIALIZED, + TAG_RAY_LAUNCH_CONFIG: launch_hash, + } + # A custom node type is specified; set the tag in this case, and also + # merge the configs. We merge the configs instead of overriding, so + # that the bootstrapped per-cloud properties are preserved. + # TODO(ekl) this logic is duplicated in commands.py (keep in sync) + if node_type: + node_tags[TAG_RAY_USER_NODE_TYPE] = node_type + node_config.update(launch_config) + + node_launch_start_time = time.time() + + error_msg = None + full_exception = None + created_nodes = {} + try: + created_nodes = self.provider.create_node_with_resources_and_labels( + node_config, node_tags, count, resources, labels + ) + except NodeLaunchException as node_launch_exception: + self.node_provider_availability_tracker.update_node_availability( + node_type, int(node_launch_start_time), node_launch_exception + ) + + if node_launch_exception.src_exc_info is not None: + full_exception = "\n".join( + traceback.format_exception(*node_launch_exception.src_exc_info) + ) + + error_msg = ( + f"Failed to launch {{}} node(s) of type {node_type}. " + f"({node_launch_exception.category}): " + f"{node_launch_exception.description}" + ) + except Exception: + error_msg = f"Failed to launch {{}} node(s) of type {node_type}." + full_exception = traceback.format_exc() + else: + # Record some metrics/observability information when a node is launched. + launch_time = time.time() - node_launch_start_time + for _ in range(count): + # Note: when launching multiple nodes we observe the time it + # took all nodes to launch for each node. For example, if 4 + # nodes were created in 25 seconds, we would observe the 25 + # second create time 4 times. + self.prom_metrics.worker_create_node_time.observe(launch_time) + self.prom_metrics.started_nodes.inc(count) + self.node_provider_availability_tracker.update_node_availability( + node_type=node_type, + timestamp=int(node_launch_start_time), + node_launch_exception=None, + ) + + if error_msg is not None: + self.event_summarizer.add( + error_msg, + quantity=count, + aggregate=operator.add, + ) + self.log(error_msg) + self.prom_metrics.node_launch_exceptions.inc() + self.prom_metrics.failed_create_nodes.inc(count) + else: + self.log("Launching {} nodes, type {}.".format(count, node_type)) + self.event_summarizer.add( + "Adding {} node(s) of type " + str(node_type) + ".", + quantity=count, + aggregate=operator.add, + ) + + if full_exception is not None: + self.log(full_exception) + + return created_nodes + + def log(self, statement): + # launcher_class is "BaseNodeLauncher", or "NodeLauncher" if called + # from that subclass. + launcher_class: str = type(self).__name__ + prefix = "{}{}:".format(launcher_class, self.index) + logger.info(prefix + " {}".format(statement)) + + +class NodeLauncher(BaseNodeLauncher, threading.Thread): + """Launches nodes asynchronously in the background.""" + + def __init__( + self, + provider, + queue, + pending, + event_summarizer, + node_provider_availability_tracker, + session_name: Optional[str] = None, + prom_metrics=None, + node_types=None, + index=None, + *thread_args, + **thread_kwargs, + ): + self.queue = queue + BaseNodeLauncher.__init__( + self, + provider=provider, + pending=pending, + event_summarizer=event_summarizer, + session_name=session_name, + node_provider_availability_tracker=node_provider_availability_tracker, + prom_metrics=prom_metrics, + node_types=node_types, + index=index, + ) + threading.Thread.__init__(self, *thread_args, **thread_kwargs) + + def run(self): + """Collects launch data from queue populated by StandardAutoscaler. + Launches nodes in a background thread. + + Overrides threading.Thread.run(). + NodeLauncher.start() executes this loop in a background thread. + """ + while True: + config, count, node_type = self.queue.get() + # launch_node is implemented in BaseNodeLauncher + self.launch_node(config, count, node_type) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/node_provider_availability_tracker.py b/lib/python3.12/site-packages/ray/autoscaler/_private/node_provider_availability_tracker.py new file mode 100644 index 0000000000000000000000000000000000000000..9126ab4b0fea9d0e90a041915ab7d21991dbde50 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/node_provider_availability_tracker.py @@ -0,0 +1,165 @@ +import threading +import time +from dataclasses import dataclass +from typing import Callable, Dict, Optional, Tuple + +from ray.autoscaler._private.constants import ( + AUTOSCALER_NODE_AVAILABILITY_MAX_STALENESS_S, +) +from ray.autoscaler.node_launch_exception import NodeLaunchException + + +@dataclass +class UnavailableNodeInformation: + category: str + description: str + + +@dataclass +class NodeAvailabilityRecord: + node_type: str + is_available: bool + last_checked_timestamp: float + unavailable_node_information: Optional[UnavailableNodeInformation] + + +@dataclass +class NodeAvailabilitySummary: + node_availabilities: Dict[ + str, NodeAvailabilityRecord + ] # Mapping from node type to node availability record. + + @classmethod + def from_fields(cls, **fields) -> Optional["NodeAvailabilitySummary"]: + """Implement marshalling from nested fields. pydantic isn't a core dependency + so we're implementing this by hand instead.""" + parsed = {} + + node_availabilites_dict = fields.get("node_availabilities", {}) + + for node_type, node_availability_record_dict in node_availabilites_dict.items(): + unavailable_information_dict = node_availability_record_dict.pop( + "unavailable_node_information", None + ) + unavaiable_information = None + if unavailable_information_dict is not None: + unavaiable_information = UnavailableNodeInformation( + **unavailable_information_dict + ) + + parsed[node_type] = NodeAvailabilityRecord( + unavailable_node_information=unavaiable_information, + **node_availability_record_dict, + ) + + return NodeAvailabilitySummary(node_availabilities=parsed) + + def __eq__(self, other: "NodeAvailabilitySummary"): + return self.node_availabilities == other.node_availabilities + + def __bool__(self) -> bool: + return bool(self.node_availabilities) + + +class NodeProviderAvailabilityTracker: + """A thread safe, TTL cache of node provider availability. We don't use + cachetools.TTLCache because it always sets the expiration time relative to + insertion time, but in our case, we want entries to expire relative to when + the node creation was attempted (and entries aren't necessarily added in + order). We want the entries to expire because the information grows stale + over time. + """ + + def __init__( + self, + timer: Callable[[], float] = time.time, + ttl: float = AUTOSCALER_NODE_AVAILABILITY_MAX_STALENESS_S, + ): + """A cache that tracks the availability of nodes and throw away + entries which have grown too stale. + + Args: + timer: A function that returns the current time in seconds. + ttl: The ttl from the insertion timestamp of an entry. + """ + self.timer = timer + self.ttl = ttl + # Mapping from node type to (eviction_time, record) + self.store: Dict[str, Tuple[float, NodeAvailabilityRecord]] = {} + # A global lock to simplify thread safety handling. + self.lock = threading.RLock() + + def _update_node_availability_requires_lock( + self, + node_type: str, + timestamp: int, + node_launch_exception: Optional[NodeLaunchException], + ) -> None: + if node_launch_exception is None: + record = NodeAvailabilityRecord( + node_type=node_type, + is_available=True, + last_checked_timestamp=timestamp, + unavailable_node_information=None, + ) + else: + info = UnavailableNodeInformation( + category=node_launch_exception.category, + description=node_launch_exception.description, + ) + record = NodeAvailabilityRecord( + node_type=node_type, + is_available=False, + last_checked_timestamp=timestamp, + unavailable_node_information=info, + ) + + expiration_time = timestamp + self.ttl + + # TODO (Alex): In theory it would be nice to make this dictionary + # ordered by expiration time, unfortunately that's a bit difficult + # since `update_node_availability` can be called with out of order + # timestamps. + self.store[node_type] = (expiration_time, record) + + self._remove_old_entries() + + def update_node_availability( + self, + node_type: str, + timestamp: int, + node_launch_exception: Optional[NodeLaunchException], + ) -> None: + """ + Update the availability and details of a single ndoe type. + + Args: + node_type: The node type. + timestamp: The timestamp that this information is accurate as of. + node_launch_exception: Details about why the node launch failed. If + empty, the node type will be considered available.""" + with self.lock: + self._update_node_availability_requires_lock( + node_type, timestamp, node_launch_exception + ) + + def summary(self) -> NodeAvailabilitySummary: + """ + Returns a summary of node availabilities and their staleness. + + Returns + A summary of node availabilities and their staleness. + """ + with self.lock: + self._remove_old_entries() + return NodeAvailabilitySummary( + {node_type: record for node_type, (_, record) in self.store.items()} + ) + + def _remove_old_entries(self): + """Remove any expired entries from the cache.""" + cur_time = self.timer() + with self.lock: + for key, (expiration_time, _) in list(self.store.items()): + if expiration_time < cur_time: + del self.store[key] diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/node_tracker.py b/lib/python3.12/site-packages/ray/autoscaler/_private/node_tracker.py new file mode 100644 index 0000000000000000000000000000000000000000..1b25117fc3bea6c81110fc9677ebd172e7eff789 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/node_tracker.py @@ -0,0 +1,77 @@ +from typing import List, Set, Tuple + +from ray.autoscaler._private import constants + + +class NodeTracker: + """Map nodes to their corresponding logs. + + We need to be a little careful here. At an given point in time, node_id <-> + ip can be interchangeably used, but the node_id -> ip relation is not + bijective _across time_ since IP addresses can be reused. Therefore, we + should treat node_id as the only unique identifier. + """ + + def __init__(self): + # Mapping from node_id -> (ip, node type, stdout_path, process runner) + self.node_mapping = {} + + # A quick, inefficient FIFO cache implementation. + self.lru_order = [] + + def _add_node_mapping(self, node_id: str, value: str): + if node_id in self.node_mapping: + return + + assert len(self.lru_order) == len(self.node_mapping) + if len(self.lru_order) >= constants.AUTOSCALER_MAX_NODES_TRACKED: + # The LRU eviction case + node_id = self.lru_order.pop(0) + del self.node_mapping[node_id] + + self.node_mapping[node_id] = value + self.lru_order.append(node_id) + + def track(self, node_id: str, ip: str, node_type: str): + """ + Begin to track a new node. + + Args: + node_id: The node id. + ip: The node ip address. + node_type: The node type. + """ + if node_id not in self.node_mapping: + self._add_node_mapping(node_id, (ip, node_type)) + + def untrack(self, node_id: str): + """Gracefully stop tracking a node. If a node is intentionally removed from + the cluster, we should stop tracking it so we don't mistakenly mark it + as failed. + + Args: + node_id: The node id which failed. + """ + if node_id in self.node_mapping: + self.lru_order.remove(node_id) + del self.node_mapping[node_id] + + def get_all_failed_node_info( + self, non_failed_ids: Set[str] + ) -> List[Tuple[str, str]]: + """Get the information about all failed nodes. A failed node is any node which + we began to track that is not pending or alive (i.e. not failed). + + Args: + non_failed_ids: Nodes are failed unless they are in this set. + + Returns: + List[Tuple[str, str]]: A list of tuples. Each tuple is the ip + address and type of a failed node. + """ + failed_nodes = self.node_mapping.keys() - non_failed_ids + failed_info = [] + # Returning the list in order is important for display purposes. + for node_id in filter(lambda node_id: node_id in failed_nodes, self.lru_order): + failed_info.append(self.node_mapping[node_id]) + return failed_info diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/prom_metrics.py b/lib/python3.12/site-packages/ray/autoscaler/_private/prom_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..a86407f3e030a54381961f33cd281b4479eff78f --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/prom_metrics.py @@ -0,0 +1,292 @@ +from typing import Optional + + +class NullMetric: + """Mock metric class to be used in case of prometheus_client import error.""" + + def set(self, *args, **kwargs): + pass + + def observe(self, *args, **kwargs): + pass + + def inc(self, *args, **kwargs): + pass + + def labels(self, *args, **kwargs): + return self + + def clear(self): + pass + + +try: + + from prometheus_client import CollectorRegistry, Counter, Gauge, Histogram + + # The metrics in this class should be kept in sync with + # python/ray/tests/test_metrics_agent.py + class AutoscalerPrometheusMetrics: + def __init__( + self, session_name: str = None, registry: Optional[CollectorRegistry] = None + ): + self.registry: CollectorRegistry = registry or CollectorRegistry( + auto_describe=True + ) + self._session_name = session_name + # Buckets: 5 seconds, 10 seconds, 20 seconds, 30 seconds, + # 45 seconds, 1 minute, 1.5 minutes, 2 minutes, + # 3 minutes, 4 minutes, 5 minutes, 6 minutes, + # 8 minutes, 10 minutes, 12 minutes, 15 minutes + # 20 minutes, 25 minutes, 30 minutes + # used for both worker launch time and worker update time + histogram_buckets = [ + 5, + 10, + 20, + 30, + 45, + 60, + 90, + 120, + 180, + 240, + 300, + 360, + 480, + 600, + 720, + 900, + 1200, + 1500, + 1800, + ] + # Buckets: .01 seconds to 1000 seconds. + # Used for autoscaler update time. + update_time_buckets = [0.01, 0.1, 1, 10, 100, 1000] + self.worker_create_node_time: Histogram = Histogram( + "worker_create_node_time_seconds", + "Worker launch time. This is the time it takes for a call to " + "a node provider's create_node method to return. Note that " + "when nodes are launched in batches, the launch time for that " + "batch will be observed once for *each* node in that batch. " + "For example, if 8 nodes are launched in 3 minutes, a launch " + "time of 3 minutes will be observed 8 times.", + labelnames=("SessionName",), + unit="seconds", + namespace="autoscaler", + registry=self.registry, + buckets=histogram_buckets, + ).labels(SessionName=session_name) + self.worker_update_time: Histogram = Histogram( + "worker_update_time_seconds", + "Worker update time. This is the time between when an updater " + "thread begins executing and when it exits successfully. This " + "metric only observes times for successful updates.", + labelnames=("SessionName",), + unit="seconds", + namespace="autoscaler", + registry=self.registry, + buckets=histogram_buckets, + ).labels(SessionName=session_name) + self.update_time: Histogram = Histogram( + "update_time", + "Autoscaler update time. This is the time for an autoscaler " + "update iteration to complete.", + labelnames=("SessionName",), + unit="seconds", + namespace="autoscaler", + registry=self.registry, + buckets=update_time_buckets, + ).labels(SessionName=session_name) + self.pending_nodes: Gauge = Gauge( + "pending_nodes", + "Number of nodes pending to be started.", + labelnames=( + "NodeType", + "SessionName", + ), + unit="nodes", + namespace="autoscaler", + registry=self.registry, + ) + self.active_nodes: Gauge = Gauge( + "active_nodes", + "Number of nodes in the cluster.", + labelnames=( + "NodeType", + "SessionName", + ), + unit="nodes", + namespace="autoscaler", + registry=self.registry, + ) + self.recently_failed_nodes = Gauge( + "recently_failed_nodes", + "The number of recently failed nodes. This count could reset " + "at undefined times.", + labelnames=( + "NodeType", + "SessionName", + ), + unit="nodes", + namespace="autoscaler", + registry=self.registry, + ) + self.started_nodes: Counter = Counter( + "started_nodes", + "Number of nodes started.", + labelnames=("SessionName",), + unit="nodes", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.stopped_nodes: Counter = Counter( + "stopped_nodes", + "Number of nodes stopped.", + labelnames=("SessionName",), + unit="nodes", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.updating_nodes: Gauge = Gauge( + "updating_nodes", + "Number of nodes in the process of updating.", + labelnames=("SessionName",), + unit="nodes", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.recovering_nodes: Gauge = Gauge( + "recovering_nodes", + "Number of nodes in the process of recovering.", + labelnames=("SessionName",), + unit="nodes", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.running_workers: Gauge = Gauge( + "running_workers", + "Number of worker nodes running.", + labelnames=("SessionName",), + unit="nodes", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.failed_create_nodes: Counter = Counter( + "failed_create_nodes", + "Number of nodes that failed to be created due to an " + "exception in the node provider's create_node method.", + labelnames=("SessionName",), + unit="nodes", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.failed_updates: Counter = Counter( + "failed_updates", + "Number of failed worker node updates.", + labelnames=("SessionName",), + unit="updates", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.successful_updates: Counter = Counter( + "successful_updates", + "Number of succesfful worker node updates.", + labelnames=("SessionName",), + unit="updates", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.failed_recoveries: Counter = Counter( + "failed_recoveries", + "Number of failed node recoveries.", + labelnames=("SessionName",), + unit="recoveries", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.successful_recoveries: Counter = Counter( + "successful_recoveries", + "Number of successful node recoveries.", + labelnames=("SessionName",), + unit="recoveries", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.update_loop_exceptions: Counter = Counter( + "update_loop_exceptions", + "Number of exceptions raised in the update loop of the autoscaler.", + labelnames=("SessionName",), + unit="exceptions", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.node_launch_exceptions: Counter = Counter( + "node_launch_exceptions", + "Number of exceptions raised while launching nodes.", + labelnames=("SessionName",), + unit="exceptions", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.reset_exceptions: Counter = Counter( + "reset_exceptions", + "Number of exceptions raised while resetting the autoscaler.", + labelnames=("SessionName",), + unit="exceptions", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.config_validation_exceptions: Counter = Counter( + "config_validation_exceptions", + "Number of exceptions raised while validating the config " + "during a reset.", + labelnames=("SessionName",), + unit="exceptions", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + self.drain_node_exceptions: Counter = Counter( + "drain_node_exceptions", + "Number of exceptions raised when making a DrainNode rpc" + "prior to node termination.", + labelnames=("SessionName",), + unit="exceptions", + namespace="autoscaler", + registry=self.registry, + ).labels(SessionName=session_name) + # This represents the autoscaler's view of essentially + # `ray.cluster_resources()`, it may be slightly different from the + # core metric from an eventual consistency perspective. + self.cluster_resources: Gauge = Gauge( + "cluster_resources", + "Total logical resources in the cluster.", + labelnames=("resource", "SessionName"), + unit="resources", + namespace="autoscaler", + registry=self.registry, + ) + # This represents the pending launches + nodes being set up for the + # autoscaler. + self.pending_resources: Gauge = Gauge( + "pending_resources", + "Pending logical resources in the cluster.", + labelnames=("resource", "SessionName"), + unit="resources", + namespace="autoscaler", + registry=self.registry, + ) + + @property + def session_name(self): + return self._session_name + +except ImportError: + + class AutoscalerPrometheusMetrics(object): + def __init__(self, session_name: str = None): + pass + + def __getattr__(self, attr): + return NullMetric() diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/providers.py b/lib/python3.12/site-packages/ray/autoscaler/_private/providers.py new file mode 100644 index 0000000000000000000000000000000000000000..cdcd7cd54cea72dcdf18b5ff46aab9a128a0262a --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/providers.py @@ -0,0 +1,299 @@ +import copy +import json +import logging +import os +from typing import Any, Dict + +import yaml + +from ray.autoscaler._private.loader import load_function_or_class + +logger = logging.getLogger(__name__) + +# For caching provider instantiations across API calls of one python session +_provider_instances = {} + +# Minimal config for compatibility with legacy-style external configs. +MINIMAL_EXTERNAL_CONFIG = { + "available_node_types": { + "ray.head.default": {}, + "ray.worker.default": {}, + }, + "head_node_type": "ray.head.default", + "head_node": {}, + "worker_nodes": {}, +} + + +def _import_aws(provider_config): + try: + # boto3 and botocore are imported in multiple places in the codebase, + # so we just import them here to ensure that they are installed. + import boto3 # noqa: F401 + except ImportError as e: + raise ImportError( + "The Ray AWS VM launcher requires the AWS SDK for Python (Boto3) " + "to be installed. You can install it with `pip install boto3`." + ) from e + + from ray.autoscaler._private.aws.node_provider import AWSNodeProvider + + return AWSNodeProvider + + +def _import_gcp(provider_config): + try: + import googleapiclient # noqa: F401 + except ImportError as e: + raise ImportError( + "The Ray GCP VM launcher requires the Google API Client to be installed. " + "You can install it with `pip install google-api-python-client`." + ) from e + + from ray.autoscaler._private.gcp.node_provider import GCPNodeProvider + + return GCPNodeProvider + + +def _import_azure(provider_config): + from ray.autoscaler._private._azure.node_provider import AzureNodeProvider + + return AzureNodeProvider + + +def _import_vsphere(provider_config): + from ray.autoscaler._private.vsphere.node_provider import VsphereWcpNodeProvider + + return VsphereWcpNodeProvider + + +def _import_local(provider_config): + if "coordinator_address" in provider_config: + from ray.autoscaler._private.local.coordinator_node_provider import ( + CoordinatorSenderNodeProvider, + ) + + return CoordinatorSenderNodeProvider + else: + from ray.autoscaler._private.local.node_provider import LocalNodeProvider + + return LocalNodeProvider + + +def _import_readonly(provider_config): + from ray.autoscaler._private.readonly.node_provider import ReadOnlyNodeProvider + + return ReadOnlyNodeProvider + + +def _import_fake_multinode(provider_config): + from ray.autoscaler._private.fake_multi_node.node_provider import ( + FakeMultiNodeProvider, + ) + + return FakeMultiNodeProvider + + +def _import_fake_multinode_docker(provider_config): + from ray.autoscaler._private.fake_multi_node.node_provider import ( + FakeMultiNodeDockerProvider, + ) + + return FakeMultiNodeDockerProvider + + +def _import_kuberay(provider_config): + from ray.autoscaler._private.kuberay.node_provider import KubeRayNodeProvider + + return KubeRayNodeProvider + + +def _import_aliyun(provider_config): + from ray.autoscaler._private.aliyun.node_provider import AliyunNodeProvider + + return AliyunNodeProvider + + +def _import_spark(provider_config): + from ray.autoscaler._private.spark.node_provider import SparkNodeProvider + + return SparkNodeProvider + + +def _load_fake_multinode_defaults_config(): + import ray.autoscaler._private.fake_multi_node as ray_fake_multinode + + return os.path.join(os.path.dirname(ray_fake_multinode.__file__), "example.yaml") + + +def _load_read_only_defaults_config(): + import ray.autoscaler._private.readonly as ray_readonly + + return os.path.join(os.path.dirname(ray_readonly.__file__), "defaults.yaml") + + +def _load_fake_multinode_docker_defaults_config(): + import ray.autoscaler._private.fake_multi_node as ray_fake_multinode + + return os.path.join( + os.path.dirname(ray_fake_multinode.__file__), "example_docker.yaml" + ) + + +def _load_local_defaults_config(): + import ray.autoscaler.local as ray_local + + return os.path.join(os.path.dirname(ray_local.__file__), "defaults.yaml") + + +def _load_aws_defaults_config(): + import ray.autoscaler.aws as ray_aws + + return os.path.join(os.path.dirname(ray_aws.__file__), "defaults.yaml") + + +def _load_vsphere_defaults_config(): + import ray.autoscaler.vsphere as ray_vsphere + + return os.path.join(os.path.dirname(ray_vsphere.__file__), "defaults.yaml") + + +def _load_gcp_defaults_config(): + import ray.autoscaler.gcp as ray_gcp + + return os.path.join(os.path.dirname(ray_gcp.__file__), "defaults.yaml") + + +def _load_azure_defaults_config(): + import ray.autoscaler.azure as ray_azure + + return os.path.join(os.path.dirname(ray_azure.__file__), "defaults.yaml") + + +def _load_aliyun_defaults_config(): + import ray.autoscaler.aliyun as ray_aliyun + + return os.path.join(os.path.dirname(ray_aliyun.__file__), "defaults.yaml") + + +def _import_external(provider_config): + provider_cls = load_function_or_class(path=provider_config["module"]) + return provider_cls + + +_NODE_PROVIDERS = { + "local": _import_local, + "fake_multinode": _import_fake_multinode, + "fake_multinode_docker": _import_fake_multinode_docker, + "readonly": _import_readonly, + "aws": _import_aws, + "gcp": _import_gcp, + "vsphere": _import_vsphere, + "azure": _import_azure, + "kuberay": _import_kuberay, + "aliyun": _import_aliyun, + "external": _import_external, # Import an external module + "spark": _import_spark, +} + +_PROVIDER_PRETTY_NAMES = { + "readonly": "Readonly (Manual Cluster Setup)", + "fake_multinode": "Fake Multinode", + "fake_multinode_docker": "Fake Multinode Docker", + "local": "Local", + "aws": "AWS", + "gcp": "GCP", + "azure": "Azure", + "kuberay": "KubeRay", + "aliyun": "Aliyun", + "external": "External", + "vsphere": "vSphere", + "spark": "Spark", +} + +_DEFAULT_CONFIGS = { + "fake_multinode": _load_fake_multinode_defaults_config, + "fake_multinode_docker": _load_fake_multinode_docker_defaults_config, + "local": _load_local_defaults_config, + "aws": _load_aws_defaults_config, + "gcp": _load_gcp_defaults_config, + "azure": _load_azure_defaults_config, + "aliyun": _load_aliyun_defaults_config, + "vsphere": _load_vsphere_defaults_config, + "readonly": _load_read_only_defaults_config, +} + + +def _get_node_provider_cls(provider_config: Dict[str, Any]): + """Get the node provider class for a given provider config. + + Note that this may be used by private node providers that proxy methods to + built-in node providers, so we should maintain backwards compatibility. + + Args: + provider_config: provider section of the autoscaler config. + + Returns: + NodeProvider class + """ + importer = _NODE_PROVIDERS.get(provider_config["type"]) + if importer is None: + raise NotImplementedError( + "Unsupported node provider: {}".format(provider_config["type"]) + ) + return importer(provider_config) + + +def _get_node_provider( + provider_config: Dict[str, Any], cluster_name: str, use_cache: bool = True +) -> Any: + """Get the instantiated node provider for a given provider config. + + Note that this may be used by private node providers that proxy methods to + built-in node providers, so we should maintain backwards compatibility. + + Args: + provider_config: provider section of the autoscaler config. + cluster_name: cluster name from the autoscaler config. + use_cache: whether or not to use a cached definition if available. If + False, the returned object will also not be stored in the cache. + + Returns: + NodeProvider + """ + provider_key = (json.dumps(provider_config, sort_keys=True), cluster_name) + if use_cache and provider_key in _provider_instances: + return _provider_instances[provider_key] + + provider_cls = _get_node_provider_cls(provider_config) + new_provider = provider_cls(provider_config, cluster_name) + + if use_cache: + _provider_instances[provider_key] = new_provider + + return new_provider + + +def _clear_provider_cache(): + global _provider_instances + _provider_instances = {} + + +def _get_default_config(provider_config): + """Retrieve a node provider. + + This is an INTERNAL API. It is not allowed to call this from any Ray + package outside the autoscaler. + """ + if provider_config["type"] == "external": + return copy.deepcopy(MINIMAL_EXTERNAL_CONFIG) + load_config = _DEFAULT_CONFIGS.get(provider_config["type"]) + if load_config is None: + raise NotImplementedError( + "Unsupported node provider: {}".format(provider_config["type"]) + ) + path_to_default = load_config() + with open(path_to_default) as f: + defaults = yaml.safe_load(f) + + return defaults diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e46c5d886d3eefbbe6cbec4856a32a04fefa1955 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c1813b06de022a00a2e727471dfa778414d03a6 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/defaults.yaml b/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/defaults.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4f30283f5dbae42d4ac88900ce2ba25d33e129ce --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/defaults.yaml @@ -0,0 +1,31 @@ +cluster_name: default +max_workers: 0 +provider: + type: readonly + # This must be true since the nodes share the same ip! + use_node_id_as_ip: True + disable_node_updaters: True + disable_launch_config_check: True +available_node_types: + ray.head.default: + resources: {} + node_config: {} + max_workers: 0 +head_node_type: ray.head.default +upscaling_speed: 1.0 +# +# !!! Configurations below are not supported in fake cluster mode !!! +# +auth: {} +docker: {} +initialization_commands: [] +setup_commands: [] +head_setup_commands: [] +worker_setup_commands: [] +head_start_ray_commands: [] +worker_start_ray_commands: [] +file_mounts: {} +cluster_synced_files: [] +file_mounts_sync_continuously: false +rsync_exclude: [] +rsync_filter: [] diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..692501bb760f28857d59d44d18202a483f356b7a --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/readonly/node_provider.py @@ -0,0 +1,80 @@ +from typing import List, Tuple + +from ray.autoscaler._private.util import format_readonly_node_type +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + STATUS_UP_TO_DATE, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_NAME, + TAG_RAY_NODE_STATUS, + TAG_RAY_USER_NODE_TYPE, +) + + +class ReadOnlyNodeProvider(NodeProvider): + """A node provider that merely reports the current cluster state. + + This is used for laptop mode / manual cluster setup modes, in order to + provide status reporting in the same way for users.""" + + def __init__(self, provider_config, cluster_name): + NodeProvider.__init__(self, provider_config, cluster_name) + self.nodes = {} + + def is_readonly(self): + return True + + def _set_nodes(self, nodes: List[Tuple[str, str]]): + """Update the set of nodes in the cluster. + + Args: + nodes: List of (node_id, node_manager_address) tuples. + """ + new_nodes = {} + for node_id, node_manager_address in nodes: + # We make up a fake node type for each node (since each node + # could have its own unique configuration). + new_nodes[node_id] = { + # Keep prefix in sync with node config gen in monitor.py + "node_type": format_readonly_node_type(node_id), + "ip": node_manager_address, + } + self.nodes = new_nodes + + def non_terminated_nodes(self, tag_filters): + return list(self.nodes.keys()) + + def is_running(self, node_id): + return node_id in self.nodes + + def is_terminated(self, node_id): + return node_id not in self.nodes + + def node_tags(self, node_id): + tags = { + TAG_RAY_NODE_KIND: NODE_KIND_HEAD, + TAG_RAY_USER_NODE_TYPE: self.nodes[node_id]["node_type"], + TAG_RAY_NODE_NAME: node_id, + TAG_RAY_NODE_STATUS: STATUS_UP_TO_DATE, + } + return tags + + def external_ip(self, node_id): + return node_id + + def internal_ip(self, node_id): + return node_id + + def set_node_tags(self, node_id, tags): + raise AssertionError("Readonly node provider cannot be updated") + + def create_node(self, node_config, tags, count): + raise AssertionError("Readonly node provider cannot be updated") + + def terminate_node(self, node_id): + raise AssertionError("Readonly node provider cannot be updated") + + @staticmethod + def bootstrap_config(cluster_config): + return cluster_config diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/resource_demand_scheduler.py b/lib/python3.12/site-packages/ray/autoscaler/_private/resource_demand_scheduler.py new file mode 100644 index 0000000000000000000000000000000000000000..f53898f14a83ba1141f90f52ab0a5d22afe734e7 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/resource_demand_scheduler.py @@ -0,0 +1,1022 @@ +"""Implements multi-node-type autoscaling. + +This file implements an autoscaling algorithm that is aware of multiple node +types (e.g., example-multi-node-type.yaml). The Ray autoscaler will pass in +a vector of resource shape demands, and the resource demand scheduler will +return a list of node types that can satisfy the demands given constraints +(i.e., reverse bin packing). +""" + +import collections +import copy +import logging +import os +from abc import abstractmethod +from functools import partial +from typing import Callable, Dict, List, Optional, Tuple + +import ray +from ray._private.gcs_utils import PlacementGroupTableData +from ray.autoscaler._private.constants import ( + AUTOSCALER_CONSERVE_GPU_NODES, + AUTOSCALER_UPSCALING_INITIAL_NUM_NODES, + AUTOSCALER_UTILIZATION_SCORER_KEY, +) +from ray.autoscaler._private.loader import load_function_or_class +from ray.autoscaler._private.node_provider_availability_tracker import ( + NodeAvailabilitySummary, +) +from ray.autoscaler._private.util import ( + NodeID, + NodeIP, + NodeType, + NodeTypeConfigDict, + ResourceDict, + is_placement_group_resource, +) +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + NODE_KIND_UNMANAGED, + NODE_KIND_WORKER, + TAG_RAY_NODE_KIND, + TAG_RAY_USER_NODE_TYPE, +) +from ray.core.generated.common_pb2 import PlacementStrategy + +logger = logging.getLogger(__name__) + +NodeResources = ResourceDict +ResourceDemands = List[ResourceDict] + + +class UtilizationScore: + """This fancy class just defines the `UtilizationScore` protocol to be + some type that is a "totally ordered set" (i.e. things that can be sorted). + + What we're really trying to express is + + ``` + UtilizationScore = TypeVar("UtilizationScore", bound=Comparable["UtilizationScore"]) + ``` + + but Comparable isn't a real type and, and a bound with a type argument + can't be enforced (f-bounded polymorphism with contravariance). See Guido's + comment for more details: https://github.com/python/typing/issues/59. + + This isn't just a `float`. In the case of the default scorer, it's a + `Tuple[float, float]` which is quite difficult to map to a single number. + + """ + + @abstractmethod + def __eq__(self, other: "UtilizationScore") -> bool: + pass + + @abstractmethod + def __lt__(self: "UtilizationScore", other: "UtilizationScore") -> bool: + pass + + def __gt__(self: "UtilizationScore", other: "UtilizationScore") -> bool: + return (not self < other) and self != other + + def __le__(self: "UtilizationScore", other: "UtilizationScore") -> bool: + return self < other or self == other + + def __ge__(self: "UtilizationScore", other: "UtilizationScore") -> bool: + return not self < other + + +class UtilizationScorer: + def __call__( + node_resources: NodeResources, + resource_demands: ResourceDemands, + *, + node_availability_summary: NodeAvailabilitySummary, + ) -> Optional[UtilizationScore]: + pass + + +class ResourceDemandScheduler: + def __init__( + self, + provider: NodeProvider, + node_types: Dict[NodeType, NodeTypeConfigDict], + max_workers: int, + head_node_type: NodeType, + upscaling_speed: float, + ) -> None: + self.provider = provider + self.node_types = copy.deepcopy(node_types) + self.node_resource_updated = set() + self.max_workers = max_workers + self.head_node_type = head_node_type + self.upscaling_speed = upscaling_speed + + utilization_scorer_str = os.environ.get( + AUTOSCALER_UTILIZATION_SCORER_KEY, + "ray.autoscaler._private.resource_demand_scheduler" + "._default_utilization_scorer", + ) + self.utilization_scorer: UtilizationScorer = load_function_or_class( + utilization_scorer_str + ) + + def _get_head_and_workers(self, nodes: List[NodeID]) -> Tuple[NodeID, List[NodeID]]: + """Returns the head node's id and the list of all worker node ids, + given a list `nodes` of all node ids in the cluster. + """ + head_id, worker_ids = None, [] + for node in nodes: + tags = self.provider.node_tags(node) + if tags[TAG_RAY_NODE_KIND] == NODE_KIND_HEAD: + head_id = node + elif tags[TAG_RAY_NODE_KIND] == NODE_KIND_WORKER: + worker_ids.append(node) + return head_id, worker_ids + + def reset_config( + self, + provider: NodeProvider, + node_types: Dict[NodeType, NodeTypeConfigDict], + max_workers: int, + head_node_type: NodeType, + upscaling_speed: float = 1, + ) -> None: + """Updates the class state variables. + + For legacy yamls, it merges previous state and new state to make sure + inferered resources are not lost. + """ + self.provider = provider + self.node_types = copy.deepcopy(node_types) + self.node_resource_updated = set() + self.max_workers = max_workers + self.head_node_type = head_node_type + self.upscaling_speed = upscaling_speed + + def is_feasible(self, bundle: ResourceDict) -> bool: + for node_type, config in self.node_types.items(): + max_of_type = config.get("max_workers", 0) + node_resources = config["resources"] + if (node_type == self.head_node_type or max_of_type > 0) and _fits( + node_resources, bundle + ): + return True + return False + + def get_nodes_to_launch( + self, + nodes: List[NodeID], + launching_nodes: Dict[NodeType, int], + resource_demands: List[ResourceDict], + unused_resources_by_ip: Dict[NodeIP, ResourceDict], + pending_placement_groups: List[PlacementGroupTableData], + max_resources_by_ip: Dict[NodeIP, ResourceDict], + ensure_min_cluster_size: List[ResourceDict], + node_availability_summary: NodeAvailabilitySummary, + ) -> (Dict[NodeType, int], List[ResourceDict]): + """Given resource demands, return node types to add to the cluster. + + This method: + (1) calculates the resources present in the cluster by: + - computing available resources for each existing node + - counting the number of nodes per node type + - including both running and launching nodes + (2) calculates the remaining nodes to add to respect min_workers + constraint per node type. + (3) for each strict spread placement group, reserve space on + available nodes and launch new nodes if necessary. + (4) calculates the unfulfilled resource bundles. + (5) calculates which nodes need to be launched to fulfill all + the bundle requests, subject to max_worker constraints. + + Args: + nodes: List of existing nodes in the cluster. + launching_nodes: Summary of node types currently being launched. + resource_demands: Vector of resource demands from the scheduler. + unused_resources_by_ip: Mapping from ip to available resources. + pending_placement_groups: Placement group demands. + max_resources_by_ip: Mapping from ip to static node resources. + ensure_min_cluster_size: Try to ensure the cluster can fit at least + this set of resources. This differs from resources_demands in + that we don't take into account existing usage. + + node_availability_summary: A snapshot of the current + NodeAvailabilitySummary. + + Returns: + Dict of count to add for each node type, and residual of resources + that still cannot be fulfilled. + """ + utilization_scorer = partial( + self.utilization_scorer, node_availability_summary=node_availability_summary + ) + self._update_node_resources_from_runtime(nodes, max_resources_by_ip) + + # Step 1: Calculate current cluster resources and node type counts + node_resources: List[ResourceDict] + node_type_counts: Dict[NodeType, int] + node_resources, node_type_counts = self.calculate_node_resources( + nodes, launching_nodes, unused_resources_by_ip + ) + + logger.debug("Cluster resources: {}".format(node_resources)) + logger.debug("Node counts: {}".format(node_type_counts)) + # Step 2: add nodes to add to satisfy min_workers for each type + ( + node_resources, + node_type_counts, + adjusted_min_workers, + ) = _add_min_workers_nodes( + node_resources, + node_type_counts, + self.node_types, + self.max_workers, + self.head_node_type, + ensure_min_cluster_size, + utilization_scorer=utilization_scorer, + ) + + # Step 3: get resource demands of placement groups and return the + # groups that should be strictly spread. + logger.debug(f"Placement group demands: {pending_placement_groups}") + # TODO(Clark): Refactor placement group bundle demands such that their placement + # group provenance is mantained, since we need to keep an accounting of the + # cumulative CPU cores allocated as fulfilled during bin packing in order to + # ensure that a placement group's cumulative allocation is under the placement + # group's max CPU fraction per node. Without this, and placement group with many + # bundles might not be schedulable, but will fail to trigger scale-up since the + # max CPU fraction is properly applied to the cumulative bundle requests for a + # single node. + # + # placement_group_demand_vector: List[Tuple[List[ResourceDict], double]] + # + # bin_pack_residual() can keep it's packing priority; we just need to account + # for (1) the running CPU allocation for the bundle's placement group for that + # particular node, and (2) the max CPU cores allocatable for a single placement + # group for that particular node. + ( + placement_group_demand_vector, + strict_spreads, + ) = placement_groups_to_resource_demands(pending_placement_groups) + # Place placement groups demand vector at the beginning of the resource + # demands vector to make it consistent (results in the same types of + # nodes to add) with pg_demands_nodes_max_launch_limit calculated later + resource_demands = placement_group_demand_vector + resource_demands + + ( + spread_pg_nodes_to_add, + node_resources, + node_type_counts, + ) = self.reserve_and_allocate_spread( + strict_spreads, + node_resources, + node_type_counts, + utilization_scorer, + ) + + # Calculate the nodes to add for bypassing max launch limit for + # placement groups and spreads. + unfulfilled_placement_groups_demands, _ = get_bin_pack_residual( + node_resources, + placement_group_demand_vector, + ) + # Add 1 to account for the head node. + max_to_add = self.max_workers + 1 - sum(node_type_counts.values()) + pg_demands_nodes_max_launch_limit, _ = get_nodes_for( + self.node_types, + node_type_counts, + self.head_node_type, + max_to_add, + unfulfilled_placement_groups_demands, + utilization_scorer=utilization_scorer, + ) + placement_groups_nodes_max_limit = { + node_type: spread_pg_nodes_to_add.get(node_type, 0) + + pg_demands_nodes_max_launch_limit.get(node_type, 0) + for node_type in self.node_types + } + + # Step 4/5: add nodes for pending tasks, actors, and non-strict spread + # groups + unfulfilled, _ = get_bin_pack_residual(node_resources, resource_demands) + logger.debug("Resource demands: {}".format(resource_demands)) + logger.debug("Unfulfilled demands: {}".format(unfulfilled)) + nodes_to_add_based_on_demand, final_unfulfilled = get_nodes_for( + self.node_types, + node_type_counts, + self.head_node_type, + max_to_add, + unfulfilled, + utilization_scorer=utilization_scorer, + ) + logger.debug("Final unfulfilled: {}".format(final_unfulfilled)) + # Merge nodes to add based on demand and nodes to add based on + # min_workers constraint. We add them because nodes to add based on + # demand was calculated after the min_workers constraint was respected. + total_nodes_to_add = {} + + for node_type in self.node_types: + nodes_to_add = ( + adjusted_min_workers.get(node_type, 0) + + spread_pg_nodes_to_add.get(node_type, 0) + + nodes_to_add_based_on_demand.get(node_type, 0) + ) + if nodes_to_add > 0: + total_nodes_to_add[node_type] = nodes_to_add + + # Limit the number of concurrent launches + total_nodes_to_add = self._get_concurrent_resource_demand_to_launch( + total_nodes_to_add, + unused_resources_by_ip.keys(), + nodes, + launching_nodes, + adjusted_min_workers, + placement_groups_nodes_max_limit, + ) + + logger.debug("Node requests: {}".format(total_nodes_to_add)) + return total_nodes_to_add, final_unfulfilled + + def _update_node_resources_from_runtime( + self, nodes: List[NodeID], max_resources_by_ip: Dict[NodeIP, ResourceDict] + ): + """Update static node type resources with runtime resources + + This will update the cached static node type resources with the runtime + resources. Because we can not know the exact autofilled memory or + object_store_memory from config file. + """ + need_update = len(self.node_types) != len(self.node_resource_updated) + + if not need_update: + return + for node_id in nodes: + tags = self.provider.node_tags(node_id) + + if TAG_RAY_USER_NODE_TYPE not in tags: + continue + + node_type = tags[TAG_RAY_USER_NODE_TYPE] + if ( + node_type in self.node_resource_updated + or node_type not in self.node_types + ): + # continue if the node type has been updated or is not an known + # node type + continue + ip = self.provider.internal_ip(node_id) + runtime_resources = max_resources_by_ip.get(ip) + if runtime_resources: + runtime_resources = copy.deepcopy(runtime_resources) + resources = self.node_types[node_type].get("resources", {}) + for key in ["CPU", "GPU", "memory"]: + if key in runtime_resources: + resources[key] = runtime_resources[key] + self.node_types[node_type]["resources"] = resources + + node_kind = tags[TAG_RAY_NODE_KIND] + if node_kind == NODE_KIND_WORKER: + # Here, we do not record the resources have been updated + # if it is the head node kind. Because it need be updated + # by worker kind runtime resource. The most difference + # between head and worker is the memory resources. The head + # node needs to configure redis memory which is not needed + # for worker nodes. + self.node_resource_updated.add(node_type) + + def _get_concurrent_resource_demand_to_launch( + self, + to_launch: Dict[NodeType, int], + connected_nodes: List[NodeIP], + non_terminated_nodes: List[NodeID], + pending_launches_nodes: Dict[NodeType, int], + adjusted_min_workers: Dict[NodeType, int], + placement_group_nodes: Dict[NodeType, int], + ) -> Dict[NodeType, int]: + """Updates the max concurrent resources to launch for each node type. + + Given the current nodes that should be launched, the non terminated + nodes (running and pending) and the pending to be launched nodes. This + method calculates the maximum number of nodes to launch concurrently + for each node type as follows: + 1) Calculates the running nodes. + 2) Calculates the pending nodes and gets the launching nodes. + 3) Limits the total number of pending + currently-launching + + to-be-launched nodes to: + max( + 5, + self.upscaling_speed * max(running_nodes[node_type], 1) + ). + + Args: + to_launch: List of number of nodes to launch based on resource + demand for every node type. + connected_nodes: Running nodes (from LoadMetrics). + non_terminated_nodes: Non terminated nodes (pending/running). + pending_launches_nodes: Nodes that are in the launch queue. + adjusted_min_workers: Nodes to launch to satisfy + min_workers and request_resources(). This overrides the launch + limits since the user is hinting to immediately scale up to + this size. + placement_group_nodes: Nodes to launch for placement groups. + This overrides the launch concurrency limits. + Returns: + Dict[NodeType, int]: Maximum number of nodes to launch for each + node type. + """ + updated_nodes_to_launch = {} + running_nodes, pending_nodes = self._separate_running_and_pending_nodes( + non_terminated_nodes, + connected_nodes, + ) + for node_type in to_launch: + # Enforce here max allowed pending nodes to be frac of total + # running nodes. + max_allowed_pending_nodes = max( + AUTOSCALER_UPSCALING_INITIAL_NUM_NODES, + int(self.upscaling_speed * max(running_nodes[node_type], 1)), + ) + total_pending_nodes = ( + pending_launches_nodes.get(node_type, 0) + pending_nodes[node_type] + ) + + upper_bound = max( + max_allowed_pending_nodes - total_pending_nodes, + # Allow more nodes if this is to respect min_workers or + # request_resources() or placement groups. + adjusted_min_workers.get(node_type, 0) + + placement_group_nodes.get(node_type, 0), + ) + + if upper_bound > 0: + updated_nodes_to_launch[node_type] = min( + upper_bound, to_launch[node_type] + ) + + return updated_nodes_to_launch + + def _separate_running_and_pending_nodes( + self, + non_terminated_nodes: List[NodeID], + connected_nodes: List[NodeIP], + ) -> (Dict[NodeType, int], Dict[NodeType, int]): + """Splits connected and non terminated nodes to pending & running.""" + + running_nodes = collections.defaultdict(int) + pending_nodes = collections.defaultdict(int) + for node_id in non_terminated_nodes: + tags = self.provider.node_tags(node_id) + if TAG_RAY_USER_NODE_TYPE in tags: + node_type = tags[TAG_RAY_USER_NODE_TYPE] + node_ip = self.provider.internal_ip(node_id) + if node_ip in connected_nodes: + running_nodes[node_type] += 1 + else: + pending_nodes[node_type] += 1 + return running_nodes, pending_nodes + + def calculate_node_resources( + self, + nodes: List[NodeID], + pending_nodes: Dict[NodeID, int], + unused_resources_by_ip: Dict[str, ResourceDict], + ) -> (List[ResourceDict], Dict[NodeType, int]): + """Returns node resource list and node type counts. + + Counts the running nodes, pending nodes. + Args: + nodes: Existing nodes. + pending_nodes: Pending nodes. + Returns: + node_resources: a list of running + pending resources. + E.g., [{"CPU": 4}, {"GPU": 2}]. + node_type_counts: running + pending workers per node type. + """ + + node_resources = [] + node_type_counts = collections.defaultdict(int) + + def add_node(node_type, available_resources=None): + if node_type not in self.node_types: + # We should not get here, but if for some reason we do, log an + # error and skip the errant node_type. + logger.error( + f"Missing entry for node_type {node_type} in " + f"cluster config: {self.node_types} under entry " + "available_node_types. This node's resources will be " + "ignored. If you are using an unmanaged node, manually " + f"set the {TAG_RAY_NODE_KIND} tag to " + f'"{NODE_KIND_UNMANAGED}" in your cloud provider\'s ' + "management console." + ) + return None + # Careful not to include the same dict object multiple times. + available = copy.deepcopy(self.node_types[node_type]["resources"]) + # If available_resources is None this might be because the node is + # no longer pending, but the raylet hasn't sent a heartbeat to gcs + # yet. + if available_resources is not None: + available = copy.deepcopy(available_resources) + + node_resources.append(available) + node_type_counts[node_type] += 1 + + for node_id in nodes: + tags = self.provider.node_tags(node_id) + if TAG_RAY_USER_NODE_TYPE in tags: + node_type = tags[TAG_RAY_USER_NODE_TYPE] + ip = self.provider.internal_ip(node_id) + available_resources = unused_resources_by_ip.get(ip) + add_node(node_type, available_resources) + + for node_type, count in pending_nodes.items(): + for _ in range(count): + add_node(node_type) + + return node_resources, node_type_counts + + def reserve_and_allocate_spread( + self, + strict_spreads: List[List[ResourceDict]], + node_resources: List[ResourceDict], + node_type_counts: Dict[NodeType, int], + utilization_scorer: Callable[ + [NodeResources, ResourceDemands], Optional[UtilizationScore] + ], + ): + """For each strict spread, attempt to reserve as much space as possible + on the node, then allocate new nodes for the unfulfilled portion. + + Args: + strict_spreads (List[List[ResourceDict]]): A list of placement + groups which must be spread out. + node_resources (List[ResourceDict]): Available node resources in + the cluster. + node_type_counts (Dict[NodeType, int]): The amount of each type of + node pending or in the cluster. + utilization_scorer: A function that, given a node + type, its resources, and resource demands, returns what its + utilization would be. + + Returns: + Dict[NodeType, int]: Nodes to add. + List[ResourceDict]: The updated node_resources after the method. + Dict[NodeType, int]: The updated node_type_counts. + + """ + to_add = collections.defaultdict(int) + for bundles in strict_spreads: + # Try to pack as many bundles of this group as possible on existing + # nodes. The remaining will be allocated on new nodes. + unfulfilled, updated_node_resources = get_bin_pack_residual( + node_resources, bundles, strict_spread=True + ) + max_to_add = self.max_workers + 1 - sum(node_type_counts.values()) + # Allocate new nodes for the remaining bundles that don't fit. + to_launch, _ = get_nodes_for( + self.node_types, + node_type_counts, + self.head_node_type, + max_to_add, + unfulfilled, + utilization_scorer=utilization_scorer, + strict_spread=True, + ) + new_node_resources = _node_type_counts_to_node_resources( + self.node_types, to_launch + ) + # Update node resources to include newly launched nodes and their + # bundles. + unfulfilled, including_reserved = get_bin_pack_residual( + new_node_resources, unfulfilled, strict_spread=True + ) + if unfulfilled: + logger.debug( + "Unfulfilled strict spread placement group: {}".format(bundles) + ) + continue + _inplace_add(node_type_counts, to_launch) + _inplace_add(to_add, to_launch) + node_resources = updated_node_resources + including_reserved + return to_add, node_resources, node_type_counts + + def debug_string( + self, + nodes: List[NodeID], + pending_nodes: Dict[NodeID, int], + unused_resources_by_ip: Dict[str, ResourceDict], + ) -> str: + node_resources, node_type_counts = self.calculate_node_resources( + nodes, pending_nodes, unused_resources_by_ip + ) + + out = "Worker node types:" + for node_type, count in node_type_counts.items(): + out += "\n - {}: {}".format(node_type, count) + if pending_nodes.get(node_type): + out += " ({} pending)".format(pending_nodes[node_type]) + + return out + + +def _node_type_counts_to_node_resources( + node_types: Dict[NodeType, NodeTypeConfigDict], + node_type_counts: Dict[NodeType, int], +) -> List[ResourceDict]: + """Converts a node_type_counts dict into a list of node_resources.""" + resources = [] + for node_type, count in node_type_counts.items(): + # Be careful, each entry in the list must be deep copied! + resources += [node_types[node_type]["resources"].copy() for _ in range(count)] + return resources + + +def _add_min_workers_nodes( + node_resources: List[ResourceDict], + node_type_counts: Dict[NodeType, int], + node_types: Dict[NodeType, NodeTypeConfigDict], + max_workers: int, + head_node_type: NodeType, + ensure_min_cluster_size: List[ResourceDict], + utilization_scorer: Callable[ + [NodeResources, ResourceDemands, str], Optional[UtilizationScore] + ], +) -> (List[ResourceDict], Dict[NodeType, int], Dict[NodeType, int]): + """Updates resource demands to respect the min_workers and + request_resources() constraints. + + Args: + node_resources: Resources of existing nodes already launched/pending. + node_type_counts: Counts of existing nodes already launched/pending. + node_types: Node types config. + max_workers: global max_workers constaint. + ensure_min_cluster_size: resource demands from request_resources(). + utilization_scorer: A function that, given a node + type, its resources, and resource demands, returns what its + utilization would be. + + Returns: + node_resources: The updated node resources after adding min_workers + and request_resources() constraints per node type. + node_type_counts: The updated node counts after adding min_workers + and request_resources() constraints per node type. + total_nodes_to_add_dict: The nodes to add to respect min_workers and + request_resources() constraints. + """ + total_nodes_to_add_dict = {} + for node_type, config in node_types.items(): + existing = node_type_counts.get(node_type, 0) + target = min(config.get("min_workers", 0), config.get("max_workers", 0)) + if node_type == head_node_type: + # Add 1 to account for head node. + target = target + 1 + if existing < target: + total_nodes_to_add_dict[node_type] = target - existing + node_type_counts[node_type] = target + node_resources.extend( + [ + copy.deepcopy(node_types[node_type]["resources"]) + for _ in range(total_nodes_to_add_dict[node_type]) + ] + ) + + if ensure_min_cluster_size: + max_to_add = max_workers + 1 - sum(node_type_counts.values()) + max_node_resources = [] + # Fit request_resources() on all the resources as if they are idle. + for node_type in node_type_counts: + max_node_resources.extend( + [ + copy.deepcopy(node_types[node_type]["resources"]) + for _ in range(node_type_counts[node_type]) + ] + ) + # Get the unfulfilled to ensure min cluster size. + resource_requests_unfulfilled, _ = get_bin_pack_residual( + max_node_resources, ensure_min_cluster_size + ) + # Get the nodes to meet the unfulfilled. + nodes_to_add_request_resources, _ = get_nodes_for( + node_types, + node_type_counts, + head_node_type, + max_to_add, + resource_requests_unfulfilled, + utilization_scorer=utilization_scorer, + ) + # Update the resources, counts and total nodes to add. + for node_type in nodes_to_add_request_resources: + nodes_to_add = nodes_to_add_request_resources.get(node_type, 0) + if nodes_to_add > 0: + node_type_counts[node_type] = nodes_to_add + node_type_counts.get( + node_type, 0 + ) + node_resources.extend( + [ + copy.deepcopy(node_types[node_type]["resources"]) + for _ in range(nodes_to_add) + ] + ) + total_nodes_to_add_dict[ + node_type + ] = nodes_to_add + total_nodes_to_add_dict.get(node_type, 0) + return node_resources, node_type_counts, total_nodes_to_add_dict + + +def get_nodes_for( + node_types: Dict[NodeType, NodeTypeConfigDict], + existing_nodes: Dict[NodeType, int], + head_node_type: NodeType, + max_to_add: int, + resources: List[ResourceDict], + utilization_scorer: Callable[ + [NodeResources, ResourceDemands, str], Optional[UtilizationScore] + ], + strict_spread: bool = False, +) -> (Dict[NodeType, int], List[ResourceDict]): + """Determine nodes to add given resource demands and constraints. + + Args: + node_types: node types config. + existing_nodes: counts of existing nodes already launched. + This sets constraints on the number of new nodes to add. + max_to_add: global constraint on nodes to add. + resources: resource demands to fulfill. + strict_spread: If true, each element in `resources` must be placed on a + different node. + utilization_scorer: A function that, given a node + type, its resources, and resource demands, returns what its + utilization would be. + + Returns: + Dict of count to add for each node type, and residual of resources + that still cannot be fulfilled. + """ + nodes_to_add: Dict[NodeType, int] = collections.defaultdict(int) + + while resources and sum(nodes_to_add.values()) < max_to_add: + utilization_scores = [] + for node_type in node_types: + max_workers_of_node_type = node_types[node_type].get("max_workers", 0) + if head_node_type == node_type: + # Add 1 to account for head node. + max_workers_of_node_type = max_workers_of_node_type + 1 + if ( + existing_nodes.get(node_type, 0) + nodes_to_add.get(node_type, 0) + >= max_workers_of_node_type + ): + continue + node_resources = node_types[node_type]["resources"] + if strict_spread: + # If handling strict spread, only one bundle can be placed on + # the node. + score = utilization_scorer(node_resources, [resources[0]], node_type) + else: + score = utilization_scorer(node_resources, resources, node_type) + if score is not None: + utilization_scores.append((score, node_type)) + + # Give up, no feasible node. + if not utilization_scores: + if not any( + is_placement_group_resource(resource) + for resources_dict in resources + for resource in resources_dict + ): + logger.warning( + f"The autoscaler could not find a node type to satisfy the " + f"request: {resources}. Please specify a node type with the " + f"necessary resources." + ) + break + + utilization_scores = sorted(utilization_scores, reverse=True) + best_node_type = utilization_scores[0][1] + nodes_to_add[best_node_type] += 1 + if strict_spread: + resources = resources[1:] + else: + allocated_resource = node_types[best_node_type]["resources"] + residual, _ = get_bin_pack_residual([allocated_resource], resources) + assert len(residual) < len(resources), (resources, residual) + resources = residual + + return nodes_to_add, resources + + +def _resource_based_utilization_scorer( + node_resources: ResourceDict, + resources: List[ResourceDict], + *, + node_availability_summary: NodeAvailabilitySummary, +) -> Optional[Tuple[bool, int, float, float]]: + remaining = copy.deepcopy(node_resources) + fittable = [] + resource_types = set() + for r in resources: + for k, v in r.items(): + if v > 0: + resource_types.add(k) + if _fits(remaining, r): + fittable.append(r) + _inplace_subtract(remaining, r) + if not fittable: + return None + + util_by_resources = [] + num_matching_resource_types = 0 + for k, v in node_resources.items(): + # Don't divide by zero. + if v < 1: + # Could test v == 0 on the nose, but v < 1 feels safer. + # (Note that node resources are integers.) + continue + if k in resource_types: + num_matching_resource_types += 1 + util = (v - remaining[k]) / v + util_by_resources.append(v * (util**3)) + + # Could happen if node_resources has only zero values. + if not util_by_resources: + return None + + # Prefer not to launch a GPU node if there aren't any GPU requirements in the + # resource bundle. + gpu_ok = True + if AUTOSCALER_CONSERVE_GPU_NODES: + is_gpu_node = "GPU" in node_resources and node_resources["GPU"] > 0 + any_gpu_task = any("GPU" in r for r in resources) + if is_gpu_node and not any_gpu_task: + gpu_ok = False + + # Prioritize avoiding gpu nodes for non-gpu workloads first, + # then prioritize matching multiple resource types, + # then prioritize using all resources, + # then prioritize overall balance of multiple resources. + return ( + gpu_ok, + num_matching_resource_types, + min(util_by_resources), + # util_by_resources should be non empty + float(sum(util_by_resources)) / len(util_by_resources), + ) + + +def _default_utilization_scorer( + node_resources: ResourceDict, + resources: List[ResourceDict], + node_type: str, + *, + node_availability_summary: NodeAvailabilitySummary, +): + return _resource_based_utilization_scorer( + node_resources, resources, node_availability_summary=node_availability_summary + ) + + +def get_bin_pack_residual( + node_resources: List[ResourceDict], + resource_demands: List[ResourceDict], + strict_spread: bool = False, +) -> (List[ResourceDict], List[ResourceDict]): + """Return a subset of resource_demands that cannot fit in the cluster. + + TODO(ekl): this currently does not guarantee the resources will be packed + correctly by the Ray scheduler. This is only possible once the Ray backend + supports a placement groups API. + + Args: + node_resources (List[ResourceDict]): List of resources per node. + resource_demands (List[ResourceDict]): List of resource bundles that + need to be bin packed onto the nodes. + strict_spread: If true, each element in resource_demands must be + placed on a different entry in `node_resources`. + + Returns: + List[ResourceDict]: the residual list resources that do not fit. + List[ResourceDict]: The updated node_resources after the method. The order of the list elements remains unchanged. + """ + + unfulfilled = [] + + # A most naive bin packing algorithm. + nodes = copy.deepcopy(node_resources) + # List of nodes that cannot be used again due to strict spread. + used = set() + # We order the resource demands in the following way: + # More complex demands first. + # Break ties: heavier demands first. + # Break ties: lexicographically (to ensure stable ordering). + for demand in sorted( + resource_demands, + key=lambda demand: ( + len(demand.values()), + sum(demand.values()), + sorted(demand.items()), + ), + reverse=True, + ): + found = False + node = None + for i in range(len(nodes)): + if i in used: + continue + node = nodes[i] + if _fits(node, demand): + found = True + # In the strict_spread case, we can't reuse nodes. + if strict_spread: + used.add(i) + break + if found and node: + _inplace_subtract(node, demand) + else: + unfulfilled.append(demand) + + return unfulfilled, nodes + + +def _fits(node: ResourceDict, resources: ResourceDict) -> bool: + for k, v in resources.items(): + # TODO(jjyao): Change ResourceDict to a class so we can + # hide the implicit resource handling. + if v > node.get( + k, 1.0 if k.startswith(ray._raylet.IMPLICIT_RESOURCE_PREFIX) else 0.0 + ): + return False + return True + + +def _inplace_subtract(node: ResourceDict, resources: ResourceDict) -> None: + for k, v in resources.items(): + if v == 0: + # This is an edge case since some reasonable programs/computers can + # do `ray.autoscaler.sdk.request_resources({"GPU": 0}"})`. + continue + if k not in node: + assert k.startswith(ray._raylet.IMPLICIT_RESOURCE_PREFIX), (k, node) + node[k] = 1 + assert k in node, (k, node) + node[k] -= v + assert node[k] >= 0.0, (node, k, v) + + +def _inplace_add(a: collections.defaultdict, b: Dict) -> None: + """Generically adds values in `b` to `a`. + a[k] should be defined for all k in b.keys()""" + for k, v in b.items(): + a[k] += v + + +def placement_groups_to_resource_demands( + pending_placement_groups: List[PlacementGroupTableData], +) -> Tuple[List[ResourceDict], List[List[ResourceDict]]]: + """Preprocess placement group requests into regular resource demand vectors + when possible. The policy is: + * STRICT_PACK - Convert to a single bundle. + * PACK - Flatten into a resource demand vector. + * STRICT_SPREAD - Cannot be converted. + * SPREAD - Flatten into a resource demand vector. + + Args: + pending_placement_groups (List[PlacementGroupData]): List of + PlacementGroupLoad's. + + Returns: + List[ResourceDict]: The placement groups which were converted to a + resource demand vector. + List[List[ResourceDict]]: The placement groups which should be strictly + spread. + """ + resource_demand_vector = [] + unconverted = [] + for placement_group in pending_placement_groups: + # Skip **placed** bundle (which has node id associated with it). + shapes = [] + for bundle in placement_group.bundles: + if bundle.node_id != b"": + continue + shapes.append(dict(bundle.unit_resources)) + + if ( + placement_group.strategy == PlacementStrategy.PACK + or placement_group.strategy == PlacementStrategy.SPREAD + ): + resource_demand_vector.extend(shapes) + elif placement_group.strategy == PlacementStrategy.STRICT_PACK: + combined = collections.defaultdict(float) + for shape in shapes: + for label, quantity in shape.items(): + combined[label] += quantity + resource_demand_vector.append(combined) + elif placement_group.strategy == PlacementStrategy.STRICT_SPREAD: + unconverted.append(shapes) + else: + logger.error( + f"Unknown placement group request type: {placement_group}. " + f"Please file a bug report " + f"https://github.com/ray-project/ray/issues/new." + ) + return resource_demand_vector, unconverted diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9de759c5ca4c9d7888370a32042ed624d62ca3f0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73d01e6756067b556c285215566082505d3a5d05 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__pycache__/spark_job_server.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__pycache__/spark_job_server.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77619a9dfbfc54e423e6d6fdf7bea179bcdd8339 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/__pycache__/spark_job_server.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/spark/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..9c59ba4ed9fddb9f27b645c447d869b3ece91cc4 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/node_provider.py @@ -0,0 +1,250 @@ +import json +import logging +import sys +from threading import RLock +from typing import Any, Dict, Optional + +import requests + +from ray._common.network_utils import build_address +from ray.autoscaler.node_launch_exception import NodeLaunchException +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + NODE_KIND_WORKER, + STATUS_SETTING_UP, + STATUS_UP_TO_DATE, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_NAME, + TAG_RAY_NODE_STATUS, + TAG_RAY_USER_NODE_TYPE, +) + +logger = logging.getLogger(__name__) + +HEAD_NODE_ID = 0 +HEAD_NODE_TYPE = "ray.head.default" + + +class SparkNodeProvider(NodeProvider): + """A node provider that implements provider for nodes of Ray on spark.""" + + def __init__(self, provider_config, cluster_name): + NodeProvider.__init__(self, provider_config, cluster_name) + self.lock = RLock() + + self._nodes = { + str(HEAD_NODE_ID): { + "tags": { + TAG_RAY_NODE_KIND: NODE_KIND_HEAD, + TAG_RAY_USER_NODE_TYPE: HEAD_NODE_TYPE, + TAG_RAY_NODE_NAME: HEAD_NODE_ID, + TAG_RAY_NODE_STATUS: STATUS_UP_TO_DATE, + } + }, + } + self._next_node_id = 0 + + self.ray_head_ip = self.provider_config["ray_head_ip"] + # The port of spark job server. We send http request to spark job server + # to launch spark jobs, ray worker nodes are launched by spark task in + # spark jobs. + spark_job_server_port = self.provider_config["spark_job_server_port"] + self.spark_job_server_url = ( + f"http://{build_address(self.ray_head_ip, spark_job_server_port)}" + ) + self.ray_head_port = self.provider_config["ray_head_port"] + # The unique id for the Ray on spark cluster. + self.cluster_id = self.provider_config["cluster_unique_id"] + + def get_next_node_id(self): + with self.lock: + self._next_node_id += 1 + return self._next_node_id + + def non_terminated_nodes(self, tag_filters): + with self.lock: + nodes = [] + + died_nodes = [] + for node_id in self._nodes: + if node_id == str(HEAD_NODE_ID): + status = "running" + else: + status = self._query_node_status(node_id) + + if status == "running": + if ( + self._nodes[node_id]["tags"][TAG_RAY_NODE_STATUS] + == STATUS_SETTING_UP + ): + self._nodes[node_id]["tags"][ + TAG_RAY_NODE_STATUS + ] = STATUS_UP_TO_DATE + logger.info( + f"Spark node provider node {node_id} starts running." + ) + + if status == "terminated": + died_nodes.append(node_id) + else: + tags = self.node_tags(node_id) + ok = True + for k, v in tag_filters.items(): + if tags.get(k) != v: + ok = False + if ok: + nodes.append(node_id) + + for died_node_id in died_nodes: + self._nodes.pop(died_node_id) + + return nodes + + def _query_node_status(self, node_id): + spark_job_group_id = self._gen_spark_job_group_id(node_id) + + response = requests.post( + url=self.spark_job_server_url + "/query_task_status", + json={"spark_job_group_id": spark_job_group_id}, + ) + response.raise_for_status() + + decoded_resp = response.content.decode("utf-8") + json_res = json.loads(decoded_resp) + return json_res["status"] + + def is_running(self, node_id): + with self.lock: + return ( + node_id in self._nodes + and self._nodes[node_id]["tags"][TAG_RAY_NODE_STATUS] + == STATUS_UP_TO_DATE + ) + + def is_terminated(self, node_id): + with self.lock: + return node_id not in self._nodes + + def node_tags(self, node_id): + with self.lock: + return self._nodes[node_id]["tags"] + + def _get_ip(self, node_id: str) -> Optional[str]: + return node_id + + def external_ip(self, node_id): + return self._get_ip(node_id) + + def internal_ip(self, node_id): + return self._get_ip(node_id) + + def set_node_tags(self, node_id, tags): + assert node_id in self._nodes + self._nodes[node_id]["tags"].update(tags) + + def create_node( + self, node_config: Dict[str, Any], tags: Dict[str, str], count: int + ) -> Optional[Dict[str, Any]]: + raise AssertionError("This method should not be called.") + + def _gen_spark_job_group_id(self, node_id): + return ( + f"ray-cluster-{self.ray_head_port}-{self.cluster_id}" + f"-worker-node-{node_id}" + ) + + def create_node_with_resources_and_labels( + self, node_config, tags, count, resources, labels + ): + for _ in range(count): + self._create_node_with_resources_and_labels( + node_config, tags, resources, labels + ) + + def _create_node_with_resources_and_labels( + self, node_config, tags, resources, labels + ): + from ray.util.spark.cluster_init import _append_resources_config + + with self.lock: + resources = resources.copy() + node_type = tags[TAG_RAY_USER_NODE_TYPE] + # NOTE: + # "NODE_ID_AS_RESOURCE" value must be an integer, + # but `node_id` used by autoscaler must be a string. + node_id = str(self.get_next_node_id()) + resources["NODE_ID_AS_RESOURCE"] = int(node_id) + + conf = self.provider_config.copy() + + num_cpus_per_node = resources.pop("CPU") + num_gpus_per_node = resources.pop("GPU") + heap_memory_per_node = resources.pop("memory") + object_store_memory_per_node = resources.pop("object_store_memory") + + conf["worker_node_options"] = _append_resources_config( + conf["worker_node_options"], resources + ) + response = requests.post( + url=self.spark_job_server_url + "/create_node", + json={ + "spark_job_group_id": self._gen_spark_job_group_id(node_id), + "spark_job_group_desc": ( + "This job group is for spark job which runs the Ray " + f"cluster worker node {node_id} connecting to ray " + f"head node {build_address(self.ray_head_ip, self.ray_head_port)}" + ), + "using_stage_scheduling": conf["using_stage_scheduling"], + "ray_head_ip": self.ray_head_ip, + "ray_head_port": self.ray_head_port, + "ray_temp_dir": conf["ray_temp_dir"], + "num_cpus_per_node": num_cpus_per_node, + "num_gpus_per_node": num_gpus_per_node, + "heap_memory_per_node": heap_memory_per_node, + "object_store_memory_per_node": object_store_memory_per_node, + "worker_node_options": conf["worker_node_options"], + "collect_log_to_path": conf["collect_log_to_path"], + "node_id": resources["NODE_ID_AS_RESOURCE"], + }, + ) + + try: + # Spark job server is locally launched, if spark job server request + # failed, it is unlikely network error but probably unrecoverable + # error, so we make it fast-fail. + response.raise_for_status() + except Exception: + raise NodeLaunchException( + "Node creation failure", + f"Starting ray worker node {node_id} failed", + sys.exc_info(), + ) + + self._nodes[node_id] = { + "tags": { + TAG_RAY_NODE_KIND: NODE_KIND_WORKER, + TAG_RAY_USER_NODE_TYPE: node_type, + TAG_RAY_NODE_NAME: node_id, + TAG_RAY_NODE_STATUS: STATUS_SETTING_UP, + }, + } + logger.info(f"Spark node provider creates node {node_id}.") + + def terminate_node(self, node_id): + if node_id in self._nodes: + response = requests.post( + url=self.spark_job_server_url + "/terminate_node", + json={"spark_job_group_id": self._gen_spark_job_group_id(node_id)}, + ) + response.raise_for_status() + + with self.lock: + if node_id in self._nodes: + self._nodes.pop(node_id) + + logger.info(f"Spark node provider terminates node {node_id}") + + @staticmethod + def bootstrap_config(cluster_config): + return cluster_config diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/spark/spark_job_server.py b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/spark_job_server.py new file mode 100644 index 0000000000000000000000000000000000000000..6e550dfb2f2dcc4687c6bef1fd8d34e7439ef617 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/spark/spark_job_server.py @@ -0,0 +1,245 @@ +import json +import logging +import os +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +from pyspark.util import inheritable_thread_target + +from ray.util.spark.cluster_init import _start_ray_worker_nodes + + +class SparkJobServerRequestHandler(BaseHTTPRequestHandler): + def setup(self) -> None: + super().setup() + self._handler_lock = threading.RLock() + self._created_node_id_set = set() + self._logger = logging.getLogger(__name__) + if "RAY_ON_SPARK_JOB_SERVER_VERBOSE" in os.environ: + self._logger.setLevel(logging.DEBUG) + else: + self._logger.setLevel(logging.WARN) + + def _set_headers(self): + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + + def handle_POST(self, path, data): + path_parts = Path(path).parts[1:] + + spark_job_group_id = data["spark_job_group_id"] + + if path_parts[0] == "create_node": + assert len(path_parts) == 1, f"Illegal request path: {path}" + spark_job_group_desc = data["spark_job_group_desc"] + using_stage_scheduling = data["using_stage_scheduling"] + ray_head_ip = data["ray_head_ip"] + ray_head_port = data["ray_head_port"] + ray_temp_dir = data["ray_temp_dir"] + num_cpus_per_node = data["num_cpus_per_node"] + num_gpus_per_node = data["num_gpus_per_node"] + heap_memory_per_node = data["heap_memory_per_node"] + object_store_memory_per_node = data["object_store_memory_per_node"] + worker_node_options = data["worker_node_options"] + collect_log_to_path = data["collect_log_to_path"] + node_id = data["node_id"] + self._created_node_id_set.add(node_id) + + def start_ray_worker_thread_fn(): + try: + err_msg = _start_ray_worker_nodes( + spark_job_server=self.server, + spark_job_group_id=spark_job_group_id, + spark_job_group_desc=spark_job_group_desc, + num_worker_nodes=1, + using_stage_scheduling=using_stage_scheduling, + ray_head_ip=ray_head_ip, + ray_head_port=ray_head_port, + ray_temp_dir=ray_temp_dir, + num_cpus_per_node=num_cpus_per_node, + num_gpus_per_node=num_gpus_per_node, + heap_memory_per_node=heap_memory_per_node, + object_store_memory_per_node=object_store_memory_per_node, + worker_node_options=worker_node_options, + collect_log_to_path=collect_log_to_path, + node_id=node_id, + ) + if err_msg: + self._logger.warning( + f"Spark job {spark_job_group_id} hosting Ray worker node " + f"launching failed, error:\n{err_msg}" + ) + except Exception: + if spark_job_group_id in self.server.task_status_dict: + self.server.task_status_dict.pop(spark_job_group_id) + + msg = ( + f"Spark job {spark_job_group_id} hosting Ray worker node exit." + ) + if self._logger.level > logging.DEBUG: + self._logger.warning( + f"{msg} To see details, you can set " + "'RAY_ON_SPARK_JOB_SERVER_VERBOSE' environmental variable " + "to '1' before calling 'ray.util.spark.setup_ray_cluster'." + ) + else: + # This branch is only for debugging Ray-on-Spark purpose. + # User can configure 'RAY_ON_SPARK_JOB_SERVER_VERBOSE' + # environment variable to make the spark job server logging + # showing full exception stack here. + self._logger.debug(msg, exc_info=True) + + threading.Thread( + target=inheritable_thread_target(start_ray_worker_thread_fn), + args=(), + daemon=True, + ).start() + + self.server.task_status_dict[spark_job_group_id] = "pending" + return {} + + elif path_parts[0] == "check_node_id_availability": + node_id = data["node_id"] + with self._handler_lock: + if node_id in self._created_node_id_set: + # If the node with the node id has been created, + # it shouldn't be created twice so fail fast here. + # The case happens when a Ray node is down unexpected + # caused by spark worker node down and spark tries to + # reschedule the spark task, so it triggers node + # creation with duplicated node id. + return {"available": False} + else: + self._created_node_id_set.add(node_id) + return {"available": True} + + elif path_parts[0] == "terminate_node": + assert len(path_parts) == 1, f"Illegal request path: {path}" + self.server.spark.sparkContext.cancelJobGroup(spark_job_group_id) + if spark_job_group_id in self.server.task_status_dict: + self.server.task_status_dict.pop(spark_job_group_id) + return {} + + elif path_parts[0] == "notify_task_launched": + if spark_job_group_id in self.server.task_status_dict: + # Note that if `spark_job_group_id` not in task_status_dict, + # the task has been terminated + self.server.task_status_dict[spark_job_group_id] = "running" + self._logger.info(f"Spark task in {spark_job_group_id} has started.") + return {} + + elif path_parts[0] == "query_task_status": + if spark_job_group_id in self.server.task_status_dict: + return {"status": self.server.task_status_dict[spark_job_group_id]} + else: + return {"status": "terminated"} + + elif path_parts[0] == "query_last_worker_err": + return {"last_worker_err": self.server.last_worker_error} + + else: + raise ValueError(f"Illegal request path: {path}") + + def do_POST(self): + """Reads post request body""" + self._set_headers() + content_len = int(self.headers["content-length"]) + content_type = self.headers["content-type"] + assert content_type == "application/json" + path = self.path + post_body = self.rfile.read(content_len).decode("utf-8") + post_body_json = json.loads(post_body) + with self._handler_lock: + response_body_json = self.handle_POST(path, post_body_json) + response_body = json.dumps(response_body_json) + self.wfile.write(response_body.encode("utf-8")) + + def log_request(self, code="-", size="-"): + # Make logs less verbose. + pass + + +class SparkJobServer(ThreadingHTTPServer): + """ + High level design: + + 1. In Ray on spark autoscaling mode, How to start and terminate Ray worker node ? + + It uses spark job to launch Ray worker node, + and each spark job contains only one spark task, the corresponding spark task + creates Ray worker node as subprocess. + When autoscaler request terminating specific Ray worker node, it cancels + corresponding spark job to trigger Ray worker node termination. + Because we can only cancel spark job not spark task when we need to scale + down a Ray worker node. So we have to have one spark job for each Ray worker node. + + 2. How to create / cancel spark job from spark node provider? + + Spark node provider runs in autoscaler process that is different process + than the one that executes "setup_ray_cluster" API. User calls "setup_ray_cluster" + API in spark application driver node, and the semantic is "setup_ray_cluster" + requests spark resources from this spark application. + Internally, "setup_ray_cluster" should use "spark session" instance to request + spark application resources. But spark node provider runs in another python + process, in order to share spark session to the separate NodeProvider process, + it sets up a spark job server that runs inside spark application driver process + (the process that calls "setup_ray_cluster" API), and in NodeProvider process, + it sends RPC request to the spark job server for creating spark jobs in the + spark application. + Note that we cannot create another spark session in NodeProvider process, + because if doing so, it means we create another spark application, and then + it causes NodeProvider requests resources belonging to the new spark application, + but we need to ensure all requested spark resources belong to + the original spark application that calls "setup_ray_cluster" API. + + Note: + The server must inherit ThreadingHTTPServer because request handler uses + the active spark session in current process to create spark jobs, so all request + handler must be running in current process. + """ + + def __init__(self, server_address, spark, ray_node_custom_env): + super().__init__(server_address, SparkJobServerRequestHandler) + self.spark = spark + + # For ray on spark autoscaling mode, + # for each ray worker node, we create an individual spark job + # to launch it, the corresponding spark job has only one + # spark task that starts ray worker node, and the spark job + # is assigned with a unique spark job group ID that is used + # to cancel this spark job (i.e., kill corresponding ray worker node). + # Each spark task has status of pending, running, or terminated. + # the task_status_dict key is spark job group id, + # and value is the corresponding spark task status. + # each spark task holds a ray worker node. + self.task_status_dict = {} + self.last_worker_error = None + self.ray_node_custom_env = ray_node_custom_env + + def shutdown(self) -> None: + super().shutdown() + for spark_job_group_id in self.task_status_dict: + self.spark.sparkContext.cancelJobGroup(spark_job_group_id) + # Sleep 1 second to wait for all spark job cancellation + # The spark job cancellation will do things asyncly in a background thread, + # On Databricks platform, when detaching a notebook, it triggers SIGTERM + # and then sigterm handler triggers Ray cluster shutdown, without sleep, + # after the SIGTERM handler execution the process is killed and then + # these cancelling spark job background threads are killed. + time.sleep(1) + + +def _start_spark_job_server(host, port, spark, ray_node_custom_env): + server = SparkJobServer((host, port), spark, ray_node_custom_env) + + def run_server(): + server.serve_forever() + + server_thread = threading.Thread(target=run_server) + server_thread.setDaemon(True) + server_thread.start() + + return server diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/subprocess_output_util.py b/lib/python3.12/site-packages/ray/autoscaler/_private/subprocess_output_util.py new file mode 100644 index 0000000000000000000000000000000000000000..2aaa2007d3c1721cb11a6c7e6eadfd45dcbc54db --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/subprocess_output_util.py @@ -0,0 +1,420 @@ +import os +import re +import subprocess +import sys +import tempfile +import time + +from ray.autoscaler._private.cli_logger import cf, cli_logger + +CONN_REFUSED_PATIENCE = 30 # how long to wait for sshd to run + +_redirect_output = False # Whether to log command output to a temporary file +_allow_interactive = True # whether to pass on stdin to running commands. + + +def is_output_redirected(): + return _redirect_output + + +def set_output_redirected(val: bool): + """Choose between logging to a temporary file and to `sys.stdout`. + + The default is to log to a file. + + Args: + val: If true, subprocess output will be redirected to + a temporary file. + """ + global _redirect_output + _redirect_output = val + + +def does_allow_interactive(): + return _allow_interactive + + +def set_allow_interactive(val: bool): + """Choose whether to pass on stdin to running commands. + + The default is to pipe stdin and close it immediately. + + Args: + val: If true, stdin will be passed to commands. + """ + global _allow_interactive + _allow_interactive = val + + +class ProcessRunnerError(Exception): + def __init__(self, msg, msg_type, code=None, command=None, special_case=None): + super(ProcessRunnerError, self).__init__( + "{} (discovered={}): type={}, code={}, command={}".format( + msg, special_case, msg_type, code, command + ) + ) + + self.msg_type = msg_type + self.code = code + self.command = command + + self.special_case = special_case + + +_ssh_output_regexes = { + "known_host_update": re.compile( + r"\s*Warning: Permanently added '.+' \(.+\) " r"to the list of known hosts.\s*" + ), + "connection_closed": re.compile(r"\s*Shared connection to .+ closed.\s*"), + "timeout": re.compile( + r"\s*ssh: connect to host .+ port .+: " r"Operation timed out\s*" + ), + "conn_refused": re.compile( + r"\s*ssh: connect to host .+ port .+: Connection refused\s*" + ) + # todo: check for other connection failures for better error messages? +} + + +def _read_subprocess_stream(f, output_file, is_stdout=False): + """Read and process a subprocess output stream. + + The goal is to find error messages and respond to them in a clever way. + Currently just used for SSH messages (CONN_REFUSED, TIMEOUT, etc.), so + the user does not get confused by these. + + Ran in a thread each for both `stdout` and `stderr` to + allow for cross-platform asynchronous IO. + + Note: `select`-based IO is another option, but Windows has + no support for `select`ing pipes, and Linux support varies somewhat. + Spefically, Older *nix systems might also have quirks in how they + handle `select` on pipes. + + Args: + f: File object for the stream. + output_file: File object to which filtered output is written. + is_stdout (bool): + When `is_stdout` is `False`, the stream is assumed to + be `stderr`. Different error message detectors are used, + and the output is displayed to the user unless it matches + a special case (e.g. SSH timeout), in which case this is + left up to the caller. + """ + + detected_special_case = None + while True: + # ! Readline here is crucial. + # ! Normal `read()` will block until EOF instead of until + # something is available. + line = f.readline() + + if line is None or line == "": + # EOF + break + + if line[-1] == "\n": + line = line[:-1] + + if not is_stdout: + if _ssh_output_regexes["connection_closed"].fullmatch(line) is not None: + # Do not log "connection closed" messages which SSH + # puts in stderr for no reason. + # + # They are never errors since the connection will + # close no matter whether the command succeeds or not. + continue + + if _ssh_output_regexes["timeout"].fullmatch(line) is not None: + # Timeout is not really an error but rather a special + # condition. It should be handled by the caller, since + # network conditions/nodes in the early stages of boot + # are expected to sometimes cause connection timeouts. + if detected_special_case is not None: + raise ValueError( + "Bug: ssh_timeout conflicts with another " + "special codition: " + detected_special_case + ) + + detected_special_case = "ssh_timeout" + continue + + if _ssh_output_regexes["conn_refused"].fullmatch(line) is not None: + # Connection refused is not really an error but + # rather a special condition. It should be handled by + # the caller, since network conditions/nodes in the + # early stages of boot are expected to sometimes cause + # CONN_REFUSED. + if detected_special_case is not None: + raise ValueError( + "Bug: ssh_conn_refused conflicts with another " + "special codition: " + detected_special_case + ) + + detected_special_case = "ssh_conn_refused" + continue + + if _ssh_output_regexes["known_host_update"].fullmatch(line) is not None: + # Since we ignore SSH host control anyway + # (-o UserKnownHostsFile=/dev/null), + # we should silence the host control warnings. + continue + + cli_logger.error(line) + + if output_file is not None and output_file != subprocess.DEVNULL: + output_file.write(line + "\n") + + return detected_special_case + + +def _run_and_process_output( + cmd, + stdout_file, + process_runner=subprocess, + stderr_file=None, + use_login_shells=False, +): + """Run a command and process its output for special cases. + + Calls a standard 'check_call' if process_runner is not subprocess. + + Specifically, run all command output through regex to detect + error conditions and filter out non-error messages that went to stderr + anyway (SSH writes ALL of its "system" messages to stderr even if they + are not actually errors). + + Args: + cmd (List[str]): Command to run. + process_runner: Used for command execution. Assumed to have + 'check_call' and 'check_output' inplemented. + stdout_file: File to redirect stdout to. + stderr_file: File to redirect stderr to. + + Implementation notes: + 1. `use_login_shells` disables special processing + If we run interactive apps, output processing will likely get + overwhelmed with the interactive output elements. + Thus, we disable output processing for login shells. This makes + the logging experience considerably worse, but it only degrades + to old-style logging. + + For example, `pip install` outputs HUNDREDS of progress-bar lines + when downloading a package, and we have to + read + regex + write all of them. + + After all, even just printing output to console can often slow + down a fast-printing app, and we do more than just print, and + all that from Python, which is much slower than C regarding + stream processing. + + 2. `stdin=PIPE` for subprocesses + Do not inherit stdin as it messes with bash signals + (ctrl-C for SIGINT) and these commands aren't supposed to + take input anyway. + + 3. `ThreadPoolExecutor` without the `Pool` + We use `ThreadPoolExecutor` to create futures from threads. + Threads are never reused. + + This approach allows us to have no custom synchronization by + off-loading the return value and exception passing to the + standard library (`ThreadPoolExecutor` internals). + + This instance will be `shutdown()` ASAP so it's fine to + create one in such a weird place. + + The code is thus 100% thread-safe as long as the stream readers + are read-only except for return values and possible exceptions. + """ + stdin_overwrite = subprocess.PIPE + # This already should be validated in a higher place of the stack. + assert not ( + does_allow_interactive() and is_output_redirected() + ), "Cannot redirect output while in interactive mode." + if process_runner != subprocess or ( + does_allow_interactive() and not is_output_redirected() + ): + stdin_overwrite = None + + # See implementation note #1 + + if use_login_shells or process_runner != subprocess: + return process_runner.check_call( + cmd, + # See implementation note #2 + stdin=stdin_overwrite, + stdout=stdout_file, + stderr=stderr_file, + ) + + with subprocess.Popen( + cmd, + # See implementation note #2 + stdin=stdin_overwrite, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=1, # line buffering + universal_newlines=True, # text mode outputs + ) as p: + from concurrent.futures import ThreadPoolExecutor + + # Closing stdin might be necessary to signal EOF to some + # apps (they might get stuck waiting for input forever otherwise). + p.stdin.close() + + # See implementation note #3 + with ThreadPoolExecutor(max_workers=2) as executor: + stdout_future = executor.submit( + _read_subprocess_stream, p.stdout, stdout_file, is_stdout=True + ) + stderr_future = executor.submit( + _read_subprocess_stream, p.stderr, stderr_file, is_stdout=False + ) + # Wait for completion. + executor.shutdown() + + # Update `p.returncode` + p.poll() + + detected_special_case = stdout_future.result() + if stderr_future.result() is not None: + if detected_special_case is not None: + # This might some day need to be changed. + # We should probably make sure the two special cases + # are compatible then and that we can handle both by + # e.g. reporting both to the caller. + raise ValueError( + "Bug: found a special case in both stdout and " + "stderr. This is not valid behavior at the time " + "of writing this code." + ) + detected_special_case = stderr_future.result() + + if p.returncode > 0: + # Process failed, but not due to a signal, since signals + # set the exit code to a negative value. + raise ProcessRunnerError( + "Command failed", + "ssh_command_failed", + code=p.returncode, + command=cmd, + special_case=detected_special_case, + ) + elif p.returncode < 0: + # Process failed due to a signal, since signals + # set the exit code to a negative value. + raise ProcessRunnerError( + "Command failed", + "ssh_command_failed", + code=p.returncode, + command=cmd, + special_case="died_to_signal", + ) + + return p.returncode + + +def run_cmd_redirected( + cmd, process_runner=subprocess, silent=False, use_login_shells=False +): + """Run a command and optionally redirect output to a file. + + Args: + cmd (List[str]): Command to run. + process_runner: Process runner used for executing commands. + silent: If true, the command output will be silenced completely + (redirected to /dev/null), unless verbose logging + is enabled. Use this for running utility commands like + rsync. + """ + if silent and cli_logger.verbosity < 1: + return _run_and_process_output( + cmd, + process_runner=process_runner, + stdout_file=process_runner.DEVNULL, + stderr_file=process_runner.DEVNULL, + use_login_shells=use_login_shells, + ) + + if not is_output_redirected(): + return _run_and_process_output( + cmd, + process_runner=process_runner, + stdout_file=sys.stdout, + stderr_file=sys.stderr, + use_login_shells=use_login_shells, + ) + else: + tmpfile_path = os.path.join( + tempfile.gettempdir(), "ray-up-{}-{}.txt".format(cmd[0], time.time()) + ) + with open( + tmpfile_path, + mode="w", + # line buffering + buffering=1, + ) as tmp: + cli_logger.verbose("Command stdout is redirected to {}", cf.bold(tmp.name)) + + return _run_and_process_output( + cmd, + process_runner=process_runner, + stdout_file=tmp, + stderr_file=tmp, + use_login_shells=use_login_shells, + ) + + +def handle_ssh_fails(e, first_conn_refused_time, retry_interval): + """Handle SSH system failures coming from a subprocess. + + Args: + e: The `ProcessRunnerException` to handle. + first_conn_refused_time: + The time (as reported by this function) or None, + indicating the last time a CONN_REFUSED error was caught. + + After exceeding a patience value, the program will be aborted + since SSH will likely never recover. + retry_interval: The interval after which the command will be retried, + used here just to inform the user. + """ + if e.msg_type != "ssh_command_failed": + return + + if e.special_case == "ssh_conn_refused": + if ( + first_conn_refused_time is not None + and time.time() - first_conn_refused_time > CONN_REFUSED_PATIENCE + ): + cli_logger.error( + "SSH connection was being refused " + "for {} seconds. Head node assumed " + "unreachable.", + cf.bold(str(CONN_REFUSED_PATIENCE)), + ) + cli_logger.abort( + "Check the node's firewall settings " + "and the cloud network configuration." + ) + + cli_logger.warning("SSH connection was refused.") + cli_logger.warning( + "This might mean that the SSH daemon is " + "still setting up, or that " + "the host is inaccessable (e.g. due to " + "a firewall)." + ) + + return time.time() + + if e.special_case in ["ssh_timeout", "ssh_conn_refused"]: + cli_logger.print( + "SSH still not available, retrying in {} seconds.", + cf.bold(str(retry_interval)), + ) + else: + raise e + + return first_conn_refused_time diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/updater.py b/lib/python3.12/site-packages/ray/autoscaler/_private/updater.py new file mode 100644 index 0000000000000000000000000000000000000000..68aec7d68475d81e63b5083b70213bc76580052a --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/updater.py @@ -0,0 +1,570 @@ +import logging +import os +import subprocess +import time +import traceback +from threading import Thread + +import click + +from ray._common.usage import usage_constants, usage_lib +from ray.autoscaler._private import subprocess_output_util as cmd_output_util +from ray.autoscaler._private.cli_logger import cf, cli_logger +from ray.autoscaler._private.command_runner import ( + AUTOSCALER_NODE_START_WAIT_S, + ProcessRunnerError, +) +from ray.autoscaler._private.constants import ( + LABELS_ENVIRONMENT_VARIABLE, + RESOURCES_ENVIRONMENT_VARIABLE, +) +from ray.autoscaler._private.event_system import CreateClusterEvent, global_event_system +from ray.autoscaler._private.log_timer import LogTimer +from ray.autoscaler.tags import ( + STATUS_SETTING_UP, + STATUS_SYNCING_FILES, + STATUS_UP_TO_DATE, + STATUS_UPDATE_FAILED, + STATUS_WAITING_FOR_SSH, + TAG_RAY_FILE_MOUNTS_CONTENTS, + TAG_RAY_NODE_STATUS, + TAG_RAY_RUNTIME_CONFIG, +) + +logger = logging.getLogger(__name__) + +NUM_SETUP_STEPS = 7 +READY_CHECK_INTERVAL = 5 + + +class NodeUpdater: + """A process for syncing files and running init commands on a node. + + Arguments: + node_id: the Node ID + provider_config: Provider section of autoscaler yaml + provider: NodeProvider Class + auth_config: Auth section of autoscaler yaml + cluster_name: the name of the cluster. + file_mounts: Map of remote to local paths + initialization_commands: Commands run before container launch + setup_commands: Commands run before ray starts + ray_start_commands: Commands to start ray + runtime_hash: Used to check for config changes + file_mounts_contents_hash: Used to check for changes to file mounts + is_head_node: Whether to use head start/setup commands + rsync_options: Extra options related to the rsync command. + process_runner: the module to use to run the commands + in the CommandRunner. E.g., subprocess. + use_internal_ip: Wwhether the node_id belongs to an internal ip + or external ip. + docker_config: Docker section of autoscaler yaml + restart_only: Whether to skip setup commands & just restart ray + for_recovery: True if updater is for a recovering node. Only used for + metric tracking. + """ + + def __init__( + self, + node_id, + provider_config, + provider, + auth_config, + cluster_name, + file_mounts, + initialization_commands, + setup_commands, + ray_start_commands, + runtime_hash, + file_mounts_contents_hash, + is_head_node, + node_resources=None, + node_labels=None, + cluster_synced_files=None, + rsync_options=None, + process_runner=subprocess, + use_internal_ip=False, + docker_config=None, + restart_only=False, + for_recovery=False, + ): + self.log_prefix = "NodeUpdater: {}: ".format(node_id) + # Three cases: + # 1) use_internal_ip arg is True -> use_internal_ip is True + # 2) worker node -> use value of provider_config["use_internal_ips"] + # 3) head node -> use value of provider_config["use_internal_ips"] unless + # overriden by provider_config["use_external_head_ip"] + use_internal_ip = use_internal_ip or ( + provider_config.get("use_internal_ips", False) + and not ( + is_head_node and provider_config.get("use_external_head_ip", False) + ) + ) + self.cmd_runner = provider.get_command_runner( + self.log_prefix, + node_id, + auth_config, + cluster_name, + process_runner, + use_internal_ip, + docker_config, + ) + + self.daemon = True + self.node_id = node_id + self.provider_type = provider_config.get("type") + self.provider = provider + # Some node providers don't specify empty structures as + # defaults. Better to be defensive. + file_mounts = file_mounts or {} + self.file_mounts = { + remote: os.path.expanduser(local) for remote, local in file_mounts.items() + } + + self.initialization_commands = initialization_commands + self.setup_commands = setup_commands + self.ray_start_commands = ray_start_commands + self.node_resources = node_resources + self.node_labels = node_labels + self.runtime_hash = runtime_hash + self.file_mounts_contents_hash = file_mounts_contents_hash + # TODO (Alex): This makes the assumption that $HOME on the head and + # worker nodes is the same. Also note that `cluster_synced_files` is + # set on the head -> worker updaters only (so `expanduser` is only run + # on the head node). + cluster_synced_files = cluster_synced_files or [] + self.cluster_synced_files = [ + os.path.expanduser(path) for path in cluster_synced_files + ] + self.rsync_options = rsync_options or {} + self.auth_config = auth_config + self.is_head_node = is_head_node + self.docker_config = docker_config + self.restart_only = restart_only + self.update_time = None + self.for_recovery = for_recovery + + def run(self): + update_start_time = time.time() + if ( + cmd_output_util.does_allow_interactive() + and cmd_output_util.is_output_redirected() + ): + # this is most probably a bug since the user has no control + # over these settings + msg = ( + "Output was redirected for an interactive command. " + "Either do not pass `--redirect-command-output` " + "or also pass in `--use-normal-shells`." + ) + cli_logger.abort(msg) + + try: + with LogTimer( + self.log_prefix + "Applied config {}".format(self.runtime_hash) + ): + self.do_update() + except Exception as e: + self.provider.set_node_tags( + self.node_id, {TAG_RAY_NODE_STATUS: STATUS_UPDATE_FAILED} + ) + cli_logger.error("New status: {}", cf.bold(STATUS_UPDATE_FAILED)) + + cli_logger.error("!!!") + if hasattr(e, "cmd"): + stderr_output = getattr(e, "stderr", "No stderr available") + cli_logger.error( + "Setup command `{}` failed with exit code {}. stderr: {}", + cf.bold(e.cmd), + e.returncode, + stderr_output, + ) + else: + cli_logger.verbose_error("Exception details: {}", str(vars(e))) + full_traceback = traceback.format_exc() + cli_logger.error("Full traceback: {}", full_traceback) + # todo: handle this better somehow? + cli_logger.error("Error message: {}", str(e)) + cli_logger.error("!!!") + cli_logger.newline() + + if isinstance(e, click.ClickException): + # todo: why do we ignore this here + return + raise + + tags_to_set = { + TAG_RAY_NODE_STATUS: STATUS_UP_TO_DATE, + TAG_RAY_RUNTIME_CONFIG: self.runtime_hash, + } + if self.file_mounts_contents_hash is not None: + tags_to_set[TAG_RAY_FILE_MOUNTS_CONTENTS] = self.file_mounts_contents_hash + + self.provider.set_node_tags(self.node_id, tags_to_set) + cli_logger.labeled_value("New status", STATUS_UP_TO_DATE) + + self.update_time = time.time() - update_start_time + self.exitcode = 0 + + def sync_file_mounts(self, sync_cmd, step_numbers=(0, 2)): + # step_numbers is (# of previous steps, total steps) + previous_steps, total_steps = step_numbers + + nolog_paths = [] + if cli_logger.verbosity == 0: + nolog_paths = ["~/ray_bootstrap_key.pem", "~/ray_bootstrap_config.yaml"] + + def do_sync(remote_path, local_path, allow_non_existing_paths=False): + if allow_non_existing_paths and not os.path.exists(local_path): + cli_logger.print("sync: {} does not exist. Skipping.", local_path) + # Ignore missing source files. In the future we should support + # the --delete-missing-args command to delete files that have + # been removed + return + + assert os.path.exists(local_path), local_path + + if os.path.isdir(local_path): + if not local_path.endswith("/"): + local_path += "/" + if not remote_path.endswith("/"): + remote_path += "/" + + with LogTimer( + self.log_prefix + "Synced {} to {}".format(local_path, remote_path) + ): + is_docker = ( + self.docker_config and self.docker_config["container_name"] != "" + ) + if not is_docker: + # The DockerCommandRunner handles this internally. + self.cmd_runner.run( + "mkdir -p {}".format(os.path.dirname(remote_path)), + run_env="host", + ) + sync_cmd(local_path, remote_path, docker_mount_if_possible=True) + + if remote_path not in nolog_paths: + # todo: timed here? + cli_logger.print( + "{} from {}", cf.bold(remote_path), cf.bold(local_path) + ) + + # Rsync file mounts + with cli_logger.group( + "Processing file mounts", _numbered=("[]", previous_steps + 1, total_steps) + ): + for remote_path, local_path in self.file_mounts.items(): + do_sync(remote_path, local_path) + previous_steps += 1 + + if self.cluster_synced_files: + with cli_logger.group( + "Processing worker file mounts", + _numbered=("[]", previous_steps + 1, total_steps), + ): + cli_logger.print("synced files: {}", str(self.cluster_synced_files)) + for path in self.cluster_synced_files: + do_sync(path, path, allow_non_existing_paths=True) + previous_steps += 1 + else: + cli_logger.print( + "No worker file mounts to sync", + _numbered=("[]", previous_steps + 1, total_steps), + ) + + def wait_ready(self, deadline): + with cli_logger.group( + "Waiting for SSH to become available", _numbered=("[]", 1, NUM_SETUP_STEPS) + ): + with LogTimer(self.log_prefix + "Got remote shell"): + cli_logger.print("Running `{}` as a test.", cf.bold("uptime")) + first_conn_refused_time = None + while True: + if time.time() > deadline: + raise Exception("wait_ready timeout exceeded.") + if self.provider.is_terminated(self.node_id): + raise Exception( + "wait_ready aborting because node " + "detected as terminated." + ) + + try: + # Run outside of the container + self.cmd_runner.run("uptime", timeout=10, run_env="host") + cli_logger.success("Success.") + return True + except ProcessRunnerError as e: + first_conn_refused_time = cmd_output_util.handle_ssh_fails( + e, + first_conn_refused_time, + retry_interval=READY_CHECK_INTERVAL, + ) + time.sleep(READY_CHECK_INTERVAL) + except Exception as e: + # TODO(maximsmol): we should not be ignoring + # exceptions if they get filtered properly + # (new style log + non-interactive shells) + # + # however threading this configuration state + # is a pain and I'm leaving it for later + + retry_str = "(" + str(e) + ")" + if hasattr(e, "cmd"): + if isinstance(e.cmd, str): + cmd_ = e.cmd + elif isinstance(e.cmd, list): + cmd_ = " ".join(e.cmd) + else: + logger.debug( + f"e.cmd type ({type(e.cmd)}) not list or str." + ) + cmd_ = str(e.cmd) + retry_str = "(Exit Status {}): {}".format( + e.returncode, cmd_ + ) + + cli_logger.print( + "SSH still not available {}, retrying in {} seconds.", + cf.dimmed(retry_str), + cf.bold(str(READY_CHECK_INTERVAL)), + ) + + time.sleep(READY_CHECK_INTERVAL) + + def do_update(self): + self.provider.set_node_tags( + self.node_id, {TAG_RAY_NODE_STATUS: STATUS_WAITING_FOR_SSH} + ) + cli_logger.labeled_value("New status", STATUS_WAITING_FOR_SSH) + + deadline = time.time() + AUTOSCALER_NODE_START_WAIT_S + self.wait_ready(deadline) + global_event_system.execute_callback(CreateClusterEvent.ssh_control_acquired) + + node_tags = self.provider.node_tags(self.node_id) + logger.debug("Node tags: {}".format(str(node_tags))) + + if self.provider_type == "aws" and self.provider.provider_config: + from ray.autoscaler._private.aws.cloudwatch.cloudwatch_helper import ( + CloudwatchHelper, + ) + + CloudwatchHelper( + self.provider.provider_config, self.node_id, self.provider.cluster_name + ).update_from_config(self.is_head_node) + + if node_tags.get(TAG_RAY_RUNTIME_CONFIG) == self.runtime_hash: + # When resuming from a stopped instance the runtime_hash may be the + # same, but the container will not be started. + init_required = self.cmd_runner.run_init( + as_head=self.is_head_node, + file_mounts=self.file_mounts, + sync_run_yet=False, + ) + if init_required: + node_tags[TAG_RAY_RUNTIME_CONFIG] += "-invalidate" + # This ensures that `setup_commands` are not removed + self.restart_only = False + + if self.restart_only: + self.setup_commands = [] + + # runtime_hash will only change whenever the user restarts + # or updates their cluster with `get_or_create_head_node` + if node_tags.get(TAG_RAY_RUNTIME_CONFIG) == self.runtime_hash and ( + not self.file_mounts_contents_hash + or node_tags.get(TAG_RAY_FILE_MOUNTS_CONTENTS) + == self.file_mounts_contents_hash + ): + # todo: we lie in the confirmation message since + # full setup might be cancelled here + cli_logger.print( + "Configuration already up to date, " + "skipping file mounts, initalization and setup commands.", + _numbered=("[]", "2-6", NUM_SETUP_STEPS), + ) + + else: + cli_logger.print( + "Updating cluster configuration.", _tags=dict(hash=self.runtime_hash) + ) + + self.provider.set_node_tags( + self.node_id, {TAG_RAY_NODE_STATUS: STATUS_SYNCING_FILES} + ) + cli_logger.labeled_value("New status", STATUS_SYNCING_FILES) + self.sync_file_mounts(self.rsync_up, step_numbers=(1, NUM_SETUP_STEPS)) + + # Only run setup commands if runtime_hash has changed because + # we don't want to run setup_commands every time the head node + # file_mounts folders have changed. + if node_tags.get(TAG_RAY_RUNTIME_CONFIG) != self.runtime_hash: + # Run init commands + self.provider.set_node_tags( + self.node_id, {TAG_RAY_NODE_STATUS: STATUS_SETTING_UP} + ) + cli_logger.labeled_value("New status", STATUS_SETTING_UP) + + if self.initialization_commands: + with cli_logger.group( + "Running initialization commands", + _numbered=("[]", 4, NUM_SETUP_STEPS), + ): + global_event_system.execute_callback( + CreateClusterEvent.run_initialization_cmd + ) + with LogTimer( + self.log_prefix + "Initialization commands", + show_status=True, + ): + for cmd in self.initialization_commands: + global_event_system.execute_callback( + CreateClusterEvent.run_initialization_cmd, + {"command": cmd}, + ) + try: + # Overriding the existing SSHOptions class + # with a new SSHOptions class that uses + # this ssh_private_key as its only __init__ + # argument. + # Run outside docker. + self.cmd_runner.run( + cmd, + ssh_options_override_ssh_key=self.auth_config.get( # noqa: E501 + "ssh_private_key" + ), + run_env="host", + ) + except ProcessRunnerError as e: + if e.msg_type == "ssh_command_failed": + cli_logger.error("Failed.") + cli_logger.error("See above for stderr.") + + raise click.ClickException( + "Initialization command failed." + ) from None + else: + cli_logger.print( + "No initialization commands to run.", + _numbered=("[]", 4, NUM_SETUP_STEPS), + ) + with cli_logger.group( + "Initializing command runner", + # todo: fix command numbering + _numbered=("[]", 5, NUM_SETUP_STEPS), + ): + self.cmd_runner.run_init( + as_head=self.is_head_node, + file_mounts=self.file_mounts, + sync_run_yet=True, + ) + if self.setup_commands: + with cli_logger.group( + "Running setup commands", + # todo: fix command numbering + _numbered=("[]", 6, NUM_SETUP_STEPS), + ): + global_event_system.execute_callback( + CreateClusterEvent.run_setup_cmd + ) + with LogTimer( + self.log_prefix + "Setup commands", show_status=True + ): + total = len(self.setup_commands) + for i, cmd in enumerate(self.setup_commands): + global_event_system.execute_callback( + CreateClusterEvent.run_setup_cmd, {"command": cmd} + ) + if cli_logger.verbosity == 0 and len(cmd) > 30: + cmd_to_print = cf.bold(cmd[:30]) + "..." + else: + cmd_to_print = cf.bold(cmd) + + cli_logger.print( + "{}", cmd_to_print, _numbered=("()", i, total) + ) + + try: + # Runs in the container if docker is in use + self.cmd_runner.run(cmd, run_env="auto") + except ProcessRunnerError as e: + if e.msg_type == "ssh_command_failed": + cli_logger.error("Failed.") + cli_logger.error("See above for stderr.") + + raise click.ClickException("Setup command failed.") + else: + cli_logger.print( + "No setup commands to run.", + _numbered=("[]", 6, NUM_SETUP_STEPS), + ) + + with cli_logger.group( + "Starting the Ray runtime", _numbered=("[]", 7, NUM_SETUP_STEPS) + ): + global_event_system.execute_callback(CreateClusterEvent.start_ray_runtime) + with LogTimer(self.log_prefix + "Ray start commands", show_status=True): + for cmd in self.ray_start_commands: + env_vars = {} + if self.is_head_node: + if usage_lib.usage_stats_enabled(): + env_vars[usage_constants.USAGE_STATS_ENABLED_ENV_VAR] = 1 + else: + # Disable usage stats collection in the cluster. + env_vars[usage_constants.USAGE_STATS_ENABLED_ENV_VAR] = 0 + + # Add a resource override env variable if needed. + # Local NodeProvider doesn't need resource and label override. + if self.provider_type != "local": + if self.node_resources: + env_vars[ + RESOURCES_ENVIRONMENT_VARIABLE + ] = self.node_resources + if self.node_labels: + env_vars[LABELS_ENVIRONMENT_VARIABLE] = self.node_labels + + try: + old_redirected = cmd_output_util.is_output_redirected() + cmd_output_util.set_output_redirected(False) + # Runs in the container if docker is in use + self.cmd_runner.run( + cmd, environment_variables=env_vars, run_env="auto" + ) + cmd_output_util.set_output_redirected(old_redirected) + except ProcessRunnerError as e: + if e.msg_type == "ssh_command_failed": + cli_logger.error("Failed.") + cli_logger.error("See above for stderr.") + + raise click.ClickException("Start command failed.") + global_event_system.execute_callback( + CreateClusterEvent.start_ray_runtime_completed + ) + + def rsync_up(self, source, target, docker_mount_if_possible=False): + options = {} + options["docker_mount_if_possible"] = docker_mount_if_possible + options["rsync_exclude"] = self.rsync_options.get("rsync_exclude") + options["rsync_filter"] = self.rsync_options.get("rsync_filter") + self.cmd_runner.run_rsync_up(source, target, options=options) + cli_logger.verbose( + "`rsync`ed {} (local) to {} (remote)", cf.bold(source), cf.bold(target) + ) + + def rsync_down(self, source, target, docker_mount_if_possible=False): + options = {} + options["docker_mount_if_possible"] = docker_mount_if_possible + options["rsync_exclude"] = self.rsync_options.get("rsync_exclude") + options["rsync_filter"] = self.rsync_options.get("rsync_filter") + self.cmd_runner.run_rsync_down(source, target, options=options) + cli_logger.verbose( + "`rsync`ed {} (remote) to {} (local)", cf.bold(source), cf.bold(target) + ) + + +class NodeUpdaterThread(NodeUpdater, Thread): + def __init__(self, *args, **kwargs): + Thread.__init__(self) + NodeUpdater.__init__(self, *args, **kwargs) + self.exitcode = -1 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/util.py b/lib/python3.12/site-packages/ray/autoscaler/_private/util.py new file mode 100644 index 0000000000000000000000000000000000000000..dcdbacb337bdd0cbea151a912977f1a134b32fa8 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/util.py @@ -0,0 +1,1043 @@ +import collections +import copy +import hashlib +import json +import logging +import os +import sys +import threading +from dataclasses import dataclass +from datetime import datetime +from io import StringIO +from numbers import Number, Real +from typing import Any, Dict, List, Optional, Tuple, Union + +import ray +import ray._private.services as services +from ray._common.utils import PLACEMENT_GROUP_BUNDLE_RESOURCE_NAME +from ray._private.utils import ( + PLACEMENT_GROUP_INDEXED_BUNDLED_RESOURCE_PATTERN, + PLACEMENT_GROUP_WILDCARD_RESOURCE_PATTERN, +) +from ray.autoscaler._private import constants +from ray.autoscaler._private.cli_logger import cli_logger +from ray.autoscaler._private.docker import validate_docker_config +from ray.autoscaler._private.local.config import prepare_local +from ray.autoscaler._private.providers import _get_default_config +from ray.autoscaler.tags import NODE_TYPE_LEGACY_HEAD, NODE_TYPE_LEGACY_WORKER + +REQUIRED, OPTIONAL = True, False + + +HEAD_TYPE_MAX_WORKERS_WARN_TEMPLATE = ( + "Setting `max_workers` for node type" + " `{node_type}` to the global `max_workers` value of {max_workers}. To" + " avoid spawning worker nodes of type `{node_type}`, explicitly set" + " `max_workers: 0` for `{node_type}`.\n" + "Note that `max_workers: 0` was the default value prior to Ray 1.3.0." + " Your current version is Ray {version}.\n" + "See the docs for more information:\n" + "https://docs.ray.io/en/master/cluster/config.html" + "#cluster-configuration-node-max-workers\n" + "https://docs.ray.io/en/master/cluster/config.html#full-configuration" +) + +ResourceBundle = Dict[str, Union[int, float]] + +# A Dict and the count of how many times it occurred. +# Refer to freq_of_dicts() below. +DictCount = Tuple[Dict, Number] + +# e.g., cpu_4_ondemand. +NodeType = str + +# e.g., head, worker, unmanaged +NodeKind = str + +# e.g., {"resources": ..., "max_workers": ...}. +NodeTypeConfigDict = Dict[str, Any] + +# e.g., {"GPU": 1}. +ResourceDict = Dict[str, Real] + +# e.g., "node-1". +NodeID = str + +# e.g., "127.0.0.1". +NodeIP = str + +# Number of nodes to launch +NodeCount = int + +# e.g. "up-to-date", "update-failed" +# See autoscaler/tags.py for other status +# values used by the autoscaler. +NodeStatus = str + +Usage = Dict[str, Tuple[Number, Number]] + +logger = logging.getLogger(__name__) + + +def is_placement_group_resource(resource_name: str) -> bool: + """ + Check if a resource name is structured like a placement group. + """ + return bool( + PLACEMENT_GROUP_WILDCARD_RESOURCE_PATTERN.match(resource_name) + or PLACEMENT_GROUP_INDEXED_BUNDLED_RESOURCE_PATTERN.match(resource_name) + ) + + +@dataclass +class LoadMetricsSummary: + # Map of resource name (e.g. "memory") to pair of (Used, Available) numbers + usage: Usage + # Counts of demand bundles from task/actor demand. + # e.g. [({"CPU": 1}, 5), ({"GPU":1}, 2)] + resource_demand: List[DictCount] + # Counts of pending placement groups + pg_demand: List[DictCount] + # Counts of demand bundles requested by autoscaler.sdk.request_resources + request_demand: List[DictCount] + node_types: List[DictCount] + # Optionally included for backwards compatibility: IP of the head node. See + # https://github.com/ray-project/ray/pull/20623 for details. + head_ip: Optional[NodeIP] = None + # Optionally included for backwards compatibility: Resource breakdown by + # node. Mapping from node id to resource usage. + usage_by_node: Optional[Dict[str, Usage]] = None + # A mapping from node name (the same key as `usage_by_node`) to node type. + # Optional for deployment modes which have the concept of node types and + # backwards compatibility. + node_type_mapping: Optional[Dict[str, str]] = None + idle_time_map: Optional[Dict[str, int]] = None + + +class ConcurrentCounter: + def __init__(self): + self._lock = threading.RLock() + self._counter = collections.defaultdict(int) + + def inc(self, key, count): + with self._lock: + self._counter[key] += count + return self.value + + def dec(self, key, count): + with self._lock: + self._counter[key] -= count + assert self._counter[key] >= 0, "counter cannot go negative" + return self.value + + def breakdown(self): + with self._lock: + return dict(self._counter) + + @property + def value(self): + with self._lock: + return sum(self._counter.values()) + + +def validate_config(config: Dict[str, Any]) -> None: + """Required Dicts indicate that no extra fields can be introduced.""" + if not isinstance(config, dict): + raise ValueError("Config {} is not a dictionary".format(config)) + + schema_path = os.path.join( + os.path.dirname(ray.autoscaler.__file__), "ray-schema.json" + ) + with open(schema_path) as f: + schema = json.load(f) + + try: + import jsonschema + except (ModuleNotFoundError, ImportError) as e: + # Don't log a warning message here. Logging be handled by upstream. + raise e from None + + try: + jsonschema.validate(config, schema) + except jsonschema.ValidationError as e: + raise e from None + + # Detect out of date defaults. This happens when the autoscaler that filled + # out the default values is older than the version of the autoscaler that + # is running on the cluster. + if "cluster_synced_files" not in config: + raise RuntimeError( + "Missing 'cluster_synced_files' field in the cluster " + "configuration. This is likely due to the Ray version running " + "in the cluster {ray_version} is greater than the Ray version " + "running on your laptop. Please try updating Ray on your local " + "machine and make sure the versions match.".format( + ray_version=ray.__version__ + ) + ) + + if "available_node_types" in config: + if "head_node_type" not in config: + raise ValueError( + "You must specify `head_node_type` if `available_node_types is set." + ) + if config["head_node_type"] not in config["available_node_types"]: + raise ValueError("`head_node_type` must be one of `available_node_types`.") + + sum_min_workers = sum( + config["available_node_types"][node_type].get("min_workers", 0) + for node_type in config["available_node_types"] + ) + if sum_min_workers > config["max_workers"]: + raise ValueError( + "The specified global `max_workers` is smaller than the " + "sum of `min_workers` of all the available node types." + ) + + if sys.platform == "win32" and config.get("file_mounts_sync_continuously", False): + raise ValueError( + "`file_mounts_sync_continuously` is not supported on Windows. " + "Please set this to False when running on Windows." + ) + + +def check_legacy_fields(config: Dict[str, Any]) -> None: + """For use in providers that have completed the migration to + available_node_types. + + Warns user that head_node and worker_nodes fields are being ignored. + Throws an error if available_node_types and head_node_type aren't + specified. + """ + # log warning if non-empty head_node field + if "head_node" in config and config["head_node"]: + cli_logger.warning( + "The `head_node` field is deprecated and will be ignored. " + "Use `head_node_type` and `available_node_types` instead." + ) + # log warning if non-empty worker_nodes field + if "worker_nodes" in config and config["worker_nodes"]: + cli_logger.warning( + "The `worker_nodes` field is deprecated and will be ignored. " + "Use `available_node_types` instead." + ) + if "available_node_types" not in config: + cli_logger.error("`available_node_types` not specified in config") + raise ValueError("`available_node_types` not specified in config") + if "head_node_type" not in config: + cli_logger.error("`head_node_type` not specified in config") + raise ValueError("`head_node_type` not specified in config") + + +def prepare_config(config: Dict[str, Any]) -> Dict[str, Any]: + """ + The returned config has the following properties: + - Uses the multi-node-type autoscaler configuration. + - Merged with the appropriate defaults.yaml + - Has a valid Docker configuration if provided. + - Has max_worker set for each node type. + """ + is_local = config.get("provider", {}).get("type") == "local" + is_kuberay = config.get("provider", {}).get("type") == "kuberay" + if is_local: + config = prepare_local(config) + elif is_kuberay: + # With KubeRay, we don't need to do anything here since KubeRay + # generate the autoscaler config from the RayCluster CR instead + # of loading from the files. + return config + + with_defaults = fillout_defaults(config) + merge_setup_commands(with_defaults) + validate_docker_config(with_defaults) + fill_node_type_min_max_workers(with_defaults) + return with_defaults + + +def translate_trivial_legacy_config(config: Dict[str, Any]): + """ + Drop empty deprecated fields ("head_node" and "worker_node"). + """ + + REMOVABLE_FIELDS = ["head_node", "worker_nodes"] + + for field in REMOVABLE_FIELDS: + if field in config and not config[field]: + logger.warning( + f"Dropping the empty legacy field {field}. {field}" + "is not supported for ray>=2.0.0. It is recommended to remove" + f"{field} from the cluster config." + ) + del config[field] + + +def fillout_defaults(config: Dict[str, Any]) -> Dict[str, Any]: + defaults = _get_default_config(config["provider"]) + defaults.update(config) + + # Just for clarity: + merged_config = copy.deepcopy(defaults) + + # Fill auth field to avoid key errors. + # This field is accessed when calling NodeUpdater but is not relevant to + # certain node providers and is thus left out of some cluster launching + # configs. + merged_config["auth"] = merged_config.get("auth", {}) + + # A legacy config is one which doesn't have available_node_types, + # but has at least one of head_node or worker_nodes. + is_legacy_config = ("available_node_types" not in config) and ( + "head_node" in config or "worker_nodes" in config + ) + # Do merging logic for legacy configs. + if is_legacy_config: + merged_config = merge_legacy_yaml_with_defaults(merged_config) + # Take care of this here, in case a config does not specify any of head, + # workers, node types, but does specify min workers: + merged_config.pop("min_workers", None) + + translate_trivial_legacy_config(merged_config) + + return merged_config + + +def merge_legacy_yaml_with_defaults(merged_config: Dict[str, Any]) -> Dict[str, Any]: + """Rewrite legacy config's available node types after it has been merged + with defaults yaml. + """ + cli_logger.warning( + "Converting legacy cluster config to a multi node type cluster " + "config. Multi-node-type cluster configs are the recommended " + "format for configuring Ray clusters. " + "See the docs for more information:\n" + "https://docs.ray.io/en/master/cluster/config.html#full-configuration" + ) + + # Get default head and worker types. + default_head_type = merged_config["head_node_type"] + # Default configs are assumed to have two node types -- one for the head + # and one for the workers. + assert len(merged_config["available_node_types"].keys()) == 2 + default_worker_type = ( + merged_config["available_node_types"].keys() - {default_head_type} + ).pop() + + if merged_config["head_node"]: + # User specified a head node in legacy config. + # Convert it into data for the head's node type. + head_node_info = { + "node_config": merged_config["head_node"], + "resources": merged_config["head_node"].get("resources") or {}, + "min_workers": 0, + "max_workers": 0, + } + else: + # Use default data for the head's node type. + head_node_info = merged_config["available_node_types"][default_head_type] + if merged_config["worker_nodes"]: + # User specified a worker node in legacy config. + # Convert it into data for the workers' node type. + worker_node_info = { + "node_config": merged_config["worker_nodes"], + "resources": merged_config["worker_nodes"].get("resources") or {}, + "min_workers": merged_config.get("min_workers", 0), + "max_workers": merged_config["max_workers"], + } + else: + # Use default data for the workers' node type. + worker_node_info = merged_config["available_node_types"][default_worker_type] + + # Rewrite available_node_types. + merged_config["available_node_types"] = { + NODE_TYPE_LEGACY_HEAD: head_node_info, + NODE_TYPE_LEGACY_WORKER: worker_node_info, + } + merged_config["head_node_type"] = NODE_TYPE_LEGACY_HEAD + + # Resources field in head/worker fields cause node launch to fail. + merged_config["head_node"].pop("resources", None) + merged_config["worker_nodes"].pop("resources", None) + + return merged_config + + +def merge_setup_commands(config): + config["head_setup_commands"] = ( + config["setup_commands"] + config["head_setup_commands"] + ) + config["worker_setup_commands"] = ( + config["setup_commands"] + config["worker_setup_commands"] + ) + return config + + +def fill_node_type_min_max_workers(config): + """Sets default per-node max workers to global max_workers. + This equivalent to setting the default per-node max workers to infinity, + with the only upper constraint coming from the global max_workers. + Sets default per-node min workers to zero. + Also sets default max_workers for the head node to zero. + """ + assert "max_workers" in config, "Global max workers should be set." + node_types = config["available_node_types"] + for node_type_name in node_types: + node_type_data = node_types[node_type_name] + + node_type_data.setdefault("min_workers", 0) + if "max_workers" not in node_type_data: + if node_type_name == config["head_node_type"]: + logger.info("setting max workers for head node type to 0") + node_type_data.setdefault("max_workers", 0) + else: + global_max_workers = config["max_workers"] + logger.info( + f"setting max workers for {node_type_name} to " + f"{global_max_workers}" + ) + node_type_data.setdefault("max_workers", global_max_workers) + + +def with_envs(cmds: List[str], kv: Dict[str, str]) -> str: + """ + Returns a list of commands with the given environment variables set. + + Args: + cmds (List[str]): List of commands to set environment variables for. + kv (Dict[str, str]): Dictionary of environment variables to set. + + Returns: + List[str]: List of commands with the given environment variables set. + + Example: + with_envs(["echo $FOO"], {"FOO": "BAR"}) + -> ["export FOO=BAR; echo $FOO"] + """ + out_cmds = [] + for cmd in cmds: + kv_str = "" + for k, v in kv.items(): + # We will need to do export here so that it works correctly with + # shell if the cmd args uses the argument. + kv_str += f"export {k}={v}; " + + out_cmds.append(f"{kv_str}{cmd}") + return out_cmds + + +def with_head_node_ip(cmds, head_ip=None): + if head_ip is None: + head_ip = services.get_node_ip_address() + return with_envs(cmds, {"RAY_HEAD_IP": head_ip}) + + +def hash_launch_conf(node_conf, auth): + hasher = hashlib.sha1() + # For hashing, we replace the path to the key with the + # key itself. This is to make sure the hashes are the + # same even if keys live at different locations on different + # machines. + full_auth = auth.copy() + for key_type in ["ssh_private_key", "ssh_public_key"]: + if key_type in auth: + with open(os.path.expanduser(auth[key_type])) as key: + full_auth[key_type] = key.read() + hasher.update(json.dumps([node_conf, full_auth], sort_keys=True).encode("utf-8")) + return hasher.hexdigest() + + +# Cache the file hashes to avoid rescanning it each time. Also, this avoids +# inadvertently restarting workers if the file mount content is mutated on the +# head node. +_hash_cache = {} + + +def hash_runtime_conf( + file_mounts, + cluster_synced_files, + extra_objs, + generate_file_mounts_contents_hash=False, +): + """Returns two hashes, a runtime hash and file_mounts_content hash. + + The runtime hash is used to determine if the configuration or file_mounts + contents have changed. It is used at launch time (ray up) to determine if + a restart is needed. + + The file_mounts_content hash is used to determine if the file_mounts or + cluster_synced_files contents have changed. It is used at monitor time to + determine if additional file syncing is needed. + """ + runtime_hasher = hashlib.sha1() + contents_hasher = hashlib.sha1() + + def add_content_hashes(path, allow_non_existing_paths: bool = False): + def add_hash_of_file(fpath): + with open(fpath, "rb") as f: + for chunk in iter(lambda: f.read(2**20), b""): + contents_hasher.update(chunk) + + path = os.path.expanduser(path) + if allow_non_existing_paths and not os.path.exists(path): + return + if os.path.isdir(path): + dirs = [] + for dirpath, _, filenames in os.walk(path): + dirs.append((dirpath, sorted(filenames))) + for dirpath, filenames in sorted(dirs): + contents_hasher.update(dirpath.encode("utf-8")) + for name in filenames: + contents_hasher.update(name.encode("utf-8")) + fpath = os.path.join(dirpath, name) + add_hash_of_file(fpath) + else: + add_hash_of_file(path) + + conf_str = json.dumps(file_mounts, sort_keys=True).encode("utf-8") + json.dumps( + extra_objs, sort_keys=True + ).encode("utf-8") + + # Only generate a contents hash if generate_contents_hash is true or + # if we need to generate the runtime_hash + if conf_str not in _hash_cache or generate_file_mounts_contents_hash: + for local_path in sorted(file_mounts.values()): + add_content_hashes(local_path) + head_node_contents_hash = contents_hasher.hexdigest() + + # Generate a new runtime_hash if its not cached + # The runtime hash does not depend on the cluster_synced_files hash + # because we do not want to restart nodes only if cluster_synced_files + # contents have changed. + if conf_str not in _hash_cache: + runtime_hasher.update(conf_str) + runtime_hasher.update(head_node_contents_hash.encode("utf-8")) + _hash_cache[conf_str] = runtime_hasher.hexdigest() + + # Add cluster_synced_files to the file_mounts_content hash + if cluster_synced_files is not None: + for local_path in sorted(cluster_synced_files): + # For cluster_synced_files, we let the path be non-existant + # because its possible that the source directory gets set up + # anytime over the life of the head node. + add_content_hashes(local_path, allow_non_existing_paths=True) + + file_mounts_contents_hash = contents_hasher.hexdigest() + + else: + file_mounts_contents_hash = None + + return (_hash_cache[conf_str], file_mounts_contents_hash) + + +def add_prefix(info_string, prefix): + """Prefixes each line of info_string, except the first, by prefix.""" + lines = info_string.split("\n") + prefixed_lines = [lines[0]] + for line in lines[1:]: + prefixed_line = ":".join([prefix, line]) + prefixed_lines.append(prefixed_line) + prefixed_info_string = "\n".join(prefixed_lines) + return prefixed_info_string + + +def format_pg(pg): + strategy = pg["strategy"] + bundles = pg["bundles"] + shape_strs = [] + for bundle, count in bundles: + shape_strs.append(f"{bundle} * {count}") + bundles_str = ", ".join(shape_strs) + return f"{bundles_str} ({strategy})" + + +def parse_placement_group_resource_str( + placement_group_resource_str: str, +) -> Tuple[str, Optional[str], bool]: + """Parse placement group resource in the form of following 3 cases: + {resource_name}_group_{bundle_id}_{group_name}; + -> This case is ignored as it is duplicated to the case below. + {resource_name}_group_{group_name}; + {resource_name} + + Returns: + Tuple of (resource_name, placement_group_name, is_countable_resource). + placement_group_name could be None if its not a placement group + resource. is_countable_resource is True if the resource + doesn't contain bundle index. We shouldn't count resources + with bundle index because it will + have duplicated resource information as + wildcard resources (resource name without bundle index). + """ + result = PLACEMENT_GROUP_INDEXED_BUNDLED_RESOURCE_PATTERN.match( + placement_group_resource_str + ) + if result: + return (result.group(1), result.group(3), False) + result = PLACEMENT_GROUP_WILDCARD_RESOURCE_PATTERN.match( + placement_group_resource_str + ) + if result: + return (result.group(1), result.group(2), True) + return (placement_group_resource_str, None, True) + + +MEMORY_SUFFIXES = [ + ("TiB", 2**40), + ("GiB", 2**30), + ("MiB", 2**20), + ("KiB", 2**10), +] + + +def format_memory(mem_bytes: Number) -> str: + """Formats memory in bytes in friendly unit. E.g. (2**30 + 1) bytes should + be displayed as 1GiB but 1 byte should be displayed as 1B, (as opposed to + rounding it to 0GiB). + """ + for suffix, bytes_per_unit in MEMORY_SUFFIXES: + if mem_bytes >= bytes_per_unit: + mem_in_unit = mem_bytes / bytes_per_unit + return f"{mem_in_unit:.2f}{suffix}" + + return f"{int(mem_bytes)}B" + + +def parse_usage(usage: Usage, verbose: bool) -> List[str]: + # first collect resources used in placement groups + placement_group_resource_usage = {} + placement_group_resource_total = collections.defaultdict(float) + for resource, (used, total) in usage.items(): + (pg_resource_name, pg_name, is_countable) = parse_placement_group_resource_str( + resource + ) + if pg_name: + if pg_resource_name not in placement_group_resource_usage: + placement_group_resource_usage[pg_resource_name] = 0 + if is_countable: + placement_group_resource_usage[pg_resource_name] += used + placement_group_resource_total[pg_resource_name] += total + continue + usage_lines = [] + for resource, (used, total) in sorted(usage.items()): + if "node:" in resource: + continue # Skip the auto-added per-node "node:" resource. + + (_, pg_name, _) = parse_placement_group_resource_str(resource) + if pg_name: + continue # Skip resource used by placement groups + + pg_used = 0 + pg_total = 0 + used_in_pg = resource in placement_group_resource_usage + if used_in_pg: + pg_used = placement_group_resource_usage[resource] + pg_total = placement_group_resource_total[resource] + # Used includes pg_total because when pgs are created + # it allocates resources. + # To get the real resource usage, we should subtract the pg + # reserved resources from the usage and add pg used instead. + used = used - pg_total + pg_used + + if resource in ["memory", "object_store_memory"]: + formatted_used = format_memory(used) + formatted_total = format_memory(total) + line = f"{formatted_used}/{formatted_total} {resource}" + if used_in_pg: + formatted_pg_used = format_memory(pg_used) + formatted_pg_total = format_memory(pg_total) + line = line + ( + f" ({formatted_pg_used} used of " + f"{formatted_pg_total} " + "reserved in placement groups)" + ) + usage_lines.append(line) + elif resource.startswith("accelerator_type:") and not verbose: + # We made a judgement call not to show this. + # https://github.com/ray-project/ray/issues/33272 + pass + else: + line = f"{used}/{total} {resource}" + if used_in_pg: + line += ( + f" ({pg_used} used of " f"{pg_total} reserved in placement groups)" + ) + usage_lines.append(line) + return usage_lines + + +def get_usage_report(lm_summary: LoadMetricsSummary, verbose: bool) -> str: + usage_lines = parse_usage(lm_summary.usage, verbose) + + sio = StringIO() + for line in usage_lines: + print(f" {line}", file=sio) + return sio.getvalue() + + +def format_resource_demand_summary( + resource_demand: List[Tuple[ResourceBundle, int]] +) -> List[str]: + def filter_placement_group_from_bundle(bundle: ResourceBundle): + """filter placement group from bundle resource name. returns + filtered bundle and a bool indicate if the bundle is using + placement group. + + Example: {"CPU_group_groupid": 1} returns {"CPU": 1}, True + {"memory": 1} return {"memory": 1}, False + """ + using_placement_group = False + result_bundle = dict() + for pg_resource_str, resource_count in bundle.items(): + (resource_name, pg_name, _) = parse_placement_group_resource_str( + pg_resource_str + ) + result_bundle[resource_name] = resource_count + if pg_name: + using_placement_group = True + return (result_bundle, using_placement_group) + + bundle_demand = collections.defaultdict(int) + pg_bundle_demand = collections.defaultdict(int) + + for bundle, count in resource_demand: + ( + pg_filtered_bundle, + using_placement_group, + ) = filter_placement_group_from_bundle(bundle) + + # bundle is a special keyword for placement group scheduling + # but it doesn't need to be exposed to users. Remove it from + # the demand report. + if ( + using_placement_group + and PLACEMENT_GROUP_BUNDLE_RESOURCE_NAME in pg_filtered_bundle.keys() + ): + del pg_filtered_bundle[PLACEMENT_GROUP_BUNDLE_RESOURCE_NAME] + + # No need to report empty request to demand (e.g., + # placement group ready task). + if len(pg_filtered_bundle.keys()) == 0: + continue + + bundle_demand[tuple(sorted(pg_filtered_bundle.items()))] += count + if using_placement_group: + pg_bundle_demand[tuple(sorted(pg_filtered_bundle.items()))] += count + + demand_lines = [] + for bundle, count in bundle_demand.items(): + line = f" {dict(bundle)}: {count}+ pending tasks/actors" + if bundle in pg_bundle_demand: + line += f" ({pg_bundle_demand[bundle]}+ using placement groups)" + demand_lines.append(line) + return demand_lines + + +def get_constraint_report(request_demand: List[DictCount]): + """Returns a formatted string describing the resource constraints from request_resources(). + + Args: + request_demand: List of tuples containing resource bundle dictionaries and counts + from request_resources() calls. + + Returns: + String containing the formatted constraints report, either listing each constraint + and count or indicating no constraints exist. + + Example: + >>> request_demand = [ + ... ({"CPU": 4}, 2), + ... ({"GPU": 1}, 1) + ... ] + >>> get_constraint_report(request_demand) + " {'CPU': 4}: 2 from request_resources()\\n {'GPU': 1}: 1 from request_resources()" + """ + constraint_lines = [] + for bundle, count in request_demand: + line = f" {bundle}: {count} from request_resources()" + constraint_lines.append(line) + if len(constraint_lines) > 0: + constraints_report = "\n".join(constraint_lines) + else: + constraints_report = " (none)" + return constraints_report + + +def get_demand_report(lm_summary: LoadMetricsSummary): + demand_lines = [] + if lm_summary.resource_demand: + demand_lines.extend(format_resource_demand_summary(lm_summary.resource_demand)) + for entry in lm_summary.pg_demand: + pg, count = entry + pg_str = format_pg(pg) + line = f" {pg_str}: {count}+ pending placement groups" + demand_lines.append(line) + if len(demand_lines) > 0: + demand_report = "\n".join(demand_lines) + else: + demand_report = " (no resource demands)" + return demand_report + + +def get_per_node_breakdown_as_dict( + lm_summary: LoadMetricsSummary, +) -> dict: + per_node_breakdown = {} + + for node_id, usage in lm_summary.usage_by_node.items(): + usage_string = "" + for line in parse_usage(usage, verbose=True): + usage_string += f"{line}\n" + per_node_breakdown[node_id] = usage_string.strip() + + return per_node_breakdown + + +def get_per_node_breakdown( + lm_summary: LoadMetricsSummary, + node_type_mapping: Optional[Dict[str, float]], + node_activities: Optional[Dict[str, List[str]]], + verbose: bool, +) -> str: + sio = StringIO() + + if node_type_mapping is None: + node_type_mapping = {} + + print(file=sio) + for node_id, usage in lm_summary.usage_by_node.items(): + print(file=sio) # Print a newline. + node_string = f"Node: {node_id}" + if node_id in node_type_mapping: + node_type = node_type_mapping[node_id] + node_string += f" ({node_type})" + print(node_string, file=sio) + if ( + lm_summary.idle_time_map + and node_id in lm_summary.idle_time_map + and lm_summary.idle_time_map[node_id] > 0 + ): + print(f" Idle: {lm_summary.idle_time_map[node_id]} ms", file=sio) + + print(" Usage:", file=sio) + for line in parse_usage(usage, verbose): + print(f" {line}", file=sio) + # Don't print anything if not provided. + if not node_activities: + continue + print(" Activity:", file=sio) + if node_id not in node_activities: + print(" (no activity)", file=sio) + else: + # Note: We have node IP here. + _, reasons = node_activities[node_id] + for reason in reasons: + print(f" {reason}", file=sio) + + return sio.getvalue() + + +def format_info_string( + lm_summary, + autoscaler_summary, + time=None, + gcs_request_time: Optional[float] = None, + non_terminated_nodes_time: Optional[float] = None, + autoscaler_update_time: Optional[float] = None, + verbose: bool = False, +): + if time is None: + time = datetime.now() + header = "=" * 8 + f" Autoscaler status: {time} " + "=" * 8 + separator = "-" * len(header) + if verbose: + header += "\n" + if gcs_request_time: + header += f"GCS request time: {gcs_request_time:3f}s\n" + if non_terminated_nodes_time: + header += ( + "Node Provider non_terminated_nodes time: " + f"{non_terminated_nodes_time:3f}s\n" + ) + if autoscaler_update_time: + header += "Autoscaler iteration time: " f"{autoscaler_update_time:3f}s\n" + + available_node_report_lines = [] + if not autoscaler_summary.active_nodes: + available_node_report = " (no active nodes)" + else: + for node_type, count in autoscaler_summary.active_nodes.items(): + line = f" {count} {node_type}" + available_node_report_lines.append(line) + available_node_report = "\n".join(available_node_report_lines) + + if not autoscaler_summary.idle_nodes: + idle_node_report = " (no idle nodes)" + else: + idle_node_report_lines = [] + for node_type, count in autoscaler_summary.idle_nodes.items(): + line = f" {count} {node_type}" + idle_node_report_lines.append(line) + idle_node_report = "\n".join(idle_node_report_lines) + + pending_lines = [] + for node_type, count in autoscaler_summary.pending_launches.items(): + line = f" {node_type}, {count} launching" + pending_lines.append(line) + for ip, node_type, status in autoscaler_summary.pending_nodes: + line = f" {ip}: {node_type}, {status.lower()}" + pending_lines.append(line) + if pending_lines: + pending_report = "\n".join(pending_lines) + else: + pending_report = " (no pending nodes)" + + failure_lines = [] + for ip, node_type in autoscaler_summary.failed_nodes: + line = f" {node_type}: NodeTerminated (ip: {ip})" + failure_lines.append(line) + if autoscaler_summary.node_availability_summary: + records = sorted( + autoscaler_summary.node_availability_summary.node_availabilities.values(), + key=lambda record: record.last_checked_timestamp, + ) + for record in records: + if record.is_available: + continue + assert record.unavailable_node_information is not None + node_type = record.node_type + category = record.unavailable_node_information.category + description = record.unavailable_node_information.description + attempted_time = datetime.fromtimestamp(record.last_checked_timestamp) + formatted_time = ( + # This `:02d` funny business is python syntax for printing a 2 + # digit number with a leading zero as padding if needed. + f"{attempted_time.hour:02d}:" + f"{attempted_time.minute:02d}:" + f"{attempted_time.second:02d}" + ) + line = f" {node_type}: {category} (latest_attempt: {formatted_time})" + if verbose: + line += f" - {description}" + failure_lines.append(line) + + failure_lines = failure_lines[: -constants.AUTOSCALER_MAX_FAILURES_DISPLAYED : -1] + failure_report = "Recent failures:\n" + if failure_lines: + failure_report += "\n".join(failure_lines) + else: + failure_report += " (no failures)" + + usage_report = get_usage_report(lm_summary, verbose) + constraints_report = get_constraint_report(lm_summary.request_demand) + demand_report = get_demand_report(lm_summary) + formatted_output = f"""{header} +Node status +{separator} +Active: +{available_node_report}""" + + if not autoscaler_summary.legacy: + formatted_output += f""" +Idle: +{idle_node_report}""" + + formatted_output += f""" +Pending: +{pending_report} +{failure_report} + +Resources +{separator} +Total Usage: +{usage_report} +From request_resources: +{constraints_report} +Pending Demands: +{demand_report}""" + + if verbose: + if lm_summary.usage_by_node: + formatted_output += get_per_node_breakdown( + lm_summary, + autoscaler_summary.node_type_mapping, + autoscaler_summary.node_activities, + verbose, + ) + else: + formatted_output += "\n" + + return formatted_output.strip() + + +def format_readonly_node_type(node_id: str): + """The anonymous node type for readonly node provider nodes.""" + return "node_{}".format(node_id) + + +def format_no_node_type_string(node_type: dict): + placement_group_resource_usage = {} + regular_resource_usage = collections.defaultdict(float) + for resource, total in node_type.items(): + (pg_resource_name, pg_name, is_countable) = parse_placement_group_resource_str( + resource + ) + if pg_name: + if not is_countable: + continue + if pg_resource_name not in placement_group_resource_usage: + placement_group_resource_usage[pg_resource_name] = 0 + placement_group_resource_usage[pg_resource_name] += total + else: + regular_resource_usage[resource] += total + + output_lines = [""] + for resource, total in regular_resource_usage.items(): + output_line = f"{resource}: {total}" + if resource in placement_group_resource_usage: + pg_resource = placement_group_resource_usage[resource] + output_line += f" ({pg_resource} reserved in placement groups)" + output_lines.append(output_line) + + return "\n ".join(output_lines) + + +def generate_rsa_key_pair(): + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + + key = rsa.generate_private_key( + backend=default_backend(), public_exponent=65537, key_size=2048 + ) + + public_key = ( + key.public_key() + .public_bytes( + serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH + ) + .decode("utf-8") + ) + + pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ).decode("utf-8") + + return public_key, pem + + +def generate_ssh_key_paths(key_name): + public_key_path = os.path.expanduser("~/.ssh/{}.pub".format(key_name)) + private_key_path = os.path.expanduser("~/.ssh/{}".format(key_name)) + return public_key_path, private_key_path + + +def generate_ssh_key_name(provider, i, region, identifier, ssh_user): + RAY_PREFIX = "ray-autoscaler" + if i is not None: + return "{}_{}_{}_{}_{}_{}".format( + RAY_PREFIX, provider, region, identifier, ssh_user, i + ) + else: + return "{}_{}_{}_{}_{}".format( + RAY_PREFIX, provider, region, identifier, ssh_user + ) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d203cd3f6636008e0cf1edf5cec185ebea51259 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/cluster_operator_client.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/cluster_operator_client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9407d8c02eba7e6c1f5283cf648727c0f3e31b0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/cluster_operator_client.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/config.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..735c802bffd35f44abe4d7a621f7ffb462645526 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/node_provider.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/node_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2886555553895392e0e7a236cafc56fee3263fa Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/__pycache__/node_provider.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/cluster_operator_client.py b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/cluster_operator_client.py new file mode 100644 index 0000000000000000000000000000000000000000..3f75f57819f28f9ba3a62750c424c907e91c62a6 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/cluster_operator_client.py @@ -0,0 +1,685 @@ +import base64 +import ipaddress +import logging +import os +import random +import string +from enum import Enum +from threading import RLock +from typing import Any, Dict, Optional, Tuple + +from kubernetes import client, config + +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + NODE_KIND_WORKER, + STATUS_SETTING_UP, + STATUS_UNINITIALIZED, + STATUS_UP_TO_DATE, + TAG_RAY_CLUSTER_NAME, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_NAME, + TAG_RAY_NODE_STATUS, + TAG_RAY_USER_NODE_TYPE, +) + +# Design: + +# Each modification the autoscaler wants to make is posted to the API server's desired +# state (e.g. if the autoscaler wants to scale up, it adds VM name to the desired +# worker list it wants to scale, if it wants to scale down it removes the name from +# the list). + +# VMRay CRD +VMRAY_CRD_VER = os.getenv("VMRAY_CRD_VER", "v1alpha1") +VMRAY_GROUP = "vmray.broadcom.com" +VMRAYCLUSTER_PLURAL = "vmrayclusters" + +# VirtualMachineService CRD +VMSERVICE_CRD_VER = os.getenv("VMSERVICE_CRD_VER", "v1alpha2") +VMSERVICE_GROUP = "vmoperator.vmware.com" +VMSERVICE_PLURAL = "virtualmachineservices" + +SERVICE_ACCOUNT_TOKEN = os.getenv("SVC_ACCOUNT_TOKEN", None) + +logger = logging.getLogger(__name__) +cur_path = os.path.dirname(__file__) + + +class VMNodeStatus(Enum): + INITIALIZED = "initialized" + RUNNING = "running" + FAIL = "failure" + + +class KubernetesHttpApiClient(object): + def __init__(self, ca_cert: str, api_server: str): + token = SERVICE_ACCOUNT_TOKEN + # If SERVICE_ACCOUNT_TOKEN not present, use local + # ~/.kube/config file. Active context will be used. + # This is usefull when Ray CLI are used and local autoscaler needs + # communicate with the k8s API server + # If the token is present then use that for communication. + if not token: + self.client = client.ApiClient(config.load_kube_config()) + else: + configuration = client.Configuration() + configuration.api_key["authorization"] = token + configuration.api_key_prefix["authorization"] = "Bearer" + configuration.host = f"https://{api_server}" + if ca_cert: + configuration.ssl_ca_cert = ca_cert + else: + configuration.verify_ssl = False + self.client = client.ApiClient(configuration) + + # Use customObjectsApi to access custom resources + self.custom_object_api = client.CustomObjectsApi(self.client) + + +class ClusterOperatorClient(KubernetesHttpApiClient): + def __init__( + self, + cluster_name: str, + provider_config: Dict[str, Any], + cluster_config: Dict[str, Any], + ): + self.cluster_name = cluster_name + self.vmraycluster_nounce = None + self.max_worker_nodes = None + + self.vsphere_config = provider_config["vsphere_config"] + + self.namespace = self.vsphere_config["namespace"] + self.k8s_api_client = KubernetesHttpApiClient( + self.vsphere_config.get("ca_cert"), + self.vsphere_config.get("api_server"), + ) + self.lock = RLock() + + if cluster_config: + self.max_worker_nodes = cluster_config["max_workers"] + self.head_setup_commands = cluster_config["head_setup_commands"] + self.available_node_types = cluster_config["available_node_types"] + self.head_node_type = cluster_config["head_node_type"] + + # docker configurations. + self.provider_auth = cluster_config["auth"] + self.docker = cluster_config["docker"] + + # create docker login info secret, if it exists. + docker_auth_secret_name = self._create_docker_auth_secrets() + if docker_auth_secret_name: + self.docker_config = { + "auth_secret_name": docker_auth_secret_name, + } + else: + self.docker_config = None + else: + self._set_max_worker_nodes() + self._create_tls_secrets() + + def _create_docker_auth_secrets(self): + docker_auth_secret_name = self.cluster_name + "-docker-auth" + docker_auth = self.vsphere_config.get("docker_auth", {}) + username = docker_auth.get("username", None) + password = docker_auth.get("password", None) + kp = {} + if username and password: + kp["username"] = username + kp["password"] = password + registry = docker_auth.get("registry", None) + if registry: + kp["registry"] = registry + self._create_secret(self.namespace, docker_auth_secret_name, kp) + return docker_auth_secret_name + return None + + def _create_tls_secrets(self): + # If token is passed that means its instance of autoscaler + # running inside the head node, so validate if tls server cert + # and key are available then create a secret with their + # value. + tls_enabled = os.environ.get("RAY_USE_TLS", None) == "1" + if not SERVICE_ACCOUNT_TOKEN or not tls_enabled: + return + + tls_cert = None + tls_key = None + + cert_path = os.environ.get("RAY_TLS_SERVER_CERT", None) + key_path = os.environ.get("RAY_TLS_SERVER_KEY", None) + if cert_path: + with open(cert_path) as f: + tls_cert = f.read() + if key_path: + with open(key_path) as f: + tls_key = f.read() + + if tls_cert and tls_key: + kp = {"tls.crt": tls_cert, "tls.key": tls_key} + self._create_secret(self.namespace, self.cluster_name + "-tls", kp) + + def list_vms(self, tag_filters: Dict[str, str]) -> Tuple[list, dict]: + """Queries K8s for VMs in the RayCluster and filter them as per + tags provided in the tag_filters. + """ + logger.info(f"Getting nodes using tags \n{tag_filters}") + tag_cache = {} + + filters = tag_filters.copy() + # Use Ray cluster name to get resources + if TAG_RAY_CLUSTER_NAME not in tag_filters: + filters[TAG_RAY_CLUSTER_NAME] = self.cluster_name + nodes = [] + vmray_cluster_response = self._get_cluster_response() + if not vmray_cluster_response: + return nodes, tag_cache + + vmray_cluster_status = vmray_cluster_response.get("status", {}) + if not vmray_cluster_status: + return nodes, tag_cache + + vmray_cluster_spec = vmray_cluster_response.get("spec", {}) + + # Check for a head node + if NODE_KIND_HEAD in tag_filters.values() or not tag_filters: + head_node_status = vmray_cluster_status.get("head_node_status", {}) + # head node found + if head_node_status: + node_id = self._get_head_name() + nodes.append(node_id) + + # Setting head node status + status = head_node_status.get("vm_status", None) + head_nt = vmray_cluster_spec["head_node"]["node_type"] + tag_cache[node_id] = self._set_tags( + node_id, NODE_KIND_HEAD, head_nt, status, filters + ) + # Check current worker nodes + if NODE_KIND_WORKER in tag_filters.values() or not tag_filters: + current_workers = vmray_cluster_status.get("current_workers", {}) + desired_workers = vmray_cluster_spec.get("autoscaler_desired_workers", {}) + + # worker nodes found + for worker in current_workers.keys(): + nodes.append(worker) + # setting worker node status + status = current_workers[worker].get("vm_status", None) + node_type = desired_workers.get(worker, "") + + tag_cache[worker] = self._set_tags( + worker, NODE_KIND_WORKER, node_type, status, filters + ) + + # List VMs from the desired workers' list + for worker in desired_workers.keys(): + if worker in current_workers.keys(): + continue + nodes.append(worker) + node_type = desired_workers.get(worker, "") + tag_cache[worker] = self._set_tags( + worker, NODE_KIND_WORKER, node_type, STATUS_SETTING_UP, filters + ) + + logger.info( + f"Non terminated nodes {nodes}, Tags for these are: {tag_cache}" + ) + return nodes, tag_cache + + def is_vm_power_on(self, node_id: str) -> bool: + """Check current vm list. If its state is Running then return + true else false.""" + node = self._get_node(node_id) + if node: + return node.get("vm_status", None) == VMNodeStatus.RUNNING.value + logger.info(f"VM {node_id} not found") + return False + + def is_vm_creating(self, node_id: str) -> bool: + """Check current vm list. If its state is INITIALIZED then return + true else false.""" + node = self._get_node(node_id) + if node: + return node.get("vm_status", None) == VMNodeStatus.INITIALIZED.value + logger.info(f"VM {node_id} is not yet initialized") + return False + + def set_node_tags(self, tags: Dict[str, str]) -> None: + """ + Not required + """ + pass + + def get_vm_external_ip(self, node_id: str) -> Optional[str]: + """Check current worker list and get the external ip.""" + node = {} + # For a Ray head node, return external IP of the VMService + # Ray head node is not accessible directly. + if node_id == self._get_head_name(): + ingress = self._get_vm_service_ingress() + for item in ingress: + if "ip" in item.keys(): + node = item + break + else: + worker_node = self._get_node(node_id) + if ( + worker_node + and worker_node.get("vm_status", None) == VMNodeStatus.RUNNING.value + ): + node = worker_node + ip = node.get("ip", None) + # Validate returned IP + if ip and _is_ipv4(ip): + return ip + logger.info( + f"External IPv4 address: {ip} of VM: {node_id}" + f"is either invalid or not available" + ) + return None + + def delete_node(self, node_id: str) -> None: + """Remove name of the vm from the desired worker list and patch + the VmRayCluster CR""" + with self.lock: + vmray_cluster_response = self._get_cluster_response() + vmray_cluster_spec = vmray_cluster_response.get("spec", {}) + + # Get desired workers + desired_workers = vmray_cluster_spec.get("autoscaler_desired_workers", {}) + logger.info(f"Current desired workers: {desired_workers}") + + # remove the node from the desired workers list + if node_id in desired_workers: + + # By default it follow patch application of `merge-patch+json` + # so we need to remove the node ids by making them null. + # refs: + # 1. https://kubernetes.io/docs/tasks/manage-kubernetes-objects/ + # update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment + # 2. https://github.com/kubernetes-client/python/blob/master/kubernetes/ + # client/api/custom_objects_api.py#L3106 + payload = {"spec": {"autoscaler_desired_workers": {node_id: None}}} + + logger.info(f"Deleting VM {node_id} | payload: {payload}") + self.k8s_api_client.custom_object_api.patch_namespaced_custom_object( + VMRAY_GROUP, + VMRAY_CRD_VER, + self.namespace, + VMRAYCLUSTER_PLURAL, + self.cluster_name, + payload, + async_req=False, + ) + elif node_id == self._get_head_name(): + # Handle case to delete a head node + # Delete VMRayCluster which will delete head node + # as well as associated secrets and other resources. + self.k8s_api_client.custom_object_api.delete_namespaced_custom_object( + VMRAY_GROUP, + VMRAY_CRD_VER, + self.namespace, + VMRAYCLUSTER_PLURAL, + self.cluster_name, + ) + + def create_nodes( + self, + tags: Dict[str, str], + to_be_launched_node_count: int, + node_config: Dict[str, Any], + ) -> Optional[Dict[str, Any]]: + """Ask cluster operator to create worker VMs""" + logger.info( + f"Creating {to_be_launched_node_count} nodes with tags: {tags}" + f"and with config: {node_config}" + ) + created_nodes_dict = {} + with self.lock: + if to_be_launched_node_count > 0: + new_desired_workers = {} + + new_vm_names = {} + for _ in range(to_be_launched_node_count): + name = self._create_node_name(tags[TAG_RAY_NODE_NAME]) + new_vm_names[name] = tags[TAG_RAY_USER_NODE_TYPE] + + # Create a head node + # Autoscaler sends a tag + # head_node_tags[TAG_RAY_NODE_NAME] = "ray-{}-head".format( + # config["cluster_name"]) + if "head" in tags[TAG_RAY_NODE_NAME]: + # head node will be created as a part of VMRayCluster CR + self._create_ssh_secret() + self._create_vmraycluster() + else: + # Once VMRayCluster CR is created, update it to create worker + # nodes. + vmray_cluster_response = self._get_cluster_response() + vmray_cluster_spec = vmray_cluster_response.get("spec", {}) + + # get desired workers + desired_workers = vmray_cluster_spec.get( + "autoscaler_desired_workers", {} + ) + + # If workers are present in both the list then it shows stable + # state for the cluster. + # Append new VM names with existing one + if desired_workers: + new_desired_workers.update(desired_workers) + + new_desired_workers.update(new_vm_names) + logger.info(f"New desired state will be {new_desired_workers}") + if len(new_desired_workers) > self.max_worker_nodes: + logger.warning( + "Autoscaler attempted to create more than max_workers VMs." + ) + return created_nodes_dict + + payload = { + "spec": {"autoscaler_desired_workers": new_desired_workers} + } + + custom_api = self.k8s_api_client.custom_object_api + custom_api.patch_namespaced_custom_object( + VMRAY_GROUP, + VMRAY_CRD_VER, + self.namespace, + VMRAYCLUSTER_PLURAL, + self.cluster_name, + payload, + async_req=False, + ) + for vm in new_vm_names: + created_nodes_dict[vm] = vm + return created_nodes_dict + + def _get_cluster_response(self): + response = {} + try: + response = ( + self.k8s_api_client.custom_object_api.get_namespaced_custom_object( + VMRAY_GROUP, + VMRAY_CRD_VER, + self.namespace, + VMRAYCLUSTER_PLURAL, + self.cluster_name, + ) + ) + return response + except client.exceptions.ApiException as e: + # If HTTP 404 received means the cluster is not yet created. + logger.warning( + f"Exception while getting {self.cluster_name}. Exception: {str(e)}" + ) + if e.status == 404: + logger.warning(f"{self.cluster_name} not available. Creating new one.") + return response + + def _get_node(self, node_id: str) -> Any: + vmray_cluster_response = self._get_cluster_response() + vmray_cluster_status = vmray_cluster_response.get("status", {}) + if not vmray_cluster_status: + return {} + head_node_status = vmray_cluster_status.get("head_node_status", {}) + current_workers = vmray_cluster_status.get("current_workers", {}) + # head node is found + if head_node_status and node_id == self._get_head_name(): + return head_node_status + # worker nodes found + for worker in current_workers.keys(): + if worker == node_id: + return current_workers.get(worker) + # If worker not found in the current worker then it might be getting created + # and not yet ready. So check if it is in the desired workers list. + vmray_cluster_spec = vmray_cluster_response.get("spec", {}) + desired_workers = vmray_cluster_spec.get("autoscaler_desired_workers", {}) + for worker in desired_workers.keys(): + if worker == node_id: + # set vm_status as VM in the desired workers' list will not + # have vm_status field. + node = {"vm_status": VMNodeStatus.INITIALIZED.value} + return node + logger.info(f"VM {node_id} not found") + return {} + + def safe_to_scale(self): + """ + It is safe to scale as long as total number of workers(desired + current) + do not exceeds cluster level max_workers. + This function should handle cases: + 1. If there are workers in the desired_workers list but not in the + current_workers list that means few workers are not yet up and running. + 2. If there are workers in the current_workers list but not in a + desired_workers list indicates workers are not yet deleted completely + and we should wait. + 3. If workers are present in both the list shows stable state for the cluster. + """ + vmray_cluster_response = self._get_cluster_response() + vmray_cluster_status = vmray_cluster_response.get("status", {}) + if not vmray_cluster_status: + return False + current_workers = vmray_cluster_status.get("current_workers", {}) + vmray_cluster_spec = vmray_cluster_response.get("spec", {}) + desired_workers = vmray_cluster_spec.get("autoscaler_desired_workers", {}) + logger.info( + f"Checking is it safe to scale:\n" + f"Current workers: {current_workers.keys()} \n" + f"Autoscaler desired workers: {desired_workers}" + ) + + # Do not scale until reaches desired state + if len(desired_workers) != len(current_workers): + return False + + # Wait until all nodes are in a Running state + for worker in current_workers.values(): + if worker.get("vm_status", None) != VMNodeStatus.RUNNING.value: + return False + + return True + + def _create_node_name(self, node_name_tag): + """Create name for a Ray node""" + # The nodes are named as follows: + # -h- for the head node + # -w-<> for the worker nodes + random_str = "".join( + random.choice(string.ascii_lowercase + string.digits) for _ in range(8) + ) + if "head" in node_name_tag: + self.vmraycluster_nounce = random_str + return f"{self.cluster_name}-h-" + self.vmraycluster_nounce + return f"{self.cluster_name}-w-" + random_str + + def _get_head_name(self): + if not self.vmraycluster_nounce: + vmray_cluster_response = self._get_cluster_response() + self.vmraycluster_nounce = vmray_cluster_response["metadata"]["labels"][ + "vmray.kubernetes.io/head-nounce" + ] + return f"{self.cluster_name}-h-" + self.vmraycluster_nounce + + def _create_vmraycluster(self): + + # Define vmraycluster config structure. + ray_cluster_config = {} + ray_cluster_config["apiVersion"] = VMRAY_GROUP + "/" + VMRAY_CRD_VER + ray_cluster_config["kind"] = "VMRayCluster" + ray_cluster_config["metadata"] = {} + ray_cluster_config["spec"] = {} + + # Start reading values from local bootstrap config. + ray_cluster_config["metadata"]["name"] = self.cluster_name + ray_cluster_config["metadata"]["labels"] = { + "vmray.kubernetes.io/head-nounce": self.vmraycluster_nounce, + "vmray.io/created-by": "ray-cli", + } + ray_cluster_config["metadata"]["namespace"] = self.namespace + + ray_cluster_config["spec"]["api_server"] = {} + ray_cluster_config["spec"]["api_server"]["location"] = self.vsphere_config.get( + "api_server" + ) + ray_cluster_config["spec"]["ray_docker_image"] = self.docker["image"] + + # Set head node specific config. + ray_cluster_config["spec"]["head_node"] = {} + ray_cluster_config["spec"]["head_node"][ + "head_setup_commands" + ] = self.head_setup_commands + ray_cluster_config["spec"]["head_node"][ + "port" + ] = 6379 # using default GCS port for now. + + ray_cluster_config["spec"]["head_node"]["node_type"] = self.head_node_type + + # Set common node config & available node types. + ray_cluster_config["spec"]["common_node_config"] = {} + ray_cluster_config["spec"]["common_node_config"][ + "vm_image" + ] = self.vsphere_config.get("vm_image") + ray_cluster_config["spec"]["common_node_config"][ + "storage_class" + ] = self.vsphere_config.get("storage_class") + ray_cluster_config["spec"]["common_node_config"][ + "vm_password_salt_hash" + ] = self.vsphere_config.get("vm_password_salt_hash", "") + ray_cluster_config["spec"]["common_node_config"][ + "max_workers" + ] = self.max_worker_nodes + available_node_types = {} + for node_type, node_config in self.available_node_types.items(): + available_node_types[node_type] = {} + available_node_types[node_type]["vm_class"] = node_config[ + "node_config" + ].get("vm_class", None) + available_node_types[node_type]["resources"] = node_config.get( + "resources", {} + ) + available_node_types[node_type]["min_workers"] = node_config.get( + "min_workers", 0 + ) + available_node_types[node_type]["max_workers"] = node_config.get( + "max_workers", 2 + ) + + ray_cluster_config["spec"]["common_node_config"][ + "available_node_types" + ] = available_node_types + ray_cluster_config["spec"]["common_node_config"][ + "vm_user" + ] = self.provider_auth["ssh_user"] + if self.docker_config is not None: + ray_cluster_config["spec"]["docker_config"] = self.docker_config + + logger.info(f"Creating VmRayCluster \n{ray_cluster_config}") + self.k8s_api_client.custom_object_api.create_namespaced_custom_object( + VMRAY_GROUP, + VMRAY_CRD_VER, + self.namespace, + VMRAYCLUSTER_PLURAL, + ray_cluster_config, + ) + + def _set_tags(self, node_id, node_kind, node_user_type, node_status, tags): + new_tags = tags.copy() + if node_status == VMNodeStatus.RUNNING.value: + new_tags[TAG_RAY_NODE_STATUS] = STATUS_UP_TO_DATE + elif node_status == VMNodeStatus.INITIALIZED.value: + new_tags[TAG_RAY_NODE_STATUS] = STATUS_SETTING_UP + else: + new_tags[TAG_RAY_NODE_STATUS] = STATUS_UNINITIALIZED + + new_tags[TAG_RAY_NODE_NAME] = node_id + new_tags[TAG_RAY_NODE_KIND] = node_kind + new_tags[TAG_RAY_USER_NODE_TYPE] = node_user_type + return new_tags + + def _set_max_worker_nodes(self): + vmray_cluster_response = self._get_cluster_response() + if not self.max_worker_nodes: + vmray_cluster_spec = vmray_cluster_response.get("spec", {}) + common_node_config = vmray_cluster_spec.get("common_node_config", {}) + # If max_workers is not provided then default to 2 + # ref: https://docs.ray.io/en/latest/cluster/vms/references/ + # ray-cluster-configuration.html#max-workers + self.max_worker_nodes = common_node_config.get("max_workers", 2) + logger.info(f"Max worker is set to {self.max_worker_nodes}") + + def _get_vm_service_ingress(self): + response = self._get_vm_service() + status = response.get("status", {}) + if not status: + return [] + ingress = status["loadBalancer"].get("ingress", []) + logger.info(f"VM service ingress is {ingress}") + return ingress + + def _get_vm_service(self): + try: + return self.k8s_api_client.custom_object_api.get_namespaced_custom_object( + VMSERVICE_GROUP, + VMSERVICE_CRD_VER, + self.namespace, + VMSERVICE_PLURAL, + self._get_head_name(), + ) + except client.exceptions.ApiException as e: + logger.warning( + f"Exception while getting vm service in namespace {self.namespace}." + f"Exception: {str(e)}" + ) + return {} + + def _create_secret(self, namespace, name, kp): + data = {} + for k, v in kp.items(): + # Base64 encode the SSH key + val = v + if type(v) is str: + val = v.encode("utf-8") + data[k] = base64.b64encode(val).decode("utf-8") + + # Create metadata + metadata = client.V1ObjectMeta(name=name) + + # Secret object + secret = client.V1Secret( + api_version="v1", kind="Secret", metadata=metadata, data=data, type="Opaque" + ) + instance = client.CoreV1Api(self.k8s_api_client.client) + + # Create the secret in the specified namespace. + try: + instance.create_namespaced_secret(namespace=namespace, body=secret) + logger.info(f"Secret {name} created in namespace {namespace}") + except client.exceptions.ApiException as e: + print("Failure while creating Secret [`%s`] : %s\n" % (name, e)) + + def _create_ssh_secret(self): + """Create a K8s SSH secret using a local private SSH key + specified in the config.""" + + # Define secret name + secret_name = f"{self.cluster_name}-ssh-key" + + pvt_key = self.provider_auth.get("ssh_private_key") + private_key_path = os.path.expanduser(pvt_key) + + # Read the SSH private key + with open(private_key_path, "rb") as ssh_key: + ssh_key_data = ssh_key.read() + + kp = {"ssh-pvt-key": ssh_key_data} + self._create_secret(self.namespace, secret_name, kp) + + +def _is_ipv4(ip): + try: + ipaddress.IPv4Address(ip) + return True + except ipaddress.AddressValueError: + return False diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/config.py b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/config.py new file mode 100644 index 0000000000000000000000000000000000000000..df9c96653016c926d3c7fa5bc28f00ff0e4810a6 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/config.py @@ -0,0 +1,148 @@ +import copy +import logging +import os + +from cryptography.hazmat.primitives import serialization as crypto_serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +from ray.autoscaler._private.constants import DISABLE_NODE_UPDATERS_KEY +from ray.autoscaler._private.event_system import CreateClusterEvent, global_event_system +from ray.autoscaler._private.util import check_legacy_fields + +PRIVATE_KEY_NAME = "ray-bootstrap-key.pem" +PUBLIC_KEY_NAME = "ray_bootstrap_public_key.key" + +PRIVATE_KEY_PATH = os.path.expanduser(f"~/{PRIVATE_KEY_NAME}") +PUBLIC_KEY_PATH = os.path.expanduser(f"~/{PUBLIC_KEY_NAME}") + +logger = logging.getLogger(__name__) + + +def bootstrap_vsphere(config): + + # create a copy of the input config to modify + config = copy.deepcopy(config) + + # Log warnings if user included deprecated `head_node` or `worker_nodes` + # fields. Raise error if no `available_node_types` + check_legacy_fields(config) + + # Configure SSH access, using an existing key pair if possible. + config = configure_key_pair(config) + # Configure docker run command to be executed on head and wroker nodes + config = configure_run_options(config) + + global_event_system.execute_callback( + CreateClusterEvent.ssh_keypair_downloaded, + {"ssh_key_path": config["auth"]["ssh_private_key"]}, + ) + logger.info(f"{config}") + return config + + +def configure_key_pair(config): + logger.info("Configuring keys for Ray Cluster Launcher to ssh into the head node.") + + if not os.path.exists(PRIVATE_KEY_PATH): + logger.warning( + "Private key file at path {} was not found".format(PRIVATE_KEY_PATH) + ) + _create_ssh_keys() + logger.info( + f"New SSH key pair {PRIVATE_KEY_PATH} and {PUBLIC_KEY_PATH} created." + ) + + # updater.py file uses the following config to ssh onto the head node + # Also, copies the file onto the head node + config["auth"]["ssh_private_key"] = PRIVATE_KEY_PATH + # The path where the public key should be copied onto the remote host + public_key_remote_path = f"~/{PUBLIC_KEY_NAME}" + + # Copy the public key to the remote host + config["file_mounts"][public_key_remote_path] = PUBLIC_KEY_PATH + + return config + + +def configure_run_options(config): + ssh_user = config["auth"]["ssh_user"] + # By default enable TLS for Head-Worker grpc communication + tls_enable = ( + 1 if config["provider"]["vsphere_config"].get("tls_enable", True) else 0 + ) + # Configure common run options + if "run_options" not in config["docker"]: + config["docker"]["run_options"] = [] + config["docker"]["run_options"].append(f"--env RAY_USE_TLS={tls_enable}") + + # Configure head_run_options + if "head_run_options" not in config["docker"]: + config["docker"]["head_run_options"] = [] + config["docker"]["head_run_options"].append( + f"--env-file /home/{ssh_user}/svc-account-token.env" + ) + + # Configure worker_run_options + if "worker_run_options" not in config["docker"]: + config["docker"]["worker_run_options"] = [] + + if tls_enable == 1: + # Generate TLS cert and key for head and worker nodes. + # This needs to be done before ray start command + config["head_start_ray_commands"].insert(0, "sh /home/ray/gencert.sh") + config["worker_start_ray_commands"].insert(0, "sh /home/ray/gencert.sh") + + config["docker"]["run_options"].append( + f"-v /home/{ssh_user}/ca.crt:/home/ray/ca.crt" + ) + config["docker"]["run_options"].append( + f"-v /home/{ssh_user}/ca.key:/home/ray/ca.key" + ) + config["docker"]["run_options"].append( + f"-v /home/{ssh_user}/gencert.sh:/home/ray/gencert.sh" + ) + + config["docker"]["run_options"].append("--env RAY_TLS_CA_CERT=/home/ray/ca.crt") + config["docker"]["run_options"].append( + "--env RAY_TLS_SERVER_KEY=/home/ray/tls.key" + ) + config["docker"]["run_options"].append( + "--env RAY_TLS_SERVER_CERT=/home/ray/tls.crt" + ) + + return config + + +def disable_node_updater(config): + logger.info( + "Disabling NodeUpdater threads as Cluster Operator is " + + "responsible for Ray setup on nodes." + ) + config["provider"][DISABLE_NODE_UPDATERS_KEY] = True + return config + + +def _create_ssh_keys(): + """Create SSH keys as specified""" + # Create a private key + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096) + # Encode it in PEM format + unencrypted_private_key = private_key.private_bytes( + encoding=crypto_serialization.Encoding.PEM, + format=crypto_serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=crypto_serialization.NoEncryption(), + ) + # Create a public key + public_key = private_key.public_key().public_bytes( + encoding=crypto_serialization.Encoding.PEM, + format=crypto_serialization.PublicFormat.SubjectPublicKeyInfo, + ) + # Write keys + with open(PRIVATE_KEY_PATH, "wb") as pvt_key: + # manage access mode for the pvt key + os.chmod(PRIVATE_KEY_PATH, 0o600) + pvt_key.write(unencrypted_private_key) + + with open(PUBLIC_KEY_PATH, "wb") as pub_key: + # manage access mode for the pvt key + pub_key.write(public_key) diff --git a/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..4aef398fc7c2b2147f791dc085a8a4bd395a3718 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/_private/vsphere/node_provider.py @@ -0,0 +1,125 @@ +import logging +import threading +from typing import Any, Dict + +from ray.autoscaler._private.vsphere.cluster_operator_client import ( + ClusterOperatorClient, +) +from ray.autoscaler._private.vsphere.config import bootstrap_vsphere +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + STATUS_SETTING_UP, + TAG_RAY_CLUSTER_NAME, + TAG_RAY_NODE_NAME, + TAG_RAY_NODE_STATUS, +) + +logger = logging.getLogger(__name__) + + +class VsphereWcpNodeProvider(NodeProvider): + max_terminate_nodes = 1000 + cluster_config = None + + def __init__(self, provider_config, cluster_name): + NodeProvider.__init__(self, provider_config, cluster_name) + self.tag_cache = {} + self.tag_cache_lock = threading.Lock() + self.client = ClusterOperatorClient( + cluster_name, provider_config, VsphereWcpNodeProvider.cluster_config + ) + + @staticmethod + def bootstrap_config(cluster_config): + config = bootstrap_vsphere(cluster_config) + VsphereWcpNodeProvider.cluster_config = config + return config + + def non_terminated_nodes(self, tag_filters): + nodes, tag_cache = self.client.list_vms(tag_filters) + with self.tag_cache_lock: + for node_id in nodes: + for k, v in tag_cache[node_id].items(): + if node_id in self.tag_cache.keys(): + self.tag_cache[node_id][k] = v + else: + self.tag_cache[node_id] = {} + self.tag_cache[node_id][k] = v + logger.info(f"Non terminated nodes' tags are {self.tag_cache}") + return nodes + + def is_running(self, node_id): + return self.client.is_vm_power_on(node_id) + + def is_terminated(self, node_id): + if self.client.is_vm_power_on(node_id): + return False + else: + # If the node is not powered on but has the creating tag, then it could + # be under reconfiguration, such as plugging the GPU. In this case we + # should consider the node is not terminated, it will be turned on later + return not self.client.is_vm_creating(node_id) + + def node_tags(self, node_id): + with self.tag_cache_lock: + return self.tag_cache[node_id] + + def external_ip(self, node_id): + return self.client.get_vm_external_ip(node_id) + + def internal_ip(self, node_id): + # Currently vSphere VMs do not show an internal IP. So we just return the + # external IP + return self.client.get_vm_external_ip(node_id) + + def set_node_tags(self, node_id, tags): + # This method gets called from the Ray and it passes + # node_id. It updates old tags (if present) with new values. + with self.tag_cache_lock: + for k, v in tags.items(): + # update tags for node_id + self.tag_cache[node_id][k] = v + logger.info(f"Updated tags for {node_id} to: {self.tag_cache[node_id]}") + + def create_node(self, node_config, tags, count) -> Dict[str, Any]: + """Creates instances. + + Returns dict mapping instance id to VM object for the created + instances. + """ + to_be_launched_node_count = count + created_nodes_dict = {} + if to_be_launched_node_count > 0: + created_nodes_dict = self.client.create_nodes( + tags, to_be_launched_node_count, node_config + ) + # make sure to mark newly created nodes as ready + # so autoscaler shouldn't provision new ones + with self.tag_cache_lock: + for node_id in created_nodes_dict.keys(): + self.tag_cache[node_id] = tags.copy() + self.tag_cache[node_id][TAG_RAY_NODE_STATUS] = STATUS_SETTING_UP + self.tag_cache[node_id][TAG_RAY_NODE_NAME] = node_id + self.tag_cache[node_id][TAG_RAY_CLUSTER_NAME] = self.cluster_name + logger.info( + f"Node {node_id} created with tags: {self.tag_cache[node_id]}" + ) + return created_nodes_dict + + def terminate_node(self, node_id): + if not node_id or self.client.is_vm_creating(node_id): + return + # Delete node iff it is either in a running or a failure state + self.client.delete_node(node_id) + with self.tag_cache_lock: + if node_id in self.tag_cache: + self.tag_cache.pop(node_id) + + def terminate_nodes(self, node_ids): + if not node_ids: + return + for node_id in node_ids: + self.terminate_node(node_id) + + def safe_to_scale(self) -> bool: + return self.client.safe_to_scale() diff --git a/lib/python3.12/site-packages/ray/autoscaler/aliyun/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/aliyun/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/aliyun/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/aliyun/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50de5254e4d4f8fc09cb8238366d6240fc9452a0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/aliyun/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/aws/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/aws/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/aws/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/aws/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e72d4e335b22cc6b405e555f8780cb8d2d3ae6b Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/aws/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/aws/cloudwatch/prometheus.yml b/lib/python3.12/site-packages/ray/autoscaler/aws/cloudwatch/prometheus.yml new file mode 100644 index 0000000000000000000000000000000000000000..98a503a9fbf979c398f872f801726a31fa66774c --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/aws/cloudwatch/prometheus.yml @@ -0,0 +1,15 @@ +# Prometheus config file + +# my global config +global: + scrape_interval: 10s + evaluation_interval: 10s + scrape_timeout: 10s + +# use ray file-based service discovery file as scrape target. +scrape_configs: +- job_name: 'ray' + file_sd_configs: + - files: + - '/tmp/ray/prom_metrics_service_discovery.json' + refresh_interval: 1m diff --git a/lib/python3.12/site-packages/ray/autoscaler/aws/cloudwatch/ray_prometheus_waiter.sh b/lib/python3.12/site-packages/ray/autoscaler/aws/cloudwatch/ray_prometheus_waiter.sh new file mode 100644 index 0000000000000000000000000000000000000000..4494d5ece79081fc4b545b1ec4e9427703e96603 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/aws/cloudwatch/ray_prometheus_waiter.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +MAX_ATTEMPTS=120 +DELAY_SECONDS=10 +RAY_PROM_METRICS_FILE_PATH="/tmp/ray/prom_metrics_service_discovery.json" +CLUSTER_NAME=$1 +while [ $MAX_ATTEMPTS -gt 0 ]; do + if [ -f $RAY_PROM_METRICS_FILE_PATH ]; then + echo "Ray Prometheus metrics service discovery file found at: $RAY_PROM_METRICS_FILE_PATH." + echo "Restarting cloudwatch agent.This may take a few minutes..." + sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -m ec2 -a stop + echo "Cloudwatch agent stopped, starting cloudwatch agent..." + sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c "ssm:AmazonCloudWatch-ray_agent_config_$CLUSTER_NAME" + echo "Cloudwatch agent successfully restarted!" + exit 0 + else + echo "Ray Prometheus metrics service discovery file not found at: $RAY_PROM_METRICS_FILE_PATH. Will check again in $DELAY_SECONDS seconds..." + sleep $DELAY_SECONDS + MAX_ATTEMPTS=$((MAX_ATTEMPTS-1)) + fi +done +echo "Ray Prometheus metrics service discovery file not found at: $RAY_PROM_METRICS_FILE_PATH. Ray system metrics will not be available in CloudWatch." +exit 1 diff --git a/lib/python3.12/site-packages/ray/autoscaler/aws/defaults.yaml b/lib/python3.12/site-packages/ray/autoscaler/aws/defaults.yaml new file mode 100644 index 0000000000000000000000000000000000000000..507a53f8cf99b3e1c6595afdc5685e8cd4901a17 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/aws/defaults.yaml @@ -0,0 +1,144 @@ +# An unique identifier for the head node and workers of this cluster. +cluster_name: default + +# The maximum number of workers nodes to launch in addition to the head +# node. +max_workers: 2 + +# The autoscaler will scale up the cluster faster with higher upscaling speed. +# E.g., if the task requires adding more nodes then autoscaler will gradually +# scale up the cluster in chunks of upscaling_speed*currently_running_nodes. +# This number should be > 0. +upscaling_speed: 1.0 + +# This executes all commands on all nodes in the docker container, +# and opens all the necessary ports to support the Ray cluster. +# Empty string means disabled. +docker: {} + +# If a node is idle for this many minutes, it will be removed. +idle_timeout_minutes: 5 + +# Cloud-provider specific configuration. +provider: + type: aws + region: us-west-2 + # Availability zone(s), comma-separated, that nodes may be launched in. + # Nodes will be launched in the first listed availability zone and will + # be tried in the subsequent availability zones if launching fails. + availability_zone: us-west-2a,us-west-2b + # Whether to allow node reuse. If set to False, nodes will be terminated + # instead of stopped. + cache_stopped_nodes: True # If not present, the default is True. + +# How Ray will authenticate with newly launched nodes. +auth: + ssh_user: ubuntu +# By default Ray creates a new private keypair, but you can also use your own. +# If you do so, make sure to also set "KeyName" in the head and worker node +# configurations below. +# ssh_private_key: /path/to/your/key.pem + +# Tell the autoscaler the allowed node types and the resources they provide. +# The key is the name of the node type, which is just for debugging purposes. +# The node config specifies the launch config and physical instance type. +available_node_types: + ray.head.default: + # The node type's CPU and GPU resources are auto-detected based on AWS instance type. + # If desired, you can override the autodetected CPU and GPU resources advertised to the autoscaler. + # You can also set custom resources. + # For example, to mark a node type as having 1 CPU, 1 GPU, and 5 units of a resource called "custom", set + # resources: {"CPU": 1, "GPU": 1, "custom": 5} + resources: {} + # Provider-specific config for this node type, e.g. instance type. By default + # Ray will auto-configure unspecified fields such as SubnetId and KeyName. + # For more documentation on available fields, see: + # http://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.ServiceResource.create_instances + node_config: + InstanceType: m5.large + # You can provision additional disk space with a conf as follows + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeSize: 256 + # Additional options in the boto docs. + ray.worker.default: + # The minimum number of nodes of this type to launch. + # This number should be >= 0. + min_workers: 0 + # The node type's CPU and GPU resources are auto-detected based on AWS instance type. + # If desired, you can override the autodetected CPU and GPU resources advertised to the autoscaler. + # You can also set custom resources. + # For example, to mark a node type as having 1 CPU, 1 GPU, and 5 units of a resource called "custom", set + # resources: {"CPU": 1, "GPU": 1, "custom": 5} + resources: {} + # Provider-specific config for this node type, e.g. instance type. By default + # Ray will auto-configure unspecified fields such as SubnetId and KeyName. + # For more documentation on available fields, see: + # http://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.ServiceResource.create_instances + node_config: + InstanceType: m5.large + # Run workers on spot by default. Comment this out to use on-demand. + InstanceMarketOptions: + MarketType: spot + # Additional options can be found in the boto docs, e.g. + # SpotOptions: + # MaxPrice: MAX_HOURLY_PRICE + # Additional options in the boto docs. + +# Specify the node type of the head node (as configured above). +head_node_type: ray.head.default + +# Files or directories to copy to the head and worker nodes. The format is a +# dictionary from REMOTE_PATH: LOCAL_PATH, e.g. +file_mounts: { +# "/path1/on/remote/machine": "/path1/on/local/machine", +# "/path2/on/remote/machine": "/path2/on/local/machine", +} + +# Files or directories to copy from the head node to the worker nodes. The format is a +# list of paths. The same path on the head node will be copied to the worker node. +# This behavior is a subset of the file_mounts behavior. In the vast majority of cases +# you should just use file_mounts. Only use this if you know what you're doing! +cluster_synced_files: [] + +# Whether changes to directories in file_mounts or cluster_synced_files in the head node +# should sync to the worker node continuously +file_mounts_sync_continuously: False + +# Patterns for files to exclude when running rsync up or rsync down +rsync_exclude: [] + +# Pattern files to use for filtering out files when running rsync up or rsync down. The file is searched for +# in the source directory and recursively through all subdirectories. For example, if .gitignore is provided +# as a value, the behavior will match git's behavior for finding and using .gitignore files. +rsync_filter: [] + +# List of commands that will be run before `setup_commands`. If docker is +# enabled, these commands will run outside the container and before docker +# is setup. +initialization_commands: [] + +# List of shell commands to run to set up nodes. +setup_commands: + - >- + (stat $HOME/anaconda3/envs/tensorflow2_p310/ &> /dev/null && + echo 'export PATH="$HOME/anaconda3/envs/tensorflow2_p310/bin:$PATH"' >> ~/.bashrc) || true + - which ray || pip install -U "ray[default] @ https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp310-cp310-manylinux2014_x86_64.whl" + +# Custom commands that will be run on the head node after common setup. +head_setup_commands: + - pip install 'boto3>=1.4.8' # 1.4.8 adds InstanceMarketOptions + +# Custom commands that will be run on worker nodes after common setup. +worker_setup_commands: [] + +# Command to start ray on the head node. You don't need to change this. +head_start_ray_commands: + - ray stop + - ulimit -n 65536; ray start --head --port=6379 --object-manager-port=8076 --autoscaling-config=~/ray_bootstrap_config.yaml --dashboard-host=0.0.0.0 + +# Command to start ray on worker nodes. You don't need to change this. +worker_start_ray_commands: + - ray stop + - ulimit -n 65536; ray start --address=$RAY_HEAD_IP:6379 --object-manager-port=8076 diff --git a/lib/python3.12/site-packages/ray/autoscaler/azure/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/azure/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/azure/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/azure/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a76329ba420ba509b4135b410fa538c674b2320 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/azure/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/azure/defaults.yaml b/lib/python3.12/site-packages/ray/autoscaler/azure/defaults.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6625818697e087564e90f3185b6261ef5abbd91b --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/azure/defaults.yaml @@ -0,0 +1,164 @@ +# An unique identifier for the head node and workers of this cluster. +cluster_name: default + +# The maximum number of workers nodes to launch in addition to the head +# node. +max_workers: 2 + +# The autoscaler will scale up the cluster faster with higher upscaling speed. +# E.g., if the task requires adding more nodes then autoscaler will gradually +# scale up the cluster in chunks of upscaling_speed*currently_running_nodes. +# This number should be > 0. +upscaling_speed: 1.0 + +# This executes all commands on all nodes in the docker container, +# and opens all the necessary ports to support the Ray cluster. +# Empty object means disabled. +docker: {} + +# If a node is idle for this many minutes, it will be removed. +idle_timeout_minutes: 5 + +# Cloud-provider specific configuration. +provider: + type: azure + # https://azure.microsoft.com/en-us/global-infrastructure/locations + location: westus2 + resource_group: ray-cluster + # set subscription id otherwise the default from az cli will be used + # subscription_id: 00000000-0000-0000-0000-000000000000 + # set unique subnet mask or a random mask will be used + # subnet_mask: 10.0.0.0/16 + # set unique id for resources in this cluster + # if not set a default id will be generated based on the resource group and cluster name + # unique_id: RAY1 + # Availability zones for VM placement (comma-separated). Examples: + # availability_zone: "1,2,3" # Use zones 1, 2, and 3 + # availability_zone: "1" # Use only zone 1 + # availability_zone: "none" # Explicitly disable zones + availability_zone: "auto" # Let Azure automatically pick zones + + +# How Ray will authenticate with newly launched nodes. +auth: + ssh_user: ubuntu + # SSH keys will be auto-generated with Ray-specific names if not specified + # Uncomment and specify custom paths if you want to use different existing keys: + # ssh_private_key: /path/to/your/key.pem + # ssh_public_key: /path/to/your/key.pub + +# More specific customization to node configurations can be made using the ARM template azure-vm-template.json file +# See documentation here: https://docs.microsoft.com/en-us/azure/templates/microsoft.compute/2019-03-01/virtualmachines +# Changes to the local file will be used during deployment of the head node, however worker nodes deployment occurs +# on the head node, so changes to the template must be included in the wheel file used in setup_commands section below + +# Tell the autoscaler the allowed node types and the resources they provide. +# The key is the name of the node type, which is just for debugging purposes. +# The node config specifies the launch config and physical instance type. +available_node_types: + ray.head.default: + resources: {"CPU": 2} + # Provider-specific config, e.g. instance type. + node_config: + azure_arm_parameters: + vmSize: Standard_D2s_v3 + # List images https://docs.microsoft.com/en-us/azure/virtual-machines/linux/cli-ps-findimage + imagePublisher: microsoft-dsvm + imageOffer: ubuntu-2204 + imageSku: 2204-gen2 + imageVersion: latest + # Head node: explicitly disable availability zones + availability_zone: "none" + + ray.worker.default: + # The minimum number of nodes of this type to launch. + # This number should be >= 0. + min_workers: 0 + # The resources provided by this node type. + resources: {"CPU": 2} + # Provider-specific config, e.g. instance type. + node_config: + azure_arm_parameters: + vmSize: Standard_D2s_v3 + # List images https://docs.microsoft.com/en-us/azure/virtual-machines/linux/cli-ps-findimage + imagePublisher: microsoft-dsvm + imageOffer: ubuntu-2204 + imageSku: 2204-gen2 + imageVersion: latest + # comment lines below to not use Spot instances + priority: Spot + # set a maximum price for spot instances if desired + # billingProfile: + # maxPrice: -1 + # Workers: inherit provider availability_zone setting + # Options: "1,2,3" for specific zones, "none" to disable zones, + # or "auto" to let Azure pick zones automatically + +# Specify the node type of the head node (as configured above). +head_node_type: ray.head.default + +# Files or directories to copy to the head and worker nodes. The format is a +# dictionary from REMOTE_PATH: LOCAL_PATH, e.g. +file_mounts: { +# "/path1/on/remote/machine": "/path1/on/local/machine", +# "/path2/on/remote/machine": "/path2/on/local/machine", +} + +# Files or directories to copy from the head node to the worker nodes. The format is a +# list of paths. The same path on the head node will be copied to the worker node. +# This behavior is a subset of the file_mounts behavior. In the vast majority of cases +# you should just use file_mounts. Only use this if you know what you're doing! +cluster_synced_files: [] + +# Whether changes to directories in file_mounts or cluster_synced_files in the head node +# should sync to the worker node continuously +file_mounts_sync_continuously: False + +# Patterns for files to exclude when running rsync up or rsync down +rsync_exclude: [] + +# Pattern files to use for filtering out files when running rsync up or rsync down. The file is searched for +# in the source directory and recursively through all subdirectories. For example, if .gitignore is provided +# as a value, the behavior will match git's behavior for finding and using .gitignore files. +rsync_filter: [] + +# List of commands that will be run before `setup_commands`. If docker is +# enabled, these commands will run outside the container and before docker +# is setup. +initialization_commands: + # get rid of annoying Ubuntu message + - touch ~/.sudo_as_admin_successful + +# List of shell commands to run to set up nodes. +setup_commands: + # Note: if you're developing Ray, you probably want to create an AMI that + # has your Ray repo pre-cloned. Then, you can replace the pip installs + # below with a git checkout (and possibly a recompile). + # Note: The Ubuntu 22.04 dsvm image has a few venvs already configured but + # they all contain python modules that are not compatible with Ray at the moment. + - (which conda && echo 'eval "$(conda shell.bash hook)"' >> ~/.bashrc) || true + - conda tos accept + - conda create -n ray-env python=3.10 -y + - conda activate ray-env && echo 'conda activate ray-env' >> ~/.bashrc + - which ray || pip install -U "ray[default] @ https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp310-cp310-manylinux2014_x86_64.whl" + # Consider uncommenting these if you also want to run apt-get commands during setup + # - sudo pkill -9 apt-get || true + # - sudo pkill -9 dpkg || true + # - sudo dpkg --configure -a + +# Custom commands that will be run on the head node after common setup. +head_setup_commands: + - pip install -U azure-core==1.35.0 azure-identity==1.23.1 azure-mgmt-compute==35.0.0 azure-mgmt-network==29.0.0 azure-mgmt-resource==24.0.0 azure-common==1.1.28 msrest==0.7.1 msrestazure==0.6.4.post1 + +# Custom commands that will be run on worker nodes after common setup. +worker_setup_commands: [] + +# Command to start ray on the head node. You don't need to change this. +head_start_ray_commands: + - ray stop + - ulimit -n 65536; ray start --head --port=6379 --object-manager-port=8076 --autoscaling-config=~/ray_bootstrap_config.yaml --dashboard-host=0.0.0.0 + +# Command to start ray on worker nodes. You don't need to change this. +worker_start_ray_commands: + - ray stop + - ulimit -n 65536; ray start --address=$RAY_HEAD_IP:6379 --object-manager-port=8076 diff --git a/lib/python3.12/site-packages/ray/autoscaler/batching_node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/batching_node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..6a9118585fa54c0e548ed0a5b5e69268107aa018 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/batching_node_provider.py @@ -0,0 +1,255 @@ +import logging +from collections import defaultdict +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Set + +from ray.autoscaler._private.constants import ( + DISABLE_LAUNCH_CONFIG_CHECK_KEY, + DISABLE_NODE_UPDATERS_KEY, + FOREGROUND_NODE_LAUNCH_KEY, +) +from ray.autoscaler._private.util import NodeID, NodeIP, NodeKind, NodeStatus, NodeType +from ray.autoscaler.node_provider import NodeProvider +from ray.autoscaler.tags import ( + NODE_KIND_HEAD, + TAG_RAY_NODE_KIND, + TAG_RAY_NODE_STATUS, + TAG_RAY_REPLICA_INDEX, + TAG_RAY_USER_NODE_TYPE, +) + +logger = logging.getLogger(__name__) + + +@dataclass +class ScaleRequest: + """Stores desired scale computed by the autoscaler. + + Attributes: + desired_num_workers: Map of worker NodeType to desired number of workers of + that type. + workers_to_delete: List of ids of nodes that should be removed. + """ + + desired_num_workers: Dict[NodeType, int] = field(default_factory=dict) + workers_to_delete: Set[NodeID] = field(default_factory=set) + + +@dataclass +class NodeData: + """Stores all data about a Ray node needed by the autoscaler. + + Attributes: + kind: Whether the node is the head or a worker. + type: The user-defined type of the node. + replica_index: An identifier for nodes in a replica of a TPU worker group. + This value is set as a Pod label by a GKE webhook when TPUs are requested + ip: Cluster-internal ip of the node. ip can be None if the ip + has not yet been assigned. + status: The status of the node. You must adhere to the following semantics + for status: + * The status must be "up-to-date" if and only if the node is running. + * The status must be "update-failed" if and only if the node is in an + unknown or failed state. + * If the node is in a pending (starting-up) state, the status should be + a brief user-facing description of why the node is pending. + """ + + kind: NodeKind + type: NodeType + ip: Optional[NodeIP] + status: NodeStatus + replica_index: Optional[str] = None + + +class BatchingNodeProvider(NodeProvider): + """Abstract subclass of NodeProvider meant for use with external cluster managers. + + Batches reads of cluster state into a single method, get_node_data, called at the + start of an autoscaling update. + + Batches modifications to cluster state into a single method, submit_scale_request, + called at the end of an autoscaling update. + + Implementing a concrete subclass of BatchingNodeProvider only requires overriding + get_node_data() and submit_scale_request(). + + See the method docstrings for more information. + + Note that an autoscaling update may be conditionally + cancelled using the optional method safe_to_scale() + of the root NodeProvider. + """ + + def __init__( + self, + provider_config: Dict[str, Any], + cluster_name: str, + ) -> None: + NodeProvider.__init__(self, provider_config, cluster_name) + self.node_data_dict: Dict[NodeID, NodeData] = {} + + # These flags enforce correct behavior for single-threaded node providers + # which interact with external cluster managers: + assert ( + provider_config.get(DISABLE_NODE_UPDATERS_KEY, False) is True + ), f"To use BatchingNodeProvider, must set `{DISABLE_NODE_UPDATERS_KEY}:True`." + assert provider_config.get(DISABLE_LAUNCH_CONFIG_CHECK_KEY, False) is True, ( + "To use BatchingNodeProvider, must set " + f"`{DISABLE_LAUNCH_CONFIG_CHECK_KEY}:True`." + ) + assert ( + provider_config.get(FOREGROUND_NODE_LAUNCH_KEY, False) is True + ), f"To use BatchingNodeProvider, must set `{FOREGROUND_NODE_LAUNCH_KEY}:True`." + + # self.scale_change_needed tracks whether we need to update scale. + # set to True in create_node and terminate_nodes calls + # reset to False in non_terminated_nodes, which occurs at the start of the + # autoscaling update. For good measure, also set to false in post_process. + self.scale_change_needed = False + + self.scale_request = ScaleRequest() + + # Initialize map of replica indices to nodes in that replica + self.replica_index_to_nodes = defaultdict(list[str]) + + def get_node_data(self) -> Dict[NodeID, NodeData]: + """Queries cluster manager for node info. Returns a mapping from node id to + NodeData. + + Each NodeData value must adhere to the semantics of the NodeData docstring. + (Note in particular the requirements for NodeData.status.) + + Consistency requirement: + If a node id was present in ScaleRequest.workers_to_delete of a previously + submitted scale request, it should no longer be present as a key in + get_node_data. + (Node termination must be registered immediately when submit_scale_request + returns.) + """ + raise NotImplementedError + + def submit_scale_request(self, scale_request: ScaleRequest) -> None: + """Tells the cluster manager which nodes to delete and how many nodes of + each node type to maintain. + + Consistency requirement: + If a node id was present in ScaleRequest.workers_to_delete of a previously + submitted scale request, it should no longer be present as key in get_node_data. + (Node termination must be registered immediately when submit_scale_request + returns.) + """ + raise NotImplementedError + + def post_process(self) -> None: + """Submit a scale request if it is necessary to do so.""" + if self.scale_change_needed: + self.submit_scale_request(self.scale_request) + self.scale_change_needed = False + + def non_terminated_nodes(self, tag_filters: Dict[str, str]) -> List[str]: + self.scale_change_needed = False + self.node_data_dict = self.get_node_data() + + # Initialize ScaleRequest + self.scale_request = ScaleRequest( + desired_num_workers=self.cur_num_workers(), # Current scale + workers_to_delete=set(), # No workers to delete yet + ) + all_nodes = list(self.node_data_dict.keys()) + self.replica_index_to_nodes.clear() + for node_id in all_nodes: + replica_index = self.node_data_dict[node_id].replica_index + # Only add node to map if it belongs to a multi-host podslice + if replica_index is not None: + self.replica_index_to_nodes[replica_index].append(node_id) + # Support filtering by TAG_RAY_NODE_KIND, TAG_RAY_NODE_STATUS, and + # TAG_RAY_USER_NODE_TYPE. + # The autoscaler only uses tag_filters={}, + # but filtering by the these keys is useful for testing. + filtered_nodes = [ + node + for node in all_nodes + if tag_filters.items() <= self.node_tags(node).items() + ] + return filtered_nodes + + def cur_num_workers(self): + """Returns dict mapping node type to the number of nodes of that type.""" + # Factor like this for convenient re-use. + return self._cur_num_workers(self.node_data_dict) + + def _cur_num_workers(self, node_data_dict: Dict[str, Any]): + num_workers_dict = defaultdict(int) + for node_data in node_data_dict.values(): + if node_data.kind == NODE_KIND_HEAD: + # Only track workers. + continue + num_workers_dict[node_data.type] += 1 + return num_workers_dict + + def node_tags(self, node_id: str) -> Dict[str, str]: + node_data = self.node_data_dict[node_id] + tags = { + TAG_RAY_NODE_KIND: node_data.kind, + TAG_RAY_NODE_STATUS: node_data.status, + TAG_RAY_USER_NODE_TYPE: node_data.type, + } + if node_data.replica_index is not None: + tags[TAG_RAY_REPLICA_INDEX] = node_data.replica_index + return tags + + def internal_ip(self, node_id: str) -> str: + return self.node_data_dict[node_id].ip + + def create_node( + self, node_config: Dict[str, Any], tags: Dict[str, str], count: int + ) -> Optional[Dict[str, Any]]: + node_type = tags[TAG_RAY_USER_NODE_TYPE] + self.scale_request.desired_num_workers[node_type] += count + self.scale_change_needed = True + + def terminate_node(self, node_id: str) -> Optional[Dict[str, Any]]: + # Sanity check: We should never try to delete the same node twice. + if node_id in self.scale_request.workers_to_delete: + logger.warning( + f"Autoscaler tried to terminate node {node_id} twice in the same update" + ". Skipping termination request." + ) + return + + # Sanity check: We should never try to delete a node we haven't seen. + if node_id not in self.node_data_dict: + logger.warning( + f"Autoscaler tried to terminate unkown node {node_id}" + ". Skipping termination request." + ) + return + + node_type = self.node_data_dict[node_id].type + + # Sanity check: Don't request less than 0 nodes. + if self.scale_request.desired_num_workers[node_type] <= 0: + # This is logically impossible. + raise AssertionError( + "NodeProvider attempted to request less than 0 workers of type " + f"{node_type}. Skipping termination request." + ) + + # Terminate node + self.scale_request.desired_num_workers[node_type] -= 1 + self.scale_request.workers_to_delete.add(node_id) + + # Scale down all nodes in replica if node_id is part of a multi-host podslice + tags = self.node_tags(node_id) + if TAG_RAY_REPLICA_INDEX in tags: + node_replica_index = tags[TAG_RAY_REPLICA_INDEX] + for worker_id in self.replica_index_to_nodes[node_replica_index]: + # Check if worker has already been scheduled to delete + if worker_id not in self.scale_request.workers_to_delete: + self.scale_request.workers_to_delete.add(worker_id) + logger.info( + f"Autoscaler terminating node {worker_id} " + f"in multi-host replica {node_replica_index}." + ) + self.scale_change_needed = True diff --git a/lib/python3.12/site-packages/ray/autoscaler/command_runner.py b/lib/python3.12/site-packages/ray/autoscaler/command_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..94a6eaa08a5edff33d2bcf8eb982dec0e7d5320b --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/command_runner.py @@ -0,0 +1,92 @@ +from typing import Any, Dict, List, Optional, Tuple + +from ray.util.annotations import DeveloperAPI + + +@DeveloperAPI +class CommandRunnerInterface: + """Interface to run commands on a remote cluster node. + + **Important**: This is an INTERNAL API that is only exposed for the purpose + of implementing custom node providers. It is not allowed to call into + CommandRunner methods from any Ray package outside the autoscaler, only to + define new implementations for use with the "external" node provider + option. + + Command runner instances are returned by provider.get_command_runner().""" + + def run( + self, + cmd: Optional[str] = None, + timeout: int = 120, + exit_on_fail: bool = False, + port_forward: Optional[List[Tuple[int, int]]] = None, + with_output: bool = False, + environment_variables: Optional[Dict[str, object]] = None, + run_env: str = "auto", + ssh_options_override_ssh_key: str = "", + shutdown_after_run: bool = False, + ) -> str: + """Run the given command on the cluster node and optionally get output. + + WARNING: the cloudgateway needs arguments of "run" function to be json + dumpable to send them over HTTP requests. + + Args: + cmd: The command to run. + timeout: The command timeout in seconds. + exit_on_fail: Whether to sys exit on failure. + port_forward: List of (local, remote) ports to forward, or + a single tuple. + with_output: Whether to return output. + environment_variables (Dict[str, str | int | Dict[str, str]): + Environment variables that `cmd` should be run with. + run_env: Options: docker/host/auto. Used in + DockerCommandRunner to determine the run environment. + ssh_options_override_ssh_key: if provided, overwrites + SSHOptions class with SSHOptions(ssh_options_override_ssh_key). + shutdown_after_run: if provided, shutdowns down the machine + after executing the command with `sudo shutdown -h now`. + """ + raise NotImplementedError + + def run_rsync_up( + self, source: str, target: str, options: Optional[Dict[str, Any]] = None + ) -> None: + """Rsync files up to the cluster node. + + Args: + source: The (local) source directory or file. + target: The (remote) destination path. + """ + raise NotImplementedError + + def run_rsync_down( + self, source: str, target: str, options: Optional[Dict[str, Any]] = None + ) -> None: + """Rsync files down from the cluster node. + + Args: + source: The (remote) source directory or file. + target: The (local) destination path. + """ + raise NotImplementedError + + def remote_shell_command_str(self) -> str: + """Return the command the user can use to open a shell.""" + raise NotImplementedError + + def run_init( + self, *, as_head: bool, file_mounts: Dict[str, str], sync_run_yet: bool + ) -> Optional[bool]: + """Used to run extra initialization commands. + + Args: + as_head: Run as head image or worker. + file_mounts: Files to copy to the head and worker nodes. + sync_run_yet: Whether sync has been run yet. + + Returns: + optional: Whether initialization is necessary. + """ + pass diff --git a/lib/python3.12/site-packages/ray/autoscaler/gcp/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/gcp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/gcp/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/gcp/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e143af7e8c95fac9be289322e9058d618d1078a Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/gcp/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/gcp/defaults.yaml b/lib/python3.12/site-packages/ray/autoscaler/gcp/defaults.yaml new file mode 100644 index 0000000000000000000000000000000000000000..79da71efe142dff528fd80f64bac2d80987ac57a --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/gcp/defaults.yaml @@ -0,0 +1,171 @@ +# An unique identifier for the head node and workers of this cluster. +cluster_name: default + +# The maximum number of workers nodes to launch in addition to the head +# node. +max_workers: 2 + +# The autoscaler will scale up the cluster faster with higher upscaling speed. +# E.g., if the task requires adding more nodes then autoscaler will gradually +# scale up the cluster in chunks of upscaling_speed*currently_running_nodes. +# This number should be > 0. +upscaling_speed: 1.0 + +# This executes all commands on all nodes in the docker container, +# and opens all the necessary ports to support the Ray cluster. +# Empty string means disabled. +docker: {} + +# If a node is idle for this many minutes, it will be removed. +idle_timeout_minutes: 5 + +# Cloud-provider specific configuration. +provider: + type: gcp + region: us-west1 + availability_zone: us-west1-a + project_id: null # Globally unique project id + +# How Ray will authenticate with newly launched nodes. +auth: + ssh_user: ubuntu +# By default Ray creates a new private keypair, but you can also use your own. +# If you do so, make sure to also set "KeyName" in the head and worker node +# configurations below. This requires that you have added the key into the +# project wide meta-data. +# ssh_private_key: /path/to/your/key.pem + +# Tell the autoscaler the allowed node types and the resources they provide. +# The key is the name of the node type, which is just for debugging purposes. +# The node config specifies the launch config and physical instance type. +available_node_types: + ray_head_default: + # The resources provided by this node type. + resources: {"CPU": 2} + # Provider-specific config for this node type, e.g. instance type. By default + # Ray will auto-configure unspecified fields such as subnets and ssh-keys. + # For more documentation on available fields, see: + # https://cloud.google.com/compute/docs/reference/rest/v1/instances/insert + node_config: + machineType: n1-standard-2 + disks: + - boot: true + autoDelete: true + type: PERSISTENT + initializeParams: + diskSizeGb: 50 + # See https://cloud.google.com/compute/docs/images for more images + sourceImage: projects/deeplearning-platform-release/global/images/family/common-cpu + + # Additional options can be found in in the compute docs at + # https://cloud.google.com/compute/docs/reference/rest/v1/instances/insert + + # If the network interface is specified as below in both head and worker + # nodes, the manual network config is used. Otherwise an existing subnet is + # used. To use a shared subnet, ask the subnet owner to grant permission + # for 'compute.subnetworks.use' to the ray autoscaler account... + # networkInterfaces: + # - kind: compute#networkInterface + # subnetwork: path/to/subnet + # aliasIpRanges: [] + ray_worker_small: + # The minimum number of nodes of this type to launch. + # This number should be >= 0. + min_workers: 0 + # The resources provided by this node type. + resources: {"CPU": 2} + # Provider-specific config for this node type, e.g. instance type. By default + # Ray will auto-configure unspecified fields such as subnets and ssh-keys. + # For more documentation on available fields, see: + # https://cloud.google.com/compute/docs/reference/rest/v1/instances/insert + node_config: + machineType: n1-standard-2 + disks: + - boot: true + autoDelete: true + type: PERSISTENT + initializeParams: + diskSizeGb: 50 + # See https://cloud.google.com/compute/docs/images for more images + sourceImage: projects/deeplearning-platform-release/global/images/family/common-cpu + # Run workers on preemtible instance by default. + # Comment this out to use on-demand. + scheduling: + - preemptible: true + + # Additional options can be found in in the compute docs at + # https://cloud.google.com/compute/docs/reference/rest/v1/instances/insert + +# Specify the node type of the head node (as configured above). +head_node_type: ray_head_default + +# Files or directories to copy to the head and worker nodes. The format is a +# dictionary from REMOTE_PATH: LOCAL_PATH, e.g. +file_mounts: { +# "/path1/on/remote/machine": "/path1/on/local/machine", +# "/path2/on/remote/machine": "/path2/on/local/machine", +} + +# Files or directories to copy from the head node to the worker nodes. The format is a +# list of paths. The same path on the head node will be copied to the worker node. +# This behavior is a subset of the file_mounts behavior. In the vast majority of cases +# you should just use file_mounts. Only use this if you know what you're doing! +cluster_synced_files: [] + +# Whether changes to directories in file_mounts or cluster_synced_files in the head node +# should sync to the worker node continuously +file_mounts_sync_continuously: False + +# Patterns for files to exclude when running rsync up or rsync down +rsync_exclude: [] + +# Pattern files to use for filtering out files when running rsync up or rsync down. The file is searched for +# in the source directory and recursively through all subdirectories. For example, if .gitignore is provided +# as a value, the behavior will match git's behavior for finding and using .gitignore files. +rsync_filter: [] + +# List of commands that will be run before `setup_commands`. If docker is +# enabled, these commands will run outside the container and before docker +# is setup. +initialization_commands: [] + +# List of shell commands to run to set up nodes. +setup_commands: + # Note: if you're developing Ray, you probably want to create an AMI that + # has your Ray repo pre-cloned. Then, you can replace the pip installs + # below with a git checkout (and possibly a recompile). + # - echo 'export PATH="$HOME/anaconda3/envs/tensorflow_p36/bin:$PATH"' >> ~/.bashrc + # Install ray if not present + - >- + (stat /opt/conda/bin/ &> /dev/null && + echo 'export PATH="/opt/conda/bin:$PATH"' >> ~/.bashrc) || true + - which ray || pip install -U "ray[default] @ https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp37-cp37m-manylinux2014_x86_64.whl" + + +# Custom commands that will be run on the head node after common setup. +head_setup_commands: + - pip install google-api-python-client==1.7.8 + +# Custom commands that will be run on worker nodes after common setup. +worker_setup_commands: [] + +# Command to start ray on the head node. You don't need to change this. +head_start_ray_commands: + - ray stop + - >- + ulimit -n 65536; + ray start + --head + --port=6379 + --object-manager-port=8076 + --autoscaling-config=~/ray_bootstrap_config.yaml + --dashboard-host=0.0.0.0 + +# Command to start ray on worker nodes. You don't need to change this. +worker_start_ray_commands: + - ray stop + - >- + ulimit -n 65536; + ray start + --address=$RAY_HEAD_IP:6379 + --object-manager-port=8076 diff --git a/lib/python3.12/site-packages/ray/autoscaler/launch_and_verify_cluster.py b/lib/python3.12/site-packages/ray/autoscaler/launch_and_verify_cluster.py new file mode 100644 index 0000000000000000000000000000000000000000..3c5216a182984e198a95d277e9b9c680d2aea892 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/launch_and_verify_cluster.py @@ -0,0 +1,539 @@ +""" +This script automates the process of launching and verifying a Ray cluster using a given +cluster configuration file. It also handles cluster cleanup before and after the +verification process. The script requires one command-line argument: the path to the +cluster configuration file. + +Usage: + python launch_and_verify_cluster.py [--no-config-cache] [--retries NUM_RETRIES] + [--num-expected-nodes NUM_NODES] [--docker-override DOCKER_OVERRIDE] + [--wheel-override WHEEL_OVERRIDE] + +""" +import argparse +import json +import os +import re +import subprocess +import sys +import tempfile +import time +import traceback +from pathlib import Path + +import boto3 +import botocore +import yaml +from google.cloud import storage + +import ray +from ray.autoscaler._private.aws.config import RAY + + +def check_arguments(): + """ + Check command line arguments and return the cluster configuration file path, the + number of retries, the number of expected nodes, and the value of the + --no-config-cache flag. + """ + parser = argparse.ArgumentParser(description="Launch and verify a Ray cluster") + parser.add_argument( + "--no-config-cache", + action="store_true", + help="Pass the --no-config-cache flag to Ray CLI commands", + ) + parser.add_argument( + "--retries", + type=int, + default=3, + help="Number of retries for verifying Ray is running (default: 3)", + ) + parser.add_argument( + "--num-expected-nodes", + type=int, + default=1, + help="Number of nodes for verifying Ray is running (default: 1)", + ) + parser.add_argument( + "--docker-override", + choices=["disable", "latest", "nightly", "commit"], + default="disable", + help="Override the docker image used for the head node and worker nodes", + ) + parser.add_argument( + "--wheel-override", + type=str, + default="", + help="Override the wheel used for the head node and worker nodes", + ) + parser.add_argument( + "cluster_config", type=str, help="Path to the cluster configuration file" + ) + args = parser.parse_args() + + assert not ( + args.docker_override != "disable" and args.wheel_override != "" + ), "Cannot override both docker and wheel" + + return ( + args.cluster_config, + args.retries, + args.no_config_cache, + args.num_expected_nodes, + args.docker_override, + args.wheel_override, + ) + + +def get_docker_image(docker_override): + """ + Get the docker image to use for the head node and worker nodes. + + Args: + docker_override: The value of the --docker-override flag. + + Returns: + The docker image to use for the head node and worker nodes, or None if not + applicable. + """ + if docker_override == "latest": + return "rayproject/ray:latest-py39" + elif docker_override == "nightly": + return "rayproject/ray:nightly-py39" + elif docker_override == "commit": + if re.match("^[0-9]+.[0-9]+.[0-9]+$", ray.__version__): + return f"rayproject/ray:{ray.__version__}.{ray.__commit__[:6]}-py39" + else: + print( + "Error: docker image is only available for " + f"release version, but we get: {ray.__version__}" + ) + sys.exit(1) + return None + + +def check_file(file_path): + """ + Check if the provided file path is valid and readable. + + Args: + file_path: The path of the file to check. + + Raises: + SystemExit: If the file is not readable or does not exist. + """ + if not file_path.is_file() or not os.access(file_path, os.R_OK): + print(f"Error: Cannot read cluster configuration file: {file_path}") + sys.exit(1) + + +def override_wheels_url(config_yaml, wheel_url): + setup_commands = config_yaml.get("setup_commands", []) + setup_commands.append( + f'pip3 uninstall -y ray && pip3 install -U "ray[default] @ {wheel_url}"' + ) + config_yaml["setup_commands"] = setup_commands + + +def override_docker_image(config_yaml, docker_image): + docker_config = config_yaml.get("docker", {}) + docker_config["image"] = docker_image + docker_config["container_name"] = "ray_container" + assert docker_config.get("head_image") is None, "Cannot override head_image" + assert docker_config.get("worker_image") is None, "Cannot override worker_image" + config_yaml["docker"] = docker_config + + +def azure_authenticate(): + """Get Azure service principal credentials from AWS Secrets Manager and authenticate.""" + print("======================================") + print("Getting Azure credentials from AWS Secrets Manager...") + + # Initialize AWS Secrets Manager client + secrets_client = boto3.client("secretsmanager", region_name="us-west-2") + + # Get service principal credentials + secret_response = secrets_client.get_secret_value( + SecretId="azure-service-principal-oss-release" + ) + secret = secret_response["SecretString"] + + client_id = json.loads(secret)["client_id"] + tenant_id = json.loads(secret)["tenant_id"] + + # Get certificate + cert_response = secrets_client.get_secret_value( + SecretId="azure-service-principal-certificate" + ) + cert = cert_response["SecretString"] + + # Write certificate to temp file + tmp_dir = tempfile.mkdtemp() + cert_path = os.path.join(tmp_dir, "azure_cert.pem") + with open(cert_path, "w") as f: + f.write(cert) + + # Login to Azure + subprocess.check_call( + [ + "az", + "login", + "--service-principal", + "--username", + client_id, + "--certificate", + cert_path, + "--tenant", + tenant_id, + ] + ) + + +def download_ssh_key_aws(): + """Download the ssh key from the S3 bucket to the local machine.""" + print("======================================") + print("Downloading ssh key...") + # Create a Boto3 client to interact with S3 + s3_client = boto3.client("s3", region_name="us-west-2") + + # Set the name of the S3 bucket and the key to download + bucket_name = "aws-cluster-launcher-test" + key_name = "ray-autoscaler_59_us-west-2.pem" + + # Download the key from the S3 bucket to a local file + local_key_path = os.path.expanduser(f"~/.ssh/{key_name}") + if not os.path.exists(os.path.dirname(local_key_path)): + os.makedirs(os.path.dirname(local_key_path)) + s3_client.download_file(bucket_name, key_name, local_key_path) + + # Set permissions on the key file + os.chmod(local_key_path, 0o400) + + +def download_ssh_key_gcp(): + """Download the ssh key from the google cloud bucket to the local machine.""" + print("======================================") + print("Downloading ssh key from GCP...") + + # Initialize the GCP storage client + client = storage.Client() + + # Set the name of the GCS bucket and the blob (key) to download + bucket_name = "gcp-cluster-launcher-release-test-ssh-keys" + key_name = "ray-autoscaler_gcp_us-west1_anyscale-bridge-cd812d38_ubuntu_0.pem" + + # Get the bucket and blob + bucket = client.get_bucket(bucket_name) + blob = bucket.get_blob(key_name) + + # Download the blob to a local file + local_key_path = os.path.expanduser(f"~/.ssh/{key_name}") + if not os.path.exists(os.path.dirname(local_key_path)): + os.makedirs(os.path.dirname(local_key_path)) + blob.download_to_filename(local_key_path) + + # Set permissions on the key file + os.chmod(local_key_path, 0o400) + + +def cleanup_cluster(config_yaml, cluster_config): + """ + Clean up the cluster using the given cluster configuration file. + + Args: + cluster_config: The path of the cluster configuration file. + """ + print("======================================") + print("Cleaning up cluster...") + + # We do multiple retries here because sometimes the cluster + # fails to clean up properly, resulting in a non-zero exit code (e.g. + # when processes have to be killed forcefully). + + last_error = None + num_tries = 3 + for i in range(num_tries): + try: + env = os.environ.copy() + env.pop("PYTHONPATH", None) + subprocess.run( + ["ray", "down", "-v", "-y", str(cluster_config)], + check=True, + capture_output=True, + env=env, + ) + cleanup_security_groups(config_yaml) + # Final success + return + except subprocess.CalledProcessError as e: + print(f"ray down fails[{i+1}/{num_tries}]: ") + print(e.output.decode("utf-8")) + + # Print full traceback + traceback.print_exc() + + # Print stdout and stderr from ray down + print(f"stdout:\n{e.stdout.decode('utf-8')}") + print(f"stderr:\n{e.stderr.decode('utf-8')}") + + last_error = e + + raise last_error + + +def cleanup_security_group(ec2_client, id): + retry = 0 + while retry < 10: + try: + ec2_client.delete_security_group(GroupId=id) + return + except botocore.exceptions.ClientError as e: + if e.response["Error"]["Code"] == "DependencyViolation": + sleep_time = 2**retry + print( + f"Waiting {sleep_time}s for the instance to be terminated before deleting the security group {id}" # noqa E501 + ) + time.sleep(sleep_time) + retry += 1 + else: + print(f"Error deleting security group: {e}") + return + + +def ensure_ssh_keys_azure(): + """ + Ensure that the SSH keys for Azure tests exist, and create them if they don't. + """ + print("======================================") + print("Ensuring Azure SSH keys exist...") + private_key_path = os.path.expanduser("~/.ssh/ray-autoscaler-tests-ssh-key") + public_key_path = os.path.expanduser("~/.ssh/ray-autoscaler-tests-ssh-key.pub") + + if os.path.exists(private_key_path) and os.path.exists(public_key_path): + print("Azure SSH keys already exist.") + return + + print("Azure SSH keys not found. Creating new keys...") + ssh_dir = os.path.dirname(private_key_path) + if not os.path.exists(ssh_dir): + os.makedirs(ssh_dir, exist_ok=True) + + try: + subprocess.run( + [ + "ssh-keygen", + "-t", + "rsa", + "-b", + "4096", + "-f", + private_key_path, + "-N", + "", + "-C", + "ray-autoscaler-azure", + ], + check=True, + capture_output=True, + ) + print("Successfully created Azure SSH keys.") + except subprocess.CalledProcessError as e: + print("Error creating SSH keys:") + print(f"stdout:\n{e.stdout.decode('utf-8')}") + print(f"stderr:\n{e.stderr.decode('utf-8')}") + sys.exit(1) + + +def cleanup_security_groups(config): + provider_type = config.get("provider", {}).get("type") + if provider_type != "aws": + return + + try: + ec2_client = boto3.client("ec2", region_name="us-west-2") + response = ec2_client.describe_security_groups( + Filters=[ + { + "Name": "tag-key", + "Values": [RAY], + }, + { + "Name": "tag:ray-cluster-name", + "Values": [config["cluster_name"]], + }, + ] + ) + for security_group in response["SecurityGroups"]: + cleanup_security_group(ec2_client, security_group["GroupId"]) + except Exception as e: + print(f"Error cleaning up security groups: {e}") + + +def run_ray_commands( + config_yaml, + cluster_config, + retries, + no_config_cache, + num_expected_nodes=1, +): + """ + Run the necessary Ray commands to start a cluster, verify Ray is running, and clean + up the cluster. + + Args: + cluster_config: The path of the cluster configuration file. + retries: The number of retries for the verification step. + no_config_cache: Whether to pass the --no-config-cache flag to the ray CLI + commands. + """ + provider_type = config_yaml.get("provider", {}).get("type") + if provider_type == "azure": + azure_authenticate() + print("======================================") + print("Starting new cluster...") + cmd = ["ray", "up", "-v", "-y"] + if no_config_cache: + cmd.append("--no-config-cache") + cmd.append(str(cluster_config)) + env = os.environ.copy() + env.pop("PYTHONPATH", None) + try: + subprocess.run(cmd, check=True, capture_output=True, env=env) + except subprocess.CalledProcessError as e: + print(e.output) + # print stdout and stderr + print(f"stdout:\n{e.stdout.decode('utf-8')}") + print(f"stderr:\n{e.stderr.decode('utf-8')}") + raise e + + print("======================================") + print("Verifying Ray is running...") + + success = False + count = 0 + while count < retries: + try: + cmd = [ + "ray", + "exec", + "-v", + str(cluster_config), + ( + 'python -c \'import ray; ray.init("localhost:6379");' + + f" assert len(ray.nodes()) >= {num_expected_nodes}'" + ), + ] + if no_config_cache: + cmd.append("--no-config-cache") + subprocess.run(cmd, check=True, env=env) + success = True + break + except subprocess.CalledProcessError: + count += 1 + print(f"Verification failed. Retry attempt {count} of {retries}...") + time.sleep(60) + + if not success: + print("======================================") + print( + f"Error: Verification failed after {retries} attempts. Cleaning up cluster " + "before exiting..." + ) + cleanup_cluster(config_yaml, cluster_config) + print("======================================") + print("Exiting script.") + sys.exit(1) + + print("======================================") + print("Ray verification successful.") + + cleanup_cluster(config_yaml, cluster_config) + + print("======================================") + print("Finished executing script successfully.") + + +if __name__ == "__main__": + ( + cluster_config, + retries, + no_config_cache, + num_expected_nodes, + docker_override, + wheel_override, + ) = check_arguments() + cluster_config = Path(cluster_config) + check_file(cluster_config) + + print(f"Using cluster configuration file: {cluster_config}") + print(f"Number of retries for 'verify ray is running' step: {retries}") + print(f"Using --no-config-cache flag: {no_config_cache}") + print(f"Number of expected nodes for 'verify ray is running': {num_expected_nodes}") + + config_yaml = yaml.safe_load(cluster_config.read_text()) + # Make the cluster name unique + config_yaml["cluster_name"] = ( + config_yaml["cluster_name"] + "-" + str(int(time.time())) + ) + + print("======================================") + print(f"Overriding ray wheel...: {wheel_override}") + if wheel_override: + override_wheels_url(config_yaml, wheel_override) + + print("======================================") + print(f"Overriding docker image...: {docker_override}") + docker_override_image = get_docker_image(docker_override) + print(f"Using docker image: {docker_override_image}") + if docker_override_image: + override_docker_image(config_yaml, docker_override_image) + + provider_type = config_yaml.get("provider", {}).get("type") + config_yaml["provider"]["cache_stopped_nodes"] = False + if provider_type == "azure": + ensure_ssh_keys_azure() + elif provider_type == "aws": + download_ssh_key_aws() + elif provider_type == "gcp": + download_ssh_key_gcp() + # Get the active account email + account_email = ( + subprocess.run( + ["gcloud", "config", "get-value", "account"], + stdout=subprocess.PIPE, + check=True, + ) + .stdout.decode("utf-8") + .strip() + ) + print("Active account email:", account_email) + # Get the current project ID + project_id = ( + subprocess.run( + ["gcloud", "config", "get-value", "project"], + stdout=subprocess.PIPE, + check=True, + ) + .stdout.decode("utf-8") + .strip() + ) + print( + f"Injecting GCP project '{project_id}' into cluster configuration file..." + ) + config_yaml["provider"]["project_id"] = project_id + elif provider_type == "vsphere": + print("======================================") + print("VSPHERE provider detected.") + else: + print("======================================") + print("Provider type not recognized. Exiting script.") + sys.exit(1) + + # Create a new temporary file and dump the updated configuration into it + with tempfile.NamedTemporaryFile(suffix=".yaml") as temp: + temp.write(yaml.dump(config_yaml).encode("utf-8")) + temp.flush() + cluster_config = Path(temp.name) + run_ray_commands( + config_yaml, cluster_config, retries, no_config_cache, num_expected_nodes + ) diff --git a/lib/python3.12/site-packages/ray/autoscaler/local/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/local/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/local/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/local/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08f0b6d423e217928092f57fe413235d811045ae Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/local/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/local/__pycache__/coordinator_server.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/local/__pycache__/coordinator_server.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..553046884c659e7d4805aaf8adc2debc72c2c01b Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/local/__pycache__/coordinator_server.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/local/coordinator_server.py b/lib/python3.12/site-packages/ray/autoscaler/local/coordinator_server.py new file mode 100644 index 0000000000000000000000000000000000000000..7cca12645631bf04808247720174cf6e8cf7bde8 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/local/coordinator_server.py @@ -0,0 +1,132 @@ +"""Web server that runs on local/private clusters to coordinate and manage +different clusters for multiple users. It receives node provider function calls +through HTTP requests from remote CoordinatorSenderNodeProvider and runs them +locally in LocalNodeProvider. To start the webserver the user runs: +`python coordinator_server.py --ips --host --port `.""" +import argparse +import json +import logging +import socket +import threading +from http.server import HTTPServer, SimpleHTTPRequestHandler + +from ray._common.network_utils import build_address +from ray.autoscaler._private.local.node_provider import LocalNodeProvider + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def runner_handler(node_provider): + class Handler(SimpleHTTPRequestHandler): + """A custom handler for OnPremCoordinatorServer. + + Handles all requests and responses coming into and from the + remote CoordinatorSenderNodeProvider. + """ + + def _do_header(self, response_code=200, headers=None): + """Sends the header portion of the HTTP response. + + Args: + response_code: Standard HTTP response code + headers (list[tuples]): Standard HTTP response headers + """ + if headers is None: + headers = [("Content-type", "application/json")] + + self.send_response(response_code) + for key, value in headers: + self.send_header(key, value) + self.end_headers() + + def do_HEAD(self): + """HTTP HEAD handler method.""" + self._do_header() + + def do_GET(self): + """Processes requests from remote CoordinatorSenderNodeProvider.""" + if self.headers["content-length"]: + raw_data = ( + self.rfile.read(int(self.headers["content-length"])) + ).decode("utf-8") + logger.info( + "OnPremCoordinatorServer received request: " + str(raw_data) + ) + request = json.loads(raw_data) + response = getattr(node_provider, request["type"])(*request["args"]) + logger.info( + "OnPremCoordinatorServer response content: " + str(raw_data) + ) + response_code = 200 + message = json.dumps(response) + self._do_header(response_code=response_code) + self.wfile.write(message.encode()) + + return Handler + + +class OnPremCoordinatorServer(threading.Thread): + """Initializes HTTPServer and serves CoordinatorSenderNodeProvider forever. + + It handles requests from the remote CoordinatorSenderNodeProvider. The + requests are forwarded to LocalNodeProvider function calls. + """ + + def __init__(self, list_of_node_ips, host, port): + """Initialize HTTPServer and serve forever by invoking self.run().""" + + logger.info( + "Running on prem coordinator server on address " + build_address(host, port) + ) + threading.Thread.__init__(self) + self._port = port + self._list_of_node_ips = list_of_node_ips + address = (host, self._port) + config = {"list_of_node_ips": list_of_node_ips} + self._server = HTTPServer( + address, + runner_handler(LocalNodeProvider(config, cluster_name=None)), + ) + self.start() + + def run(self): + self._server.serve_forever() + + def shutdown(self): + """Shutdown the underlying server.""" + self._server.shutdown() + self._server.server_close() + + +def main(): + parser = argparse.ArgumentParser( + description="Please provide a list of node ips and port." + ) + parser.add_argument( + "--ips", required=True, help="Comma separated list of node ips." + ) + parser.add_argument( + "--host", + type=str, + required=False, + help="The Host on which the coordinator listens.", + ) + parser.add_argument( + "--port", + type=int, + required=True, + help="The port on which the coordinator listens.", + ) + args = parser.parse_args() + host = args.host or socket.gethostbyname(socket.gethostname()) + list_of_node_ips = args.ips.split(",") + OnPremCoordinatorServer( + list_of_node_ips=list_of_node_ips, + host=host, + port=args.port, + ) + + +if __name__ == "__main__": + main() diff --git a/lib/python3.12/site-packages/ray/autoscaler/local/defaults.yaml b/lib/python3.12/site-packages/ray/autoscaler/local/defaults.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8a3f8de6fb1fb1be8769256dbfc973512d33a75f --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/local/defaults.yaml @@ -0,0 +1,33 @@ +# This configuration file is used internally +# to fill default settings for on-prem Ray clusters +# bootstrapped by the Ray autoscaler. +# For annotated examples, see the example yamls in this directory. + +cluster_name: default + +auth: {} + +upscaling_speed: 1.0 +idle_timeout_minutes: 5 + +docker: {} + +# Defaults are empty to avoid any surprise changes to on-prem cluster's state. +# Refer to example yamls for examples of ray installation in setup commands. +initialization_commands: [] +setup_commands: [] +head_setup_commands: [] +worker_setup_commands: [] + +head_start_ray_commands: + - ray stop + - ulimit -c unlimited; ray start --head --port=6379 --autoscaling-config=~/ray_bootstrap_config.yaml +worker_start_ray_commands: + - ray stop + - ray start --address=$RAY_HEAD_IP:6379 + +file_mounts: {} +cluster_synced_files: [] +file_mounts_sync_continuously: false +rsync_exclude: [] +rsync_filter: [] diff --git a/lib/python3.12/site-packages/ray/autoscaler/node_launch_exception.py b/lib/python3.12/site-packages/ray/autoscaler/node_launch_exception.py new file mode 100644 index 0000000000000000000000000000000000000000..eb6bd25f2c612162b97bca2f721b4ebb9fad0242 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/node_launch_exception.py @@ -0,0 +1,37 @@ +from typing import Any, Optional, Tuple + +from ray.util.annotations import DeveloperAPI + + +@DeveloperAPI +class NodeLaunchException(Exception): + """A structured exception that can be thrown by a node provider during a + `create_node` call to pass additional information for observability. + """ + + def __init__( + self, + category: str, + description: str, + src_exc_info: Optional[Tuple[Any, Any, Any]], # The + ): + """Args: + category: A short (<20 chars) label for the error. + description: A longer, human readable description of the error. + src_exc_info: The source exception info if applicable. This is a + tuple of (type, exception, traceback) as returned by + sys.exc_info() + + """ + super().__init__(f"Node Launch Exception ({category}): {description}") + self.category = category + self.description = description + self.src_exc_info = src_exc_info + + def __reduce__(self): + # NOTE: Since tracebacks can't be pickled, we'll drop the optional + # traceback if we have to serialize this object. + return ( + self.__class__, + (self.category, self.description, None), + ) diff --git a/lib/python3.12/site-packages/ray/autoscaler/node_provider.py b/lib/python3.12/site-packages/ray/autoscaler/node_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..5565a7a4774e12db1ffacb1255950d27de46a73d --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/node_provider.py @@ -0,0 +1,274 @@ +import logging +from types import ModuleType +from typing import Any, Dict, List, Optional + +from ray.autoscaler._private.command_runner import DockerCommandRunner, SSHCommandRunner +from ray.autoscaler.command_runner import CommandRunnerInterface +from ray.util.annotations import DeveloperAPI + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +class NodeProvider: + """Interface for getting and returning nodes from a Cloud. + + **Important**: This is an INTERNAL API that is only exposed for the purpose + of implementing custom node providers. It is not allowed to call into + NodeProvider methods from any Ray package outside the autoscaler, only to + define new implementations of NodeProvider for use with the "external" node + provider option. + + NodeProviders are namespaced by the `cluster_name` parameter; they only + operate on nodes within that namespace. + + Nodes may be in one of three states: {pending, running, terminated}. Nodes + appear immediately once started by `create_node`, and transition + immediately to terminated when `terminate_node` is called. + + Threading and concurrency: + - The autoscaler calls the following methods from multiple threads + (NodeLauncher, NodeUpdaterThread, autoscaler main loop, and + NodeProviderAdapter executors). + - These methods MUST be thread-safe: + non_terminated_nodes, is_running, is_terminated, node_tags, internal_ip, + external_ip, get_node_id, create_node/create_node_with_resources_and_labels, + set_node_tags, terminate_node/terminate_nodes. + + TODO (rueian): make sure all the existing implementations are thread-safe. + """ + + def __init__(self, provider_config: Dict[str, Any], cluster_name: str) -> None: + self.provider_config = provider_config + self.cluster_name = cluster_name + self._internal_ip_cache: Dict[str, str] = {} + self._external_ip_cache: Dict[str, str] = {} + + def is_readonly(self) -> bool: + """Returns whether this provider is readonly. + + Readonly node providers do not allow nodes to be created or terminated. + """ + return False + + def non_terminated_nodes(self, tag_filters: Dict[str, str]) -> List[str]: + """Return a list of node ids filtered by the specified tags dict. + + This list must not include terminated nodes. For performance reasons, + providers are allowed to cache the result of a call to + non_terminated_nodes() to serve single-node queries + (e.g. is_running(node_id)). This means that non_terminate_nodes() must + be called again to refresh results. + + Examples: + >>> from ray.autoscaler.node_provider import NodeProvider + >>> from ray.autoscaler.tags import TAG_RAY_NODE_KIND + >>> provider = NodeProvider(...) # doctest: +SKIP + >>> provider.non_terminated_nodes( # doctest: +SKIP + ... {TAG_RAY_NODE_KIND: "worker"}) + ["node-1", "node-2"] + + """ + raise NotImplementedError + + def is_running(self, node_id: str) -> bool: + """Return whether the specified node is running.""" + raise NotImplementedError + + def is_terminated(self, node_id: str) -> bool: + """Return whether the specified node is terminated.""" + raise NotImplementedError + + def node_tags(self, node_id: str) -> Dict[str, str]: + """Returns the tags of the given node (string dict).""" + raise NotImplementedError + + def external_ip(self, node_id: str) -> str: + """Returns the external ip of the given node.""" + raise NotImplementedError + + def internal_ip(self, node_id: str) -> str: + """Returns the internal ip (Ray ip) of the given node.""" + raise NotImplementedError + + def get_node_id(self, ip_address: str, use_internal_ip: bool = False) -> str: + """Returns the node_id given an IP address. + + Assumes ip-address is unique per node. + + Args: + ip_address: Address of node. + use_internal_ip: Whether the ip address is + public or private. + + Raises: + ValueError: If not found. + """ + + def find_node_id(): + if use_internal_ip: + return self._internal_ip_cache.get(ip_address) + else: + return self._external_ip_cache.get(ip_address) + + if not find_node_id(): + all_nodes = self.non_terminated_nodes({}) + ip_func = self.internal_ip if use_internal_ip else self.external_ip + ip_cache = ( + self._internal_ip_cache if use_internal_ip else self._external_ip_cache + ) + for node_id in all_nodes: + ip_cache[ip_func(node_id)] = node_id + + if not find_node_id(): + if use_internal_ip: + known_msg = f"Worker internal IPs: {list(self._internal_ip_cache)}" + else: + known_msg = f"Worker external IP: {list(self._external_ip_cache)}" + raise ValueError(f"ip {ip_address} not found. " + known_msg) + + return find_node_id() + + def create_node( + self, node_config: Dict[str, Any], tags: Dict[str, str], count: int + ) -> Optional[Dict[str, Any]]: + """Creates a number of nodes within the namespace. + + Optionally returns a mapping from created node ids to node metadata. + + Optionally may throw a + ray.autoscaler.node_launch_exception.NodeLaunchException which the + autoscaler may use to provide additional functionality such as + observability. + + """ + raise NotImplementedError + + def create_node_with_resources_and_labels( + self, + node_config: Dict[str, Any], + tags: Dict[str, str], + count: int, + resources: Dict[str, float], + labels: Dict[str, str], + ) -> Optional[Dict[str, Any]]: + """Create nodes with a given resource and label config. + + This is the method actually called by the autoscaler. Prefer to + implement this when possible directly, otherwise it delegates to the + create_node() implementation. + + Optionally may throw a ray.autoscaler.node_launch_exception.NodeLaunchException. + """ + return self.create_node(node_config, tags, count) + + def set_node_tags(self, node_id: str, tags: Dict[str, str]) -> None: + """Sets the tag values (string dict) for the specified node.""" + raise NotImplementedError + + def terminate_node(self, node_id: str) -> Optional[Dict[str, Any]]: + """Terminates the specified node. + + Optionally return a mapping from deleted node ids to node + metadata. + """ + raise NotImplementedError + + def terminate_nodes(self, node_ids: List[str]) -> Optional[Dict[str, Any]]: + """Terminates a set of nodes. + + May be overridden with a batch method, which optionally may return a + mapping from deleted node ids to node metadata. + """ + for node_id in node_ids: + logger.info("NodeProvider: {}: Terminating node".format(node_id)) + self.terminate_node(node_id) + return None + + @property + def max_terminate_nodes(self) -> Optional[int]: + """The maximum number of nodes which can be terminated in one single + API request. By default, this is "None", which means that the node + provider's underlying API allows infinite requests to be terminated + with one request. + + For example, AWS only allows 1000 nodes to be terminated + at once; to terminate more, we must issue multiple separate API + requests. If the limit is infinity, then simply set this to None. + + This may be overridden. The value may be useful when overriding the + "terminate_nodes" method. + """ + return None + + @staticmethod + def bootstrap_config(cluster_config: Dict[str, Any]) -> Dict[str, Any]: + """Bootstraps the cluster config by adding env defaults if needed.""" + return cluster_config + + def get_command_runner( + self, + log_prefix: str, + node_id: str, + auth_config: Dict[str, Any], + cluster_name: str, + process_runner: ModuleType, + use_internal_ip: bool, + docker_config: Optional[Dict[str, Any]] = None, + ) -> CommandRunnerInterface: + """Returns the CommandRunner class used to perform SSH commands. + + Args: + log_prefix: stores "NodeUpdater: {}: ".format(). Used + to print progress in the CommandRunner. + node_id: the node ID. + auth_config: the authentication configs from the autoscaler + yaml file. + cluster_name: the name of the cluster. + process_runner: the module to use to run the commands + in the CommandRunner. E.g., subprocess. + use_internal_ip: whether the node_id belongs to an internal ip + or external ip. + docker_config: If set, the docker information of the docker + container that commands should be run on. + """ + common_args = { + "log_prefix": log_prefix, + "node_id": node_id, + "provider": self, + "auth_config": auth_config, + "cluster_name": cluster_name, + "process_runner": process_runner, + "use_internal_ip": use_internal_ip, + } + if docker_config and docker_config["container_name"] != "": + return DockerCommandRunner(docker_config, **common_args) + else: + return SSHCommandRunner(**common_args) + + def prepare_for_head_node(self, cluster_config: Dict[str, Any]) -> Dict[str, Any]: + """Returns a new cluster config with custom configs for head node.""" + return cluster_config + + @staticmethod + def fillout_available_node_types_resources( + cluster_config: Dict[str, Any] + ) -> Dict[str, Any]: + """Fills out missing "resources" field for available_node_types.""" + return cluster_config + + def safe_to_scale(self) -> bool: + """Optional condition to determine if it's safe to proceed with an autoscaling + update. Can be used to wait for convergence of state managed by an external + cluster manager. + + Called by the autoscaler immediately after non_terminated_nodes(). + If False is returned, the autoscaler will abort the update. + """ + return True + + def post_process(self) -> None: + """This optional method is executed at the end of + StandardAutoscaler._update(). + """ + pass diff --git a/lib/python3.12/site-packages/ray/autoscaler/ray-schema.json b/lib/python3.12/site-packages/ray/autoscaler/ray-schema.json new file mode 100644 index 0000000000000000000000000000000000000000..e65b239f233469308088399957b0d7e64e706aec --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/ray-schema.json @@ -0,0 +1,400 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://github.com/ray-project/ray/python/ray/autoscaler/ray-schema.json", + "title": "Ray AutoScaler", + "description": "Ray autoscaler schema", + "type": "object", + "definitions": { + "commands": { + "type": "array", + "items": { + "type": "string", + "description": "shell command" + } + } + }, + "required": [ + "cluster_name", + "provider" + ], + "additionalProperties": false, + "properties": { + "cluster_name": { + "description": "A unique identifier for the head node and workers of this cluster.", + "type": "string" + }, + "max_workers": { + "description": "The maximum number of workers nodes to launch in addition to the head node. This should be no larger than the sum of min_workers for all available node types.", + "type": "integer", + "minimum": 0 + }, + "upscaling_speed": { + "description": "The autoscaler will scale up the cluster faster with higher upscaling speed. E.g., if the task requires adding more nodes then autoscaler will gradually scale up the cluster in chunks of upscaling_speed*currently_running_nodes. This number should be > 0.", + "type": "number", + "minimum": 0 + }, + "idle_timeout_minutes": { + "description": "If a node is idle for this many minutes, it will be removed.", + "type": "number", + "minimum": 0 + }, + "provider": { + "type": "object", + "description": "Cloud-provider specific configuration.", + "required": [ "type" ], + "additionalProperties": true, + "properties": { + "type": { + "type": "string", + "description": "e.g. aws, azure, gcp,..." + }, + "region": { + "type": "string", + "description": "e.g. us-east-1" + }, + "module": { + "type": "string", + "description": "module, if using external node provider" + }, + "head_ip": { + "type": "string", + "description": "gcp project id, if using gcp" + }, + "worker_ips": { + "type": "array", + "description": "local cluster head node" + }, + "use_internal_ips": { + "type": "boolean", + "description": "don't require public ips" + }, + "namespace": { + "type": "string", + "description": "k8s namespace, if using k8s" + }, + "location": { + "type": "string", + "description": "Azure location" + }, + "resource_group": { + "type": "string", + "description": "Azure resource group" + }, + "tags": { + "type": "object", + "description": "Azure user-defined tags" + }, + "subscription_id": { + "type": "string", + "description": "Azure subscription id" + }, + "msi_identity_id": { + "type": "string", + "description": "User-defined managed identity (generated by config)" + }, + "msi_identity_principal_id": { + "type": "string", + "description": "User-defined managed identity principal id (generated by config)" + }, + "subnet_id": { + "type": "string", + "description": "Network subnet id" + }, + "autoscaler_service_account": { + "type": "object", + "description": "k8s autoscaler permissions, if using k8s" + }, + "autoscaler_role": { + "type": "object", + "description": "k8s autoscaler permissions, if using k8s" + }, + "autoscaler_role_binding": { + "type": "object", + "description": "k8s autoscaler permissions, if using k8s" + }, + "cache_stopped_nodes": { + "type": "boolean", + "description": " Whether to try to reuse previously stopped nodes instead of launching nodes. This will also cause the autoscaler to stop nodes instead of terminating them. Only implemented for AWS." + }, + "availability_zone": { + "type": "string", + "description": "GCP availability zone" + }, + "project_id": { + "type": ["string", "null"], + "description": "GCP globally unique project id" + }, + "security_group": { + "type": "object", + "description": "AWS security group", + "additionalProperties": false, + "properties": { + "GroupName": { + "type": "string", + "description": "Security group name" + }, + "IpPermissions": { + "type": "array", + "description": "Security group in bound rules" + } + } + }, + "disable_node_updaters": { + "type": "boolean", + "description": "Disables node updaters if set to True. Default is False. (For Kubernetes operator usage.)" + }, + "gcp_credentials": { + "type": "object", + "description": "Credentials for authenticating with the GCP client", + "required": [ "type" ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": ["credentials_token", "service_account"], + "description": "Credentials type: either temporary OAuth 2.0 token or permanent service account credentials blob." + }, + "credentials": { + "type": "string", + "description": "Oauth token or JSON string constituting service account credentials" + } + } + }, + "cloudwatch": { + "agent": { + "CLOUDWATCH_AGENT_INSTALLED_AMI_TAG": { + "type": ["string"], + "description": "Tag to be added to cloudwatch agent pre-installed AMI name." + }, + "config": { + "type": ["string", "null"], + "description": "Path to Unified CloudWatch Agent config file. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html for additional details." + }, + "retryer": { + "max_attempts": { + "type": ["integer", "null"], + "description": "Max allowed Unified CloudWatch Agent installation attempts on any host." + }, + "delay_seconds": { + "type": ["integer", "null"], + "description": "Seconds to wait between each Unified CloudWatch Agent installation attempt." + } + } + }, + "dashboard": { + "name": { + "type": ["string", "null"], + "description": "User defined CloudWatch Dashboard name." + }, + "config": { + "type": ["string", "null"], + "description": "Path to CloudWatch Dashboard config file. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/CloudWatch-Dashboard-Body-Structure.html for additional details." + } + }, + "alarm": { + "config": { + "type": ["string", "null"], + "description": "Path to CloudWatch Alarm config file. See https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricAlarm.html for additional details." + } + } + } + } + }, + "auth": { + "type": "object", + "description": "How Ray will authenticate with newly launched nodes.", + "additionalProperties": false, + "properties": { + "ssh_user": { + "type": "string", + "default": "ubuntu" + }, + "ssh_public_key": { + "type": "string" + }, + "ssh_private_key": { + "type": "string" + }, + "ssh_proxy_command": { + "description": "A value for ProxyCommand ssh option, for connecting through proxies. Example: nc -x proxy.example.com:1234 %h %p", + "type": "string" + } + } + }, + "docker": { + "type": "object", + "description": "Docker configuration. If this is specified, all setup and start commands will be executed in the container.", + "additionalProperties": false, + "properties": { + "image": { + "type": "string", + "description": "the docker image name", + "default": "rayproject/ray:latest" + }, + "container_name": { + "type": "string", + "default": "ray_docker" + }, + "pull_before_run": { + "type": "boolean", + "description": "run `docker pull` first" + }, + "run_options": { + "type": "array", + "description": "shared options for starting head/worker docker" + }, + "head_image": { + "type": "string", + "description": "image for head node, takes precedence over 'image' if specified" + }, + "head_run_options": { + "type": "array", + "description": "head specific run options, appended to run_options" + }, + "worker_image": { + "type": "string", + "description": "analogous to head_image" + }, + "worker_run_options": { + "type": "array", + "description": "analogous to head_run_options" + }, + "disable_automatic_runtime_detection" : { + "type": "boolean", + "description": "disable Ray from automatically using the NVIDIA runtime if available", + "default": false + }, + "disable_shm_size_detection" : { + "type": "boolean", + "description": "disable Ray from automatically detecting /dev/shm size for the container", + "default": false + }, + "use_podman" : { + "type": "boolean", + "description": "Use 'podman' command in place of 'docker'", + "default": false + } + } + }, + "head_node_type": { + "type": "string", + "description": "If using multiple node types, specifies the head node type." + }, + "file_mounts": { + "type": "object", + "description": "Map of remote paths to local paths, e.g. {\"/tmp/data\": \"/my/local/data\"}" + }, + "cluster_synced_files": { + "type": "array", + "description": "List of paths on the head node which should sync to the worker nodes, e.g. [\"/some/data/somehwere\"]" + }, + "file_mounts_sync_continuously": { + "type": "boolean", + "description": "If enabled, file mounts will sync continously between the head node and the worker nodes. The nodes will not re-run setup commands if only the contents of the file mounts folders change." + }, + "rsync_exclude": { + "type": "array", + "description": "File pattern to not sync up or down when using the rsync command. Matches the format of rsync's --exclude param." + }, + "rsync_filter": { + "type": "array", + "description": "Pattern files to lookup patterns to exclude when using rsync up or rsync down. This file is checked for recursively in all directories. For example, if .gitignore is provided here, the behavior will match git's .gitignore behavior." + }, + "metadata": { + "type": "object", + "description": "Metadata field that can be used to store user-defined data in the cluster config. Ray does not interpret these fields." + }, + "initialization_commands": { + "$ref": "#/definitions/commands", + "description": "List of commands that will be run before `setup_commands`. If docker is enabled, these commands will run outside the container and before docker is setup." + }, + "setup_commands": { + "$ref": "#/definitions/commands", + "description": "List of common shell commands to run to setup nodes." + }, + "head_setup_commands": { + "$ref": "#/definitions/commands", + "description": "Commands that will be run on the head node after common setup." + }, + "worker_setup_commands": { + "$ref": "#/definitions/commands", + "description": "Commands that will be run on worker nodes after common setup." + }, + "head_start_ray_commands": { + "$ref": "#/definitions/commands", + "description": "Command to start ray on the head node. You shouldn't need to modify this." + }, + "worker_start_ray_commands": { + "$ref": "#/definitions/commands", + "description": "Command to start ray on worker nodes. You shouldn't need to modify this." + }, + "no_restart": { + "description": "Whether to avoid restarting the cluster during updates. This field is controlled by the ray --no-restart flag and cannot be set by the user." + }, + "available_node_types": { + "type": "object", + "description": "A list of node types for multi-node-type autoscaling.", + "patternProperties": { + ".*": { + "type": "object", + "required": [ "resources", "node_config" ], + "properties": { + "node_config": { + "type": "object", + "description": "Provider-specific config for the node, e.g. instance type." + }, + "min_workers": {"type": "integer"}, + "max_workers": {"type": "integer"}, + "idle_timeout_s": {"type": "number", "nullable": true}, + "resources": { + "type": "object", + "patternProperties": { + ".*":{ + "type": "integer", + "minimum": 0 + } + } + }, + "labels": { + "type": "object", + "patternProperties": { + ".*":{ + "type": "string" + } + } + }, + "initialization_commands": { + "$ref": "#/definitions/commands", + "description": "List of commands that will be run before `setup_commands`. If docker is enabled, these commands will run outside the container and before docker is setup." + }, + "worker_setup_commands": { + "$ref": "#/definitions/commands", + "description": "List of common shell commands to run to setup nodes. This node specfic list will override the global setup_commands and worker_setup_commands." + }, + "docker": { + "description": "Configuration of Worker nodes.", + "type": "object", + "properties": { + "pull_before_run": { + "type": "boolean", + "description": "run `docker pull` first" + }, + "worker_image": { + "type": "string", + "description": "analogous to head_image" + }, + "worker_run_options": { + "type": "array", + "description": "analogous to head_run_options, merged with the global docker run_options." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + } +} diff --git a/lib/python3.12/site-packages/ray/autoscaler/sdk/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/sdk/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7dcad853f6a118f03a4e1c4198f11f3be281c5f9 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/sdk/__init__.py @@ -0,0 +1,29 @@ +from ray.autoscaler.sdk.sdk import ( + bootstrap_config, + configure_logging, + create_or_update_cluster, + fillout_defaults, + get_docker_host_mount_location, + get_head_node_ip, + get_worker_node_ips, + register_callback_handler, + request_resources, + rsync, + run_on_cluster, + teardown_cluster, +) + +__all__ = [ + "create_or_update_cluster", + "teardown_cluster", + "run_on_cluster", + "rsync", + "get_head_node_ip", + "get_worker_node_ips", + "request_resources", + "configure_logging", + "bootstrap_config", + "fillout_defaults", + "register_callback_handler", + "get_docker_host_mount_location", +] diff --git a/lib/python3.12/site-packages/ray/autoscaler/sdk/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/sdk/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b748fc3628a6505d580622500e055e76d7d4bcb Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/sdk/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/sdk/__pycache__/sdk.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/sdk/__pycache__/sdk.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5581a182fc10606b64afacdb082934d0ef962ecb Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/sdk/__pycache__/sdk.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/sdk/sdk.py b/lib/python3.12/site-packages/ray/autoscaler/sdk/sdk.py new file mode 100644 index 0000000000000000000000000000000000000000..30bcfd652dafad8e22af543116b1a5bdf6867266 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/sdk/sdk.py @@ -0,0 +1,386 @@ +"""IMPORTANT: this is an experimental interface and not currently stable.""" + +import json +import os +import tempfile +from contextlib import contextmanager +from typing import Any, Callable, Dict, Iterator, List, Optional, Union + +from ray._private.label_utils import validate_label_selector +from ray.autoscaler._private import commands +from ray.autoscaler._private.cli_logger import cli_logger +from ray.autoscaler._private.event_system import ( + CreateClusterEvent, # noqa: F401 + global_event_system, # noqa: F401 +) +from ray.util.annotations import DeveloperAPI + + +@DeveloperAPI +def create_or_update_cluster( + cluster_config: Union[dict, str], + *, + no_restart: bool = False, + restart_only: bool = False, + no_config_cache: bool = False, +) -> Dict[str, Any]: + """Create or updates an autoscaling Ray cluster from a config json. + + Args: + cluster_config (Union[str, dict]): Either the config dict of the + cluster, or a path pointing to a file containing the config. + no_restart: Whether to skip restarting Ray services during the + update. This avoids interrupting running jobs and can be used to + dynamically adjust autoscaler configuration. + restart_only: Whether to skip running setup commands and only + restart Ray. This cannot be used with 'no-restart'. + no_config_cache: Whether to disable the config cache and fully + resolve all environment settings from the Cloud provider again. + """ + with _as_config_file(cluster_config) as config_file: + return commands.create_or_update_cluster( + config_file=config_file, + override_min_workers=None, + override_max_workers=None, + no_restart=no_restart, + restart_only=restart_only, + yes=True, + override_cluster_name=None, + no_config_cache=no_config_cache, + redirect_command_output=None, + use_login_shells=True, + ) + + +@DeveloperAPI +def teardown_cluster( + cluster_config: Union[dict, str], + workers_only: bool = False, + keep_min_workers: bool = False, +) -> None: + """Destroys all nodes of a Ray cluster described by a config json. + + Args: + cluster_config (Union[str, dict]): Either the config dict of the + cluster, or a path pointing to a file containing the config. + workers_only: Whether to keep the head node running and only + teardown worker nodes. + keep_min_workers: Whether to keep min_workers (as specified + in the YAML) still running. + """ + with _as_config_file(cluster_config) as config_file: + return commands.teardown_cluster( + config_file=config_file, + yes=True, + workers_only=workers_only, + override_cluster_name=None, + keep_min_workers=keep_min_workers, + ) + + +@DeveloperAPI +def run_on_cluster( + cluster_config: Union[dict, str], + *, + cmd: Optional[str] = None, + run_env: str = "auto", + tmux: bool = False, + stop: bool = False, + no_config_cache: bool = False, + port_forward: Optional[commands.Port_forward] = None, + with_output: bool = False, +) -> Optional[str]: + """Runs a command on the specified cluster. + + Args: + cluster_config (Union[str, dict]): Either the config dict of the + cluster, or a path pointing to a file containing the config. + cmd: the command to run, or None for a no-op command. + run_env: whether to run the command on the host or in a + container. Select between "auto", "host" and "docker". + tmux: whether to run in a tmux session + stop: whether to stop the cluster after command run + no_config_cache: Whether to disable the config cache and fully + resolve all environment settings from the Cloud provider again. + port_forward ( (int,int) or list[(int,int)]): port(s) to forward. + with_output: Whether to capture command output. + + Returns: + The output of the command as a string. + """ + with _as_config_file(cluster_config) as config_file: + return commands.exec_cluster( + config_file, + cmd=cmd, + run_env=run_env, + screen=False, + tmux=tmux, + stop=stop, + start=False, + override_cluster_name=None, + no_config_cache=no_config_cache, + port_forward=port_forward, + with_output=with_output, + ) + + +@DeveloperAPI +def rsync( + cluster_config: Union[dict, str], + *, + source: Optional[str], + target: Optional[str], + down: bool, + ip_address: Optional[str] = None, + use_internal_ip: bool = False, + no_config_cache: bool = False, + should_bootstrap: bool = True, +): + """Rsyncs files to or from the cluster. + + Args: + cluster_config (Union[str, dict]): Either the config dict of the + cluster, or a path pointing to a file containing the config. + source: rsync source argument. + target: rsync target argument. + down: whether we're syncing remote -> local. + ip_address: Address of node. + use_internal_ip: Whether the provided ip_address is + public or private. + no_config_cache: Whether to disable the config cache and fully + resolve all environment settings from the Cloud provider again. + should_bootstrap: whether to bootstrap cluster config before syncing + + Raises: + RuntimeError: If the cluster head node is not found. + """ + with _as_config_file(cluster_config) as config_file: + return commands.rsync( + config_file=config_file, + source=source, + target=target, + override_cluster_name=None, + down=down, + ip_address=ip_address, + use_internal_ip=use_internal_ip, + no_config_cache=no_config_cache, + all_nodes=False, + should_bootstrap=should_bootstrap, + ) + + +@DeveloperAPI +def get_head_node_ip(cluster_config: Union[dict, str]) -> str: + """Returns head node IP for given configuration file if exists. + + Args: + cluster_config (Union[str, dict]): Either the config dict of the + cluster, or a path pointing to a file containing the config. + + Returns: + The ip address of the cluster head node. + + Raises: + RuntimeError: If the cluster is not found. + """ + with _as_config_file(cluster_config) as config_file: + return commands.get_head_node_ip(config_file) + + +@DeveloperAPI +def get_worker_node_ips(cluster_config: Union[dict, str]) -> List[str]: + """Returns worker node IPs for given configuration file. + + Args: + cluster_config (Union[str, dict]): Either the config dict of the + cluster, or a path pointing to a file containing the config. + + Returns: + List of worker node ip addresses. + + Raises: + RuntimeError: If the cluster is not found. + """ + with _as_config_file(cluster_config) as config_file: + return commands.get_worker_node_ips(config_file) + + +@DeveloperAPI +def request_resources( + num_cpus: Optional[int] = None, + bundles: Optional[List[dict]] = None, + bundle_label_selectors: Optional[List[dict]] = None, +) -> None: + """Command the autoscaler to scale to accommodate the specified requests. + + The cluster will immediately attempt to scale to accommodate the requested + resources, bypassing normal upscaling speed constraints. This takes into + account existing resource usage. + + For example, suppose you call ``request_resources(num_cpus=100)`` and + there are 45 currently running tasks, each requiring 1 CPU. Then, enough + nodes will be added so up to 100 tasks can run concurrently. It does + **not** add enough nodes so that 145 tasks can run. + + This call is only a hint to the autoscaler. The actual resulting cluster + size may be slightly larger or smaller than expected depending on the + internal bin packing algorithm and max worker count restrictions. + + Args: + num_cpus: Scale the cluster to ensure this number of CPUs are + available. This request is persistent until another call to + request_resources() is made to override. + bundles (List[ResourceDict]): Scale the cluster to ensure this set of + resource shapes can fit. This request is persistent until another + call to request_resources() is made to override. + bundle_label_selectors: A list of label selectors, applied per-bundle to the same + index in the `bundles` list. For bundles without a label requirement, the + corresponding item in the list is an empty dictionary. For each bundle. + Label selectors consist of zero or more key-value pairs where the key is + a label and the value is a operator (in, !in, etc.) and label value. + + Examples: + >>> from ray.autoscaler.sdk import request_resources + >>> # Request 1000 CPUs. + >>> request_resources(num_cpus=1000) # doctest: +SKIP + >>> # Request 64 CPUs and also fit a 1-GPU/4-CPU task. + >>> request_resources( # doctest: +SKIP + ... num_cpus=64, bundles=[{"GPU": 1, "CPU": 4}]) + >>> # Same as requesting num_cpus=3. + >>> request_resources( # doctest: +SKIP + ... bundles=[{"CPU": 1}, {"CPU": 1}, {"CPU": 1}]) + >>> # Requests 2 num_cpus=1 bundles, the first with + >>> # label_selector={"accelerator-type": "in(A100)"} and second with + >>> # label_selector={"market-type": "spot"}. + >>> request_resources( # doctest: +SKIP + ... bundles=[{"CPU": 1}, {"CPU": 1}]), + ... bundle_label_selectors=[{"accelerator-type": "in(A100)"}, + ... {"market-type": "spot"}]) + """ + if num_cpus is not None and not isinstance(num_cpus, int): + raise TypeError("num_cpus should be of type int.") + if bundles is not None: + if isinstance(bundles, List): + for bundle in bundles: + if isinstance(bundle, Dict): + for key in bundle.keys(): + if not (isinstance(key, str) and isinstance(bundle[key], int)): + raise TypeError( + "each bundle key should be str and value as int." + ) + else: + raise TypeError("each bundle should be a Dict.") + else: + raise TypeError("bundles should be of type List") + if bundle_label_selectors is not None: + if bundles is None: + raise ValueError( + "`bundles` must be provided when `bundle_label_selectors` is specified." + ) + if len(bundle_label_selectors) != len(bundles): + raise ValueError( + "`bundle_label_selector` must be a list with length equal to the number of bundles." + ) + for label_selector in bundle_label_selectors: + if ( + not isinstance(label_selector, dict) + or not all(isinstance(k, str) for k in label_selector.keys()) + or not all(isinstance(v, str) for v in label_selector.values()) + ): + raise ValueError( + "Bundle label selector must be a list of string dictionary" + " label selectors. For example: " + '`[{ray.io/market_type": "spot"}, {"ray.io/accelerator-type": "A100"}]`.' + ) + error_message = validate_label_selector(label_selector) + if error_message: + raise ValueError( + f"Invalid label selector provided in bundle_label_selectors list." + f" Detailed error: '{error_message}'" + ) + + return commands.request_resources(num_cpus, bundles, bundle_label_selectors) + + +@DeveloperAPI +def configure_logging( + log_style: Optional[str] = None, + color_mode: Optional[str] = None, + verbosity: Optional[int] = None, +): + """Configures logging for cluster command calls. + + Args: + log_style: If 'pretty', outputs with formatting and color. + If 'record', outputs record-style without formatting. + 'auto' defaults to 'pretty', and disables pretty logging + if stdin is *not* a TTY. Defaults to "auto". + color_mode (str): + Can be "true", "false", or "auto". + + Enables or disables `colorful`. + + If `color_mode` is "auto", is set to `not stdout.isatty()` + vebosity (int): + Output verbosity (0, 1, 2, 3). + + Low verbosity will disable `verbose` and `very_verbose` messages. + + """ + cli_logger.configure( + log_style=log_style, color_mode=color_mode, verbosity=verbosity + ) + + +@contextmanager +@DeveloperAPI +def _as_config_file(cluster_config: Union[dict, str]) -> Iterator[str]: + if isinstance(cluster_config, dict): + tmp = tempfile.NamedTemporaryFile("w", prefix="autoscaler-sdk-tmp-") + tmp.write(json.dumps(cluster_config)) + tmp.flush() + cluster_config = tmp.name + if not os.path.exists(cluster_config): + raise ValueError("Cluster config not found {}".format(cluster_config)) + yield cluster_config + + +@DeveloperAPI +def bootstrap_config( + cluster_config: Dict[str, Any], no_config_cache: bool = False +) -> Dict[str, Any]: + """Validate and add provider-specific fields to the config. For example, + IAM/authentication may be added here.""" + return commands._bootstrap_config(cluster_config, no_config_cache) + + +@DeveloperAPI +def fillout_defaults(config: Dict[str, Any]) -> Dict[str, Any]: + """Fillout default values for a cluster_config based on the provider.""" + from ray.autoscaler._private.util import fillout_defaults + + return fillout_defaults(config) + + +@DeveloperAPI +def register_callback_handler( + event_name: str, + callback: Union[Callable[[Dict], None], List[Callable[[Dict], None]]], +) -> None: + """Registers a callback handler for autoscaler events. + + Args: + event_name: Event that callback should be called on. See + CreateClusterEvent for details on the events available to be + registered against. + callback: Callable object that is invoked + when specified event occurs. + """ + global_event_system.add_callback_handler(event_name, callback) + + +@DeveloperAPI +def get_docker_host_mount_location(cluster_name: str) -> str: + """Return host path that Docker mounts attach to.""" + docker_mount_prefix = "/tmp/ray_tmp_mount/{cluster_name}" + return docker_mount_prefix.format(cluster_name=cluster_name) diff --git a/lib/python3.12/site-packages/ray/autoscaler/spark/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/spark/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/spark/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/spark/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19d18bdfee194c327504a321118e9a6428ab55a5 Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/spark/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/spark/defaults.yaml b/lib/python3.12/site-packages/ray/autoscaler/spark/defaults.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bc69977cfc6f5678caf5a2c77df3072df088c21c --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/spark/defaults.yaml @@ -0,0 +1,49 @@ +# Example command to start a cluster with this config: +# +# ray start --autoscaling-config=default.yaml --head --block +# +cluster_name: spark +max_workers: 8 +provider: + type: spark + # This must be true since the nodes share the same ip! + use_node_id_as_ip: True + disable_node_updaters: True + disable_launch_config_check: True +available_node_types: + ray.head.default: + # You must set this manually to your "head" node resources!! The head + # node is launched via `ray start` and hence the autoscaler cannot + # configure its resources. The resources specified for its node type + # must line up with what Ray detects/is configured with on start. + resources: + CPU: 8 # <-- set this to num CPUs used/detected in `ray start` + GPU: 0 # <-- set this to num GPUs used/detected in `ray start` + node_config: {} + max_workers: 0 + ray.worker: + resources: + CPU: 1 + object_store_memory: 1000000000 + node_config: {} + min_workers: 0 + max_workers: 4 +head_node_type: ray.head.default +upscaling_speed: 1.0 +idle_timeout_minutes: 1.0 +# +# !!! Configurations below are not supported in spark cluster mode +# +auth: {} +docker: {} +initialization_commands: [] +setup_commands: [] +head_setup_commands: [] +worker_setup_commands: [] +head_start_ray_commands: [] +worker_start_ray_commands: [] +file_mounts: {} +cluster_synced_files: [] +file_mounts_sync_continuously: false +rsync_exclude: [] +rsync_filter: [] diff --git a/lib/python3.12/site-packages/ray/autoscaler/tags.py b/lib/python3.12/site-packages/ray/autoscaler/tags.py new file mode 100644 index 0000000000000000000000000000000000000000..38d03855040fd512e32b2d31f9972e1da8dcedbb --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/tags.py @@ -0,0 +1,47 @@ +"""The Ray autoscaler uses tags/labels to associate metadata with instances.""" + +# Tag for the name of the node +TAG_RAY_NODE_NAME = "ray-node-name" + +# Tag for the kind of node (e.g. Head, Worker). For legacy reasons, the tag +# value says 'type' instead of 'kind'. +TAG_RAY_NODE_KIND = "ray-node-type" +NODE_KIND_HEAD = "head" +NODE_KIND_WORKER = "worker" +NODE_KIND_UNMANAGED = "unmanaged" + +# Tag for user defined node types (e.g., m4xl_spot). This is used for multi +# node type clusters. +TAG_RAY_USER_NODE_TYPE = "ray-user-node-type" +# Tag for index of replica node belongs to. Used for multi-host worker groups. +TAG_RAY_REPLICA_INDEX = "ray-replica-index" +# Tag for autofilled node types for legacy cluster yamls without multi +# node type defined in the cluster configs. +NODE_TYPE_LEGACY_HEAD = "ray-legacy-head-node-type" +NODE_TYPE_LEGACY_WORKER = "ray-legacy-worker-node-type" + +# Tag that reports the current state of the node (e.g. Updating, Up-to-date) +TAG_RAY_NODE_STATUS = "ray-node-status" +STATUS_UNINITIALIZED = "uninitialized" +STATUS_WAITING_FOR_SSH = "waiting-for-ssh" +STATUS_SYNCING_FILES = "syncing-files" +STATUS_SETTING_UP = "setting-up" +STATUS_UPDATE_FAILED = "update-failed" +STATUS_UP_TO_DATE = "up-to-date" + +# Tag uniquely identifying all nodes of a cluster +TAG_RAY_CLUSTER_NAME = "ray-cluster-name" + +# Hash of the node launch config, used to identify out-of-date nodes +TAG_RAY_LAUNCH_CONFIG = "ray-launch-config" + +# Hash of the node runtime config, used to determine if updates are needed +TAG_RAY_RUNTIME_CONFIG = "ray-runtime-config" +# Hash of the contents of the directories specified by the file_mounts config +# if the node is a worker, this also hashes content of the directories +# specified by the cluster_synced_files config +TAG_RAY_FILE_MOUNTS_CONTENTS = "ray-file-mounts-contents" + +# Tag for the launch request id, used to identify nodes launched by the same +# launch request. +TAG_RAY_LAUNCH_REQUEST = "ray-launch-request" diff --git a/lib/python3.12/site-packages/ray/autoscaler/vsphere/__init__.py b/lib/python3.12/site-packages/ray/autoscaler/vsphere/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/autoscaler/vsphere/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/autoscaler/vsphere/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d82e93fa159a0a8e0a5e6065faaeec2e3ce2ed7b Binary files /dev/null and b/lib/python3.12/site-packages/ray/autoscaler/vsphere/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/autoscaler/vsphere/defaults.yaml b/lib/python3.12/site-packages/ray/autoscaler/vsphere/defaults.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1270a111586645380cea6da1afbccec18db0a0f7 --- /dev/null +++ b/lib/python3.12/site-packages/ray/autoscaler/vsphere/defaults.yaml @@ -0,0 +1,123 @@ +# An unique identifier for the head node and workers of this cluster. +cluster_name: ray-cluster + +# The maximum number of workers nodes to launch in addition to the head +# node. +max_workers: 5 + +# The autoscaler will scale up the cluster faster with higher upscaling speed. +# E.g., if the task requires adding more nodes then autoscaler will gradually +# scale up the cluster in chunks of upscaling_speed*currently_running_nodes. +# This number should be > 0. +upscaling_speed: 1.0 + +# This executes all commands on all nodes in the docker container, +# and opens all the necessary ports to support the Ray cluster. +# Empty string means disabled. +docker: + image: "rayproject/ray-ml:latest" + # image: rayproject/ray:latest # use this one if you don't need ML dependencies, it's faster to pull + container_name: "ray_container" + # If true, pulls latest version of image. Otherwise, `docker run` will only pull the image + # if no cached version is present. + pull_before_run: True + run_options: # Extra options to pass into "docker run" + - --ulimit nofile=65536:65536 + +# If a node is idle for this many minutes, it will be removed. +idle_timeout_minutes: 5 + +# Cloud-provider specific configuration. +provider: + type: vsphere + +# How Ray will authenticate with newly launched nodes. +auth: + ssh_user: ray +# By default Ray creates a new private keypair, but you can also use your own. +# If you do so, make sure to also set "KeyName" in the head and worker node +# configurations below. + ssh_private_key: ~/ray-bootstrap-key.pem + +# Tell the autoscaler the allowed node types and the resources they provide. +# The key is the name of the node type, which is just for debugging purposes. +# The node config specifies the launch config and physical instance type. +available_node_types: + ray.head.default: + # You can override the resources here. Adding GPU to the head node is not recommended. + # resources: { "CPU": 2, "Memory": 4096} + resources: {} + worker: + # The minimum number of nodes of this type to launch. + # This number should be >= 0. + min_workers: 1 + max_workers: 3 + # You can override the resources here. For GPU, currently only NVIDIA GPU is supported. If no ESXi host can + # fulfill the requirement, the Ray node creation will fail. The number of created nodes may not meet the desired + # minimum number. The vSphere node provider will not distinguish the GPU type. It will just count the quantity: + # mount the first k random available NVIDIA GPU to the VM, if the user set {"GPU": k}. + # resources: {"CPU": 2, "Memory": 4096, "GPU": 1} + resources: {} + worker_2: + # The minimum number of nodes of this type to launch. + # This number should be >= 0. + min_workers: 1 + max_workers: 2 + # You can override the resources here. For GPU, currently only NVIDIA GPU is supported. If no ESXi host can + # fulfill the requirement, the Ray node creation will fail. The number of created nodes may not meet the desired + # minimum number. The vSphere node provider will not distinguish the GPU type. It will just count the quantity: + # mount the first k random available NVIDIA GPU to the VM, if the user set {"GPU": k}. + # resources: {"CPU": 2, "Memory": 4096, "GPU": 1} + resources: {} + +# Specify the node type of the head node (as configured above). +head_node_type: ray.head.default + +# Files or directories to copy to the head and worker nodes. The format is a +# dictionary from REMOTE_PATH: LOCAL_PATH, e.g. +file_mounts: { +# "/path1/on/remote/machine": "/path1/on/local/machine", +# "/path2/on/remote/machine": "/path2/on/local/machine", +} + +# Files or directories to copy from the head node to the worker nodes. The format is a +# list of paths. The same path on the head node will be copied to the worker node. +# This behavior is a subset of the file_mounts behavior. In the vast majority of cases +# you should just use file_mounts. Only use this if you know what you're doing! +cluster_synced_files: [] + +# Whether changes to directories in file_mounts or cluster_synced_files in the head node +# should sync to the worker node continuously +file_mounts_sync_continuously: False + +# Patterns for files to exclude when running rsync up or rsync down +rsync_exclude: [] + +# Pattern files to use for filtering out files when running rsync up or rsync down. The file is searched for +# in the source directory and recursively through all subdirectories. For example, if .gitignore is provided +# as a value, the behavior will match git's behavior for finding and using .gitignore files. +rsync_filter: [] + +# List of commands that will be run before `setup_commands`. If docker is +# enabled, these commands will run outside the container and before docker +# is setup. +initialization_commands: [] + +# List of shell commands to run to set up nodes. +setup_commands: [] + +# Custom commands that will be run on the head node after common setup. +head_setup_commands: [] + +# Custom commands that will be run on worker nodes after common setup. +worker_setup_commands: [] + +# Command to start ray on the head node. You don't need to change this. +head_start_ray_commands: + - ray stop + - ulimit -n 65536; ray start --head --port=6379 --autoscaling-config=~/ray_bootstrap_config.yaml --dashboard-host=0.0.0.0 + +# Command to start ray on worker nodes. You don't need to change this. +worker_start_ray_commands: + - ray stop + - ulimit -n 65536; ray start --address=$RAY_HEAD_IP:6379 diff --git a/lib/python3.12/site-packages/ray/dashboard/__init__.py b/lib/python3.12/site-packages/ray/dashboard/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/dashboard/agent.py b/lib/python3.12/site-packages/ray/dashboard/agent.py new file mode 100644 index 0000000000000000000000000000000000000000..355ac03d9aa5a9b96b686d8b1247588cf917035d --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/agent.py @@ -0,0 +1,471 @@ +import argparse +import asyncio +import json +import logging +import os +import signal +import sys + +import ray._private.ray_constants as ray_constants +import ray.dashboard.consts as dashboard_consts +import ray.dashboard.utils as dashboard_utils +from ray._common.network_utils import build_address, is_localhost +from ray._common.utils import get_or_create_event_loop +from ray._private import logging_utils +from ray._private.process_watcher import create_check_raylet_task +from ray._private.ray_constants import AGENT_GRPC_MAX_MESSAGE_LENGTH +from ray._private.ray_logging import setup_component_logger +from ray._raylet import GcsClient + +logger = logging.getLogger(__name__) + + +class DashboardAgent: + def __init__( + self, + node_ip_address, + grpc_port, + gcs_address, + cluster_id_hex, + minimal, + metrics_export_port=None, + node_manager_port=None, + events_export_addr=None, + listen_port=ray_constants.DEFAULT_DASHBOARD_AGENT_LISTEN_PORT, + disable_metrics_collection: bool = False, + *, # the following are required kwargs + object_store_name: str, + raylet_name: str, + log_dir: str, + temp_dir: str, + session_dir: str, + session_name: str, + ): + """Initialize the DashboardAgent object.""" + # Public attributes are accessible for all agent modules. + assert node_ip_address is not None + self.ip = node_ip_address + self.minimal = minimal + + assert gcs_address is not None + self.gcs_address = gcs_address + self.cluster_id_hex = cluster_id_hex + + self.temp_dir = temp_dir + self.session_dir = session_dir + self.log_dir = log_dir + self.grpc_port = grpc_port + self.metrics_export_port = metrics_export_port + self.node_manager_port = node_manager_port + self.events_export_addr = events_export_addr + self.listen_port = listen_port + self.object_store_name = object_store_name + self.raylet_name = raylet_name + self.node_id = os.environ["RAY_NODE_ID"] + self.metrics_collection_disabled = disable_metrics_collection + self.session_name = session_name + + # grpc server is None in mininal. + self.server = None + # http_server is None in minimal. + self.http_server = None + + # Used by the agent and sub-modules. + self.gcs_client = GcsClient( + address=self.gcs_address, + cluster_id=self.cluster_id_hex, + ) + + if not self.minimal: + self._init_non_minimal() + + def _init_non_minimal(self): + from grpc import aio as aiogrpc + + from ray._private.authentication.authentication_utils import ( + is_token_auth_enabled, + ) + from ray._private.authentication.grpc_authentication_server_interceptor import ( + AsyncAuthenticationServerInterceptor, + ) + from ray._private.tls_utils import add_port_to_grpc_server + from ray.dashboard.http_server_agent import HttpServerAgent + + # We would want to suppress deprecating warnings from aiogrpc library + # with the usage of asyncio.get_event_loop() in python version >=3.10 + # This could be removed once https://github.com/grpc/grpc/issues/32526 + # is released, and we used higher versions of grpcio that that. + if sys.version_info.major >= 3 and sys.version_info.minor >= 10: + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + aiogrpc.init_grpc_aio() + else: + aiogrpc.init_grpc_aio() + + # Add authentication interceptor if token auth is enabled + interceptors = [] + if is_token_auth_enabled(): + interceptors.append(AsyncAuthenticationServerInterceptor()) + + self.server = aiogrpc.server( + interceptors=interceptors, + options=( + ("grpc.so_reuseport", 0), + ( + "grpc.max_send_message_length", + AGENT_GRPC_MAX_MESSAGE_LENGTH, + ), # noqa + ( + "grpc.max_receive_message_length", + AGENT_GRPC_MAX_MESSAGE_LENGTH, + ), + ), # noqa + ) + try: + add_port_to_grpc_server(self.server, build_address(self.ip, self.grpc_port)) + if not is_localhost(self.ip): + add_port_to_grpc_server(self.server, f"127.0.0.1:{self.grpc_port}") + except Exception: + # TODO(SongGuyang): Catch the exception here because there is + # port conflict issue which brought from static port. We should + # remove this after we find better port resolution. + logger.exception( + "Failed to add port to grpc server. Agent will stay alive but " + "disable the grpc service." + ) + self.server = None + self.grpc_port = None + else: + logger.info( + "Dashboard agent grpc address: %s", + build_address(self.ip, self.grpc_port), + ) + + # If the agent is not minimal it should start the http server + # to communicate with the dashboard in a head node. + # Http server is not started in the minimal version because + # it requires additional dependencies that are not + # included in the minimal ray package. + self.http_server = HttpServerAgent(self.ip, self.listen_port) + + def _load_modules(self): + """Load dashboard agent modules.""" + modules = [] + agent_cls_list = dashboard_utils.get_all_modules( + dashboard_utils.DashboardAgentModule + ) + for cls in agent_cls_list: + logger.info( + "Loading %s: %s", dashboard_utils.DashboardAgentModule.__name__, cls + ) + c = cls(self) + modules.append(c) + logger.info("Loaded %d modules.", len(modules)) + return modules + + @property + def http_session(self): + assert ( + self.http_server + ), "Accessing unsupported API (HttpServerAgent) in a minimal ray." + return self.http_server.http_session + + def get_node_id(self) -> str: + return self.node_id + + async def run(self): + # Start a grpc asyncio server. + if self.server: + await self.server.start() + + modules = self._load_modules() + + launch_http_server = True + if self.http_server: + try: + await self.http_server.start(modules) + except Exception as e: + # TODO(kevin85421): We should fail the agent if the HTTP server + # fails to start to avoid hiding the root cause. However, + # agent processes are not cleaned up correctly after some tests + # finish. If we fail the agent, the CI will always fail until + # we fix the leak. + logger.exception( + f"Failed to start HTTP server with exception: {e}. " + "The agent will stay alive but the HTTP service will be disabled.", + ) + launch_http_server = False + + if launch_http_server: + # Writes agent address to kv. + # DASHBOARD_AGENT_ADDR_NODE_ID_PREFIX: -> (ip, http_port, grpc_port) + # DASHBOARD_AGENT_ADDR_IP_PREFIX: -> (node_id, http_port, grpc_port) + # -1 should indicate that http server is not started. + http_port = -1 if not self.http_server else self.http_server.http_port + grpc_port = -1 if not self.server else self.grpc_port + put_by_node_id = self.gcs_client.async_internal_kv_put( + f"{dashboard_consts.DASHBOARD_AGENT_ADDR_NODE_ID_PREFIX}{self.node_id}".encode(), + json.dumps([self.ip, http_port, grpc_port]).encode(), + True, + namespace=ray_constants.KV_NAMESPACE_DASHBOARD, + ) + put_by_ip = self.gcs_client.async_internal_kv_put( + f"{dashboard_consts.DASHBOARD_AGENT_ADDR_IP_PREFIX}{self.ip}".encode(), + json.dumps([self.node_id, http_port, grpc_port]).encode(), + True, + namespace=ray_constants.KV_NAMESPACE_DASHBOARD, + ) + + await asyncio.gather(put_by_node_id, put_by_ip) + + tasks = [m.run(self.server) for m in modules] + + if sys.platform not in ["win32", "cygwin"]: + + def callback(msg): + logger.info( + f"Terminated Raylet: ip={self.ip}, node_id={self.node_id}. {msg}" + ) + + check_parent_task = create_check_raylet_task( + self.log_dir, self.gcs_client, callback, loop + ) + tasks.append(check_parent_task) + + if self.server: + tasks.append(self.server.wait_for_termination()) + else: + + async def wait_forever(): + while True: + await asyncio.sleep(3600) + + tasks.append(wait_forever()) + + await asyncio.gather(*tasks) + + if self.http_server: + await self.http_server.cleanup() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Dashboard agent.") + parser.add_argument( + "--node-ip-address", + required=True, + type=str, + help="the IP address of this node.", + ) + parser.add_argument( + "--gcs-address", required=True, type=str, help="The address (ip:port) of GCS." + ) + parser.add_argument( + "--cluster-id-hex", + required=True, + type=str, + help="The cluster id in hex.", + ) + parser.add_argument( + "--metrics-export-port", + required=True, + type=int, + help="The port to expose metrics through Prometheus.", + ) + parser.add_argument( + "--grpc-port", + required=True, + type=int, + help="The port on which the dashboard agent will receive GRPCs.", + ) + parser.add_argument( + "--node-manager-port", + required=True, + type=int, + help="The port to use for starting the node manager", + ) + parser.add_argument( + "--object-store-name", + required=True, + type=str, + default=None, + help="The socket name of the plasma store", + ) + parser.add_argument( + "--listen-port", + required=False, + type=int, + default=ray_constants.DEFAULT_DASHBOARD_AGENT_LISTEN_PORT, + help="Port for HTTP server to listen on", + ) + parser.add_argument( + "--raylet-name", + required=True, + type=str, + default=None, + help="The socket path of the raylet process", + ) + parser.add_argument( + "--logging-level", + required=False, + type=lambda s: logging.getLevelName(s.upper()), + default=ray_constants.LOGGER_LEVEL, + choices=ray_constants.LOGGER_LEVEL_CHOICES, + help=ray_constants.LOGGER_LEVEL_HELP, + ) + parser.add_argument( + "--logging-format", + required=False, + type=str, + default=ray_constants.LOGGER_FORMAT, + help=ray_constants.LOGGER_FORMAT_HELP, + ) + parser.add_argument( + "--logging-filename", + required=False, + type=str, + default=dashboard_consts.DASHBOARD_AGENT_LOG_FILENAME, + help="Specify the name of log file, " + 'log to stdout if set empty, default is "{}".'.format( + dashboard_consts.DASHBOARD_AGENT_LOG_FILENAME + ), + ) + parser.add_argument( + "--logging-rotate-bytes", + required=True, + type=int, + help="Specify the max bytes for rotating log file.", + ) + parser.add_argument( + "--logging-rotate-backup-count", + required=True, + type=int, + help="Specify the backup count of rotated log file.", + ) + parser.add_argument( + "--log-dir", + required=True, + type=str, + default=None, + help="Specify the path of log directory.", + ) + parser.add_argument( + "--temp-dir", + required=True, + type=str, + default=None, + help="Specify the path of the temporary directory use by Ray process.", + ) + parser.add_argument( + "--session-dir", + required=True, + type=str, + default=None, + help="Specify the path of this session.", + ) + + parser.add_argument( + "--minimal", + action="store_true", + help=( + "Minimal agent only contains a subset of features that don't " + "require additional dependencies installed when ray is installed " + "by `pip install 'ray[default]'`." + ), + ) + parser.add_argument( + "--disable-metrics-collection", + action="store_true", + help=("If this arg is set, metrics report won't be enabled from the agent."), + ) + parser.add_argument( + "--session-name", + required=False, + type=str, + default=None, + help="The current Ray session name.", + ) + parser.add_argument( + "--stdout-filepath", + required=False, + type=str, + default="", + help="The filepath to dump dashboard agent stdout.", + ) + parser.add_argument( + "--stderr-filepath", + required=False, + type=str, + default="", + help="The filepath to dump dashboard agent stderr.", + ) + + args = parser.parse_args() + + try: + # Disable log rotation for windows platform. + logging_rotation_bytes = ( + args.logging_rotate_bytes if sys.platform != "win32" else 0 + ) + logging_rotation_backup_count = ( + args.logging_rotate_backup_count if sys.platform != "win32" else 1 + ) + + logger = setup_component_logger( + logging_level=args.logging_level, + logging_format=args.logging_format, + log_dir=args.log_dir, + filename=args.logging_filename, + max_bytes=logging_rotation_bytes, + backup_count=logging_rotation_backup_count, + ) + + # Setup stdout/stderr redirect files if redirection enabled. + logging_utils.redirect_stdout_stderr_if_needed( + args.stdout_filepath, + args.stderr_filepath, + logging_rotation_bytes, + logging_rotation_backup_count, + ) + + # Initialize event loop, see Dashboard init code for caveat + # w.r.t grpc server init in the DashboardAgent initializer. + loop = get_or_create_event_loop() + + agent = DashboardAgent( + args.node_ip_address, + args.grpc_port, + args.gcs_address, + args.cluster_id_hex, + args.minimal, + temp_dir=args.temp_dir, + session_dir=args.session_dir, + log_dir=args.log_dir, + metrics_export_port=args.metrics_export_port, + node_manager_port=args.node_manager_port, + listen_port=args.listen_port, + object_store_name=args.object_store_name, + raylet_name=args.raylet_name, + disable_metrics_collection=args.disable_metrics_collection, + session_name=args.session_name, + ) + + def sigterm_handler(): + logger.warning("Exiting with SIGTERM immediately...") + # Exit code 0 will be considered as an expected shutdown + os._exit(signal.SIGTERM) + + if sys.platform != "win32": + # TODO(rickyyx): we currently do not have any logic for actual + # graceful termination in the agent. Most of the underlying + # async tasks run by the agent head doesn't handle CancelledError. + # So a truly graceful shutdown is not trivial w/o much refactoring. + # Re-open the issue: https://github.com/ray-project/ray/issues/25518 + # if a truly graceful shutdown is required. + loop.add_signal_handler(signal.SIGTERM, sigterm_handler) + + loop.run_until_complete(agent.run()) + except Exception: + logger.exception("Agent is working abnormally. It will exit immediately.") + exit(1) diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/asset-manifest.json b/lib/python3.12/site-packages/ray/dashboard/client/build/asset-manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..18da12dcf29cfd65c3d8cb1aa8c36194c83d8c7b --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/asset-manifest.json @@ -0,0 +1,42 @@ +{ + "files": { + "main.css": "./static/css/main.388a904b.css", + "main.js": "./static/js/main.32399d4c.js", + "static/js/495.01ff0983.chunk.js": "./static/js/495.01ff0983.chunk.js", + "static/js/591.36c340d9.chunk.js": "./static/js/591.36c340d9.chunk.js", + "static/media/roboto-latin-900italic.woff": "./static/media/roboto-latin-900italic.c20d916c1a1b094c1cec.woff", + "static/media/roboto-latin-300italic.woff": "./static/media/roboto-latin-300italic.bd5b7a13f2c52b531a2a.woff", + "static/media/roboto-latin-500italic.woff": "./static/media/roboto-latin-500italic.18d00f739ff1e1c52db1.woff", + "static/media/roboto-latin-400italic.woff": "./static/media/roboto-latin-400italic.b1d9d9904bfca8802a63.woff", + "static/media/roboto-latin-100italic.woff": "./static/media/roboto-latin-100italic.451d4e559d6f57cdf6a1.woff", + "static/media/roboto-latin-700italic.woff": "./static/media/roboto-latin-700italic.9360531f9bb817f917f0.woff", + "static/media/roboto-latin-500.woff": "./static/media/roboto-latin-500.cea99d3e3e13a3a599a0.woff", + "static/media/roboto-latin-900.woff": "./static/media/roboto-latin-900.bac8362e7a6ea60b6983.woff", + "static/media/roboto-latin-100.woff": "./static/media/roboto-latin-100.a45108d3b34af91f9113.woff", + "static/media/roboto-latin-700.woff": "./static/media/roboto-latin-700.2267169ee7270a22a963.woff", + "static/media/roboto-latin-300.woff": "./static/media/roboto-latin-300.865f928cbabcc9f8f2b5.woff", + "static/media/roboto-latin-400.woff": "./static/media/roboto-latin-400.49ae34d4cc6b98c00c69.woff", + "static/media/roboto-latin-900italic.woff2": "./static/media/roboto-latin-900italic.cb5ad999740e9d8a8bd1.woff2", + "static/media/roboto-latin-300italic.woff2": "./static/media/roboto-latin-300italic.c64e7e354c88e613c77c.woff2", + "static/media/roboto-latin-400italic.woff2": "./static/media/roboto-latin-400italic.d022bc70dc1bf7b3425d.woff2", + "static/media/roboto-latin-500italic.woff2": "./static/media/roboto-latin-500italic.0d8bb5b3ee5f5dac9e44.woff2", + "static/media/roboto-latin-700italic.woff2": "./static/media/roboto-latin-700italic.7d8125ff7f707231fd89.woff2", + "static/media/roboto-latin-100italic.woff2": "./static/media/roboto-latin-100italic.7f839a8652da29745ce4.woff2", + "static/media/roboto-latin-500.woff2": "./static/media/roboto-latin-500.f5b74d7ffcdf85b9dd60.woff2", + "static/media/roboto-latin-700.woff2": "./static/media/roboto-latin-700.c18ee39fb002ad58b6dc.woff2", + "static/media/roboto-latin-100.woff2": "./static/media/roboto-latin-100.c2aa4ab115bf9c6057cb.woff2", + "static/media/roboto-latin-300.woff2": "./static/media/roboto-latin-300.37a7069dc30fc663c878.woff2", + "static/media/roboto-latin-400.woff2": "./static/media/roboto-latin-400.176f8f5bd5f02b3abfcf.woff2", + "static/media/roboto-latin-900.woff2": "./static/media/roboto-latin-900.870c8c1486f76054301a.woff2", + "static/media/logo.svg": "./static/media/logo.3704c1bbca650bb72a64b5d4c3fa5ced.svg", + "index.html": "./index.html", + "main.388a904b.css.map": "./static/css/main.388a904b.css.map", + "main.32399d4c.js.map": "./static/js/main.32399d4c.js.map", + "495.01ff0983.chunk.js.map": "./static/js/495.01ff0983.chunk.js.map", + "591.36c340d9.chunk.js.map": "./static/js/591.36c340d9.chunk.js.map" + }, + "entrypoints": [ + "static/css/main.388a904b.css", + "static/js/main.32399d4c.js" + ] +} \ No newline at end of file diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/favicon.ico b/lib/python3.12/site-packages/ray/dashboard/client/build/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9417b6ada1d3a2399d631c76f2fb49981adf27cf Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/favicon.ico differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/index.html b/lib/python3.12/site-packages/ray/dashboard/client/build/index.html new file mode 100644 index 0000000000000000000000000000000000000000..bfafecc8d7d2a34158c9f191c588d724a93c961e --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/index.html @@ -0,0 +1 @@ +Ray Dashboard
\ No newline at end of file diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/LICENSE b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..baf3e9d982fc948620518a9e57e8a718fcaea4ec --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Jamie Wong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/README b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/README new file mode 100644 index 0000000000000000000000000000000000000000..4d897271d380a3fd9964a4aad334ffb4e76c5429 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/README @@ -0,0 +1,2 @@ +This is a self-contained release of https://github.com/jlfwong/speedscope. +To use it, open index.html in Chrome or Firefox. diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/demangle-cpp.8a387750.js b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/demangle-cpp.8a387750.js new file mode 100644 index 0000000000000000000000000000000000000000..73686d88c66534c4567a288953879bae5f36d220 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/demangle-cpp.8a387750.js @@ -0,0 +1,4 @@ +parcelRequire=function(e,r,n,t){var i="function"==typeof parcelRequire&&parcelRequire,o="function"==typeof require&&require;function u(n,t){if(!r[n]){if(!e[n]){var f="function"==typeof parcelRequire&&parcelRequire;if(!t&&f)return f(n,!0);if(i)return i(n,!0);if(o&&"string"==typeof n)return o(n);var c=new Error("Cannot find module '"+n+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[n][1][r]||r};var l=r[n]=new u.Module(n);e[n][0].call(l.exports,p,l,l.exports,this)}return r[n].exports;function p(e){return u(p.resolve(e))}}u.isParcelRequire=!0,u.Module=function(e){this.id=e,this.bundle=u,this.exports={}},u.modules=e,u.cache=r,u.parent=i,u.register=function(r,n){e[r]=[function(e,r){r.exports=n},{}]};for(var f=0;f0&&$e.streams[2].object.output("\\n".charCodeAt(0)),$e.streams[3]&&$e.streams[3].object.output.buffer.length>0&&$e.streams[3].object.output("\\n".charCodeAt(0)))}},Je=Ja;Ve.unshift({func:function(){$e.ignorePermissions=!1,$e.init.initialized||$e.init()}}),Be.push({func:function(){$e.quit()}}),Ya(0),ae.buf=_(12,"void*",we),Module.callMain=function(r){function a(){for(var r=0;r<3;r++)i.push(0)}var e=r.length+1,i=[_(p("/bin/this.program"),"i8",we)];a();for(var v=0;v>2]=0|He.__str,Se[ri+4>>2]=0|He.__str1,Se[ri+16>>2]=0|He.__str2,Se[ri+20>>2]=0|He.__str3,Se[ri+32>>2]=0|He.__str4,Se[ri+36>>2]=0|He.__str5,Se[ri+48>>2]=0|He.__str6,Se[ri+52>>2]=0|He.__str7,Se[ri+64>>2]=0|He.__str8,Se[ri+68>>2]=0|He.__str7,Se[ri+80>>2]=0|He.__str9,Se[ri+84>>2]=0|He.__str10,Se[ri+96>>2]=0|He.__str11,Se[ri+100>>2]=0|He.__str12,Se[ri+112>>2]=0|He.__str13,Se[ri+116>>2]=0|He.__str14,Se[ri+128>>2]=0|He.__str15,Se[ri+132>>2]=0|He.__str16,Se[ri+144>>2]=0|He.__str17,Se[ri+148>>2]=0|He.__str18,Se[ri+160>>2]=0|He.__str19,Se[ri+164>>2]=0|He.__str20,Se[ri+176>>2]=0|He.__str21,Se[ri+180>>2]=0|He.__str22,Se[ri+192>>2]=0|He.__str23,Se[ri+196>>2]=0|He.__str24,Se[ri+208>>2]=0|He.__str25,Se[ri+212>>2]=0|He.__str26,Se[ri+224>>2]=0|He.__str27,Se[ri+228>>2]=0|He.__str28,Se[ri+240>>2]=0|He.__str29,Se[ri+244>>2]=0|He.__str30,Se[ri+256>>2]=0|He.__str31,Se[ri+260>>2]=0|He.__str32,Se[ri+272>>2]=0|He.__str33,Se[ri+276>>2]=0|He.__str34,Se[ri+288>>2]=0|He.__str35,Se[ri+292>>2]=0|He.__str36,Se[ri+304>>2]=0|He.__str37,Se[ri+308>>2]=0|He.__str38,Se[ri+320>>2]=0|He.__str39,Se[ri+324>>2]=0|He.__str40,Se[ri+336>>2]=0|He.__str41,Se[ri+340>>2]=0|He.__str42,Se[ri+352>>2]=0|He.__str43,Se[ri+356>>2]=0|He.__str44,Se[ri+368>>2]=0|He.__str45,Se[ri+372>>2]=0|He.__str46,Se[ri+384>>2]=0|He.__str47,Se[ri+388>>2]=0|He.__str48,Se[ri+400>>2]=0|He.__str49,Se[ri+404>>2]=0|He.__str119289,Se[ri+416>>2]=0|He.__str51,Se[ri+420>>2]=0|He.__str20,Se[ri+432>>2]=0|He.__str52,Se[ri+436>>2]=0|He.__str53,Se[ri+448>>2]=0|He.__str54,Se[ri+452>>2]=0|He.__str55,Se[ri+464>>2]=0|He.__str56,Se[ri+468>>2]=0|He.__str57,Se[ri+480>>2]=0|He.__str58,Se[ri+484>>2]=0|He.__str119289,Se[ri+496>>2]=0|He.__str59,Se[ri+500>>2]=0|He.__str60,Se[ri+512>>2]=0|He.__str61,Se[ri+516>>2]=0|He.__str62,Se[ri+528>>2]=0|He.__str63,Se[ri+532>>2]=0|He.__str64,Se[ri+544>>2]=0|He.__str65,Se[ri+548>>2]=0|He.__str66,Se[ri+560>>2]=0|He.__str67,Se[ri+564>>2]=0|He.__str68,Se[ri+576>>2]=0|He.__str69,Se[ri+580>>2]=0|He.__str70,Se[ri+592>>2]=0|He.__str71,Se[ri+596>>2]=0|He.__str72,Se[ri+608>>2]=0|He.__str73,Se[ri+612>>2]=0|He.__str74,Se[ri+624>>2]=0|He.__str75,Se[ri+628>>2]=0|He.__str76,Se[ri+640>>2]=0|He.__str77,Se[ri+644>>2]=0|He.__str72,Se[ri+656>>2]=0|He.__str78,Se[ri+660>>2]=0|He.__str79,Se[ri+672>>2]=0|He.__str80,Se[ri+676>>2]=0|He.__str81,Se[ri+688>>2]=0|He.__str82,Se[ri+692>>2]=0|He.__str83,Se[ri+704>>2]=0|He.__str84,Se[ri+708>>2]=0|He.__str85,Se[ri+720>>2]=0|He.__str86,Se[ri+724>>2]=0|He.__str87,Se[ri+736>>2]=0|He.__str88,Se[ri+740>>2]=0|He.__str89,Se[ri+752>>2]=0|He.__str90,Se[ri+756>>2]=0|He.__str91,Se[ri+768>>2]=0|He.__str92,Se[ri+772>>2]=0|He.__str91,Se[ai>>2]=0|He.__str145315,Se[ai+8>>2]=0|He.__str145315,Se[ai+20>>2]=0|He.__str167337,Se[ai+28>>2]=0|He.__str95,Se[ai+40>>2]=0|He.__str146316,Se[ai+48>>2]=0|He.__str97,Se[ai+60>>2]=0|He.__str155325,Se[ai+68>>2]=0|He.__str155325,Se[ai+80>>2]=0|He.__str156326,Se[ai+88>>2]=0|He.__str156326,Se[ai+100>>2]=0|He.__str154324,Se[ai+108>>2]=0|He.__str154324,Se[ai+120>>2]=0|He.__str101,Se[ai+128>>2]=0|He.__str101,Se[ai+140>>2]=0|He.__str147317,Se[ai+148>>2]=0|He.__str147317,Se[ai+160>>2]=0|He.__str150320,Se[ai+168>>2]=0|He.__str150320,Se[ai+180>>2]=0|He.__str151321,Se[ai+188>>2]=0|He.__str105,Se[ai+220>>2]=0|He.__str152322,Se[ai+228>>2]=0|He.__str152322,Se[ai+240>>2]=0|He.__str153323,Se[ai+248>>2]=0|He.__str153323,Se[ai+260>>2]=0|He.__str165335,Se[ai+268>>2]=0|He.__str165335,Se[ai+280>>2]=0|He.__str166336,Se[ai+288>>2]=0|He.__str166336,Se[ai+360>>2]=0|He.__str148318,Se[ai+368>>2]=0|He.__str148318,Se[ai+380>>2]=0|He.__str149319,Se[ai+388>>2]=0|He.__str149319,Se[ai+420>>2]=0|He.__str84254,Se[ai+428>>2]=0|He.__str84254,Se[ai+440>>2]=0|He.__str168338,Se[ai+448>>2]=0|He.__str146316,Se[ai+460>>2]=0|He.__str114,Se[ai+468>>2]=0|He.__str152322,Se[ai+480>>2]=0|He.__str115,Se[ai+488>>2]=0|He.__str115,Se[ai+500>>2]=0|He.__str110280,Se[ai+508>>2]=0|He.__str110280,Se[ei+4>>2]=0|He.__str152,Se[ei+12>>2]=0|He.__str152,Se[ei+32>>2]=0|He.__str153,Se[ei+40>>2]=0|He.__str153,Se[ei+48>>2]=0|He.__str154,Se[ei+60>>2]=0|He.__str155,Se[ei+68>>2]=0|He.__str155,Se[ei+76>>2]=0|He.__str156,Se[ei+88>>2]=0|He.__str157,Se[ei+96>>2]=0|He.__str158,Se[ei+104>>2]=0|He.__str156,Se[ei+116>>2]=0|He.__str159,Se[ei+124>>2]=0|He.__str160,Se[ei+132>>2]=0|He.__str161,Se[ei+144>>2]=0|He.__str162,Se[ei+152>>2]=0|He.__str163,Se[ei+160>>2]=0|He.__str164,Se[ei+172>>2]=0|He.__str165,Se[ei+180>>2]=0|He.__str166,Se[ei+188>>2]=0|He.__str167,Se[si+4>>2]=bi,Se[ni+4>>2]=ki,oi=_([2,0,0,0,0],["i8*",0,0,0,0],we),Se[bi>>2]=oi+8|0,Se[bi+4>>2]=0|He.__ZTSSt9bad_alloc,Se[bi+8>>2]=li,Se[ki>>2]=oi+8|0,Se[ki+4>>2]=0|He.__ZTSSt20bad_array_new_length,Se[ki+8>>2]=bi,ui=16,ci=6,hi=18,di=6,wi=6,pe=[0,0,Jr,0,va,0,ya,0,ga,0,wa,0,Sa,0,pa,0,Ea,0,ma,0],Module.FUNCTION_TABLE=pe,Module.run=ee,Module.preRun&&Module.preRun(),0==Ke){ee()}Module.postRun&&Module.postRun(),Module.___cxa_demangle=G;var pi=v("__cxa_demangle","string",["string","string","number","number"]);return function(r){return pi(r,"",1,0)}}();\n'; +},{}]},{},[180], null) +//# sourceMappingURL=demangle-cpp.8a387750.map \ No newline at end of file diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/favicon-16x16.361d2b26.png b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/favicon-16x16.361d2b26.png new file mode 100644 index 0000000000000000000000000000000000000000..2db188b0932fc2e98db8343a89b8f9d2a3950411 Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/favicon-16x16.361d2b26.png differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/favicon-32x32.1165a94e.png b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/favicon-32x32.1165a94e.png new file mode 100644 index 0000000000000000000000000000000000000000..d49690de3382ff513d1426620d0b7acb53135da2 Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/favicon-32x32.1165a94e.png differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/file-format-schema.json b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/file-format-schema.json new file mode 100644 index 0000000000000000000000000000000000000000..63842505efb74d1d8e57f4bfe1591a7a28abcc0e --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/file-format-schema.json @@ -0,0 +1,324 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "FileFormat.Profile": { + "anyOf": [ + { + "$ref": "#/definitions/FileFormat.EventedProfile" + }, + { + "$ref": "#/definitions/FileFormat.SampledProfile" + } + ] + }, + "FileFormat.File": { + "title": "FileFormat.File", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "enum": [ + "https://www.speedscope.app/file-format-schema.json" + ], + "title": "$schema" + }, + "shared": { + "type": "object", + "properties": { + "frames": { + "type": "array", + "items": { + "$ref": "#/definitions/FileFormat.Frame" + }, + "title": "frames" + } + }, + "required": [ + "frames" + ], + "title": "shared" + }, + "profiles": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/FileFormat.EventedProfile" + }, + { + "$ref": "#/definitions/FileFormat.SampledProfile" + } + ] + }, + "title": "profiles" + }, + "name": { + "type": "string", + "title": "name" + }, + "activeProfileIndex": { + "type": "number", + "title": "activeProfileIndex" + }, + "exporter": { + "type": "string", + "title": "exporter" + } + }, + "required": [ + "$schema", + "profiles", + "shared" + ] + }, + "FileFormat.Frame": { + "title": "FileFormat.Frame", + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "name" + }, + "file": { + "type": "string", + "title": "file" + }, + "line": { + "type": "number", + "title": "line" + }, + "col": { + "type": "number", + "title": "col" + } + }, + "required": [ + "name" + ] + }, + "FileFormat.ProfileType": { + "title": "FileFormat.ProfileType", + "enum": [ + "evented", + "sampled" + ], + "type": "string" + }, + "FileFormat.IProfile": { + "title": "FileFormat.IProfile", + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/FileFormat.ProfileType", + "title": "type" + } + }, + "required": [ + "type" + ] + }, + "FileFormat.EventedProfile": { + "title": "FileFormat.EventedProfile", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "evented" + ], + "title": "type" + }, + "name": { + "type": "string", + "title": "name" + }, + "unit": { + "$ref": "#/definitions/FileFormat.ValueUnit", + "title": "unit" + }, + "startValue": { + "type": "number", + "title": "startValue" + }, + "endValue": { + "type": "number", + "title": "endValue" + }, + "events": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/OpenFrameEvent" + }, + { + "$ref": "#/definitions/CloseFrameEvent" + } + ] + }, + "title": "events" + } + }, + "required": [ + "endValue", + "events", + "name", + "startValue", + "type", + "unit" + ] + }, + "SampledStack": { + "type": "array", + "items": { + "type": "number" + } + }, + "FileFormat.SampledProfile": { + "title": "FileFormat.SampledProfile", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "sampled" + ], + "title": "type" + }, + "name": { + "type": "string", + "title": "name" + }, + "unit": { + "$ref": "#/definitions/FileFormat.ValueUnit", + "title": "unit" + }, + "startValue": { + "type": "number", + "title": "startValue" + }, + "endValue": { + "type": "number", + "title": "endValue" + }, + "samples": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number" + } + }, + "title": "samples" + }, + "weights": { + "type": "array", + "items": { + "type": "number" + }, + "title": "weights" + } + }, + "required": [ + "endValue", + "name", + "samples", + "startValue", + "type", + "unit", + "weights" + ] + }, + "FileFormat.ValueUnit": { + "title": "FileFormat.ValueUnit", + "enum": [ + "bytes", + "microseconds", + "milliseconds", + "nanoseconds", + "none", + "seconds" + ], + "type": "string" + }, + "FileFormat.EventType": { + "title": "FileFormat.EventType", + "enum": [ + "C", + "O" + ], + "type": "string" + }, + "IEvent": { + "title": "IEvent", + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/FileFormat.EventType", + "title": "type" + }, + "at": { + "type": "number", + "title": "at" + } + }, + "required": [ + "at", + "type" + ] + }, + "OpenFrameEvent": { + "title": "OpenFrameEvent", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "O" + ], + "title": "type" + }, + "frame": { + "type": "number", + "title": "frame" + }, + "at": { + "type": "number", + "title": "at" + } + }, + "required": [ + "at", + "frame", + "type" + ] + }, + "CloseFrameEvent": { + "title": "CloseFrameEvent", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "C" + ], + "title": "type" + }, + "frame": { + "type": "number", + "title": "frame" + }, + "at": { + "type": "number", + "title": "at" + } + }, + "required": [ + "at", + "frame", + "type" + ] + } + }, + "$ref": "#/definitions/FileFormat.File" +} diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/import.a03c2bef.js b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/import.a03c2bef.js new file mode 100644 index 0000000000000000000000000000000000000000..483e825f3d3e8f9eb1a20acd592000af909cf934 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/import.a03c2bef.js @@ -0,0 +1,113 @@ +parcelRequire=function(e,r,n,t){var i="function"==typeof parcelRequire&&parcelRequire,o="function"==typeof require&&require;function u(n,t){if(!r[n]){if(!e[n]){var f="function"==typeof parcelRequire&&parcelRequire;if(!t&&f)return f(n,!0);if(i)return i(n,!0);if(o&&"string"==typeof n)return o(n);var c=new Error("Cannot find module '"+n+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[n][1][r]||r};var l=r[n]=new u.Module(n);e[n][0].call(l.exports,p,l,l.exports,this)}return r[n].exports;function p(e){return u(p.resolve(e))}}u.isParcelRequire=!0,u.Module=function(e){this.id=e,this.bundle=u,this.exports={}},u.modules=e,u.cache=r,u.parent=i,u.register=function(r,n){e[r]=[function(e,r){r.exports=n},{}]};for(var f=0;fe.id)}),r.children.forEach(e)}(e),t}function t(e,t){return e.map((r,n)=>{return r-(0===n?1e6*t:e[n-1])})}function r(r){return{samples:r.samples,startTime:1e6*r.startTime,endTime:1e6*r.endTime,nodes:e(r.head),timeDeltas:t(r.timestamps,r.startTime)}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.chromeTreeToNodes=r; +},{}],146:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.isChromeTimeline=i,exports.importFromChromeTimeline=o,exports.importFromChromeCPUProfile=f,exports.importFromOldV8CPUProfile=u;var e=require("../lib/profile"),t=require("../lib/utils"),r=require("../lib/value-formatters"),n=require("./v8cpuFormatter");function i(e){if(!Array.isArray(e))return!1;if(e.length<1)return!1;const t=e[0];return"pid"in t&&"tid"in t&&"ph"in t&&"cat"in t&&!!e.find(e=>"CpuProfile"===e.name||"Profile"===e.name||"ProfileChunk"===e.name)}function o(e,r){const n=new Map,i=new Map,o=new Map;(0,t.sortBy)(e,e=>e.ts);for(let t of e){if("CpuProfile"===t.name){const e=`${t.pid}:${t.tid}`,r=t.id||e;n.set(r,t.args.data.cpuProfile),i.set(r,e)}if("Profile"===t.name){const e=`${t.pid}:${t.tid}`;n.set(t.id||e,Object.assign({startTime:0,endTime:0,nodes:[],samples:[],timeDeltas:[]},t.args.data)),t.id&&i.set(t.id,`${t.pid}:${t.tid}`)}if("thread_name"===t.name&&o.set(`${t.pid}:${t.tid}`,t.args.name),"ProfileChunk"===t.name){const e=`${t.pid}:${t.tid}`,r=n.get(t.id||e);if(r){const e=t.args.data;e.cpuProfile&&(e.cpuProfile.nodes&&(r.nodes=r.nodes.concat(e.cpuProfile.nodes)),e.cpuProfile.samples&&(r.samples=r.samples.concat(e.cpuProfile.samples))),e.timeDeltas&&(r.timeDeltas=r.timeDeltas.concat(e.timeDeltas)),null!=e.startTime&&(r.startTime=e.startTime),null!=e.endTime&&(r.endTime=e.endTime)}else console.warn(`Ignoring ProfileChunk for undeclared Profile with id ${t.id||e}`)}}if(n.size>0){const e=[];let l=0;return(0,t.itForEach)(n.keys(),t=>{let a=null,s=i.get(t);s&&(a=o.get(s)||null);const m=f(n.get(t));a&&n.size>1?(m.setName(`${r} - ${a}`),"CrRendererMain"===a&&(l=e.length)):m.setName(`${r}`),e.push(m)}),{name:r,indexToView:l,profiles:e}}throw new Error("Could not find CPU profile in Timeline")}const l=new Map;function a(e){return(0,t.getOrInsert)(l,e,e=>{const t=e.functionName||"(anonymous)",r=e.url,n=e.lineNumber,i=e.columnNumber;return{key:`${t}:${r}:${n}:${i}`,name:t,file:r,line:n,col:i}})}function s(e){const{functionName:t,url:r}=e;return"native dummy.js"===r||("(root)"===t||"(idle)"===t)}function m(e){return"(garbage collector)"===e||"(program)"===e}function f(n){const i=new e.CallTreeProfileBuilder(n.endTime-n.startTime),o=new Map;for(let e of n.nodes)o.set(e.id,e);for(let e of n.nodes)if("number"==typeof e.parent&&(e.parent=o.get(e.parent)),e.children)for(let t of e.children){const r=o.get(t);r&&(r.parent=e)}const l=[],f=[];let u=n.timeDeltas[0],c=NaN;for(let e=0;e0&&(0,t.lastOf)(p)!=c;){const e=a(p.pop().callFrame);i.leaveFrame(e,r)}const d=[];for(let e=u;e&&e!=c&&!s(e.callFrame);e=m(e.callFrame.functionName)?(0,t.lastOf)(p):e.parent||null)d.push(e);d.reverse();for(let e of d)i.enterFrame(a(e.callFrame),r);p=p.concat(d)}for(let e=p.length-1;e>=0;e--)i.leaveFrame(a(p[e].callFrame),(0,t.lastOf)(f));return i.setValueFormatter(new r.TimeFormatter("microseconds")),i.build()}function u(e){return f((0,n.chromeTreeToNodes)(e))} +},{"../lib/profile":139,"../lib/utils":70,"../lib/value-formatters":140,"./v8cpuFormatter":172}],147:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.importFromStackprof=r;var e=require("../lib/profile"),t=require("../lib/value-formatters");function r(r){const o=r.raw_timestamp_deltas.reduce((e,t)=>e+t,0),a=new e.StackListProfileBuilder(o),{frames:l,raw:s,raw_timestamp_deltas:i}=r;let n=0,c=[];for(let e=0;e=0;)e[t]=0}var i=0,l=1,d=2,f=3,o=258,b=29,s=256,u=s+1+b,c=30,p=19,h=2*u+1,v=15,y=16,x=7,g=256,m=16,w=17,A=18,k=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],q=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],z=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],S=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],j=512,B=new Array(2*(u+2));a(B);var C=new Array(2*c);a(C);var D=new Array(j);a(D);var E=new Array(o-f+1);a(E);var F=new Array(b);a(F);var G,H,I,J=new Array(c);function K(e,t,n,_,r){this.static_tree=e,this.extra_bits=t,this.extra_base=n,this.elems=_,this.max_length=r,this.has_stree=e&&e.length}function L(e,t){this.dyn_tree=e,this.max_code=0,this.stat_desc=t}function M(e){return e<256?D[e]:D[256+(e>>>7)]}function N(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function O(e,t,n){e.bi_valid>y-n?(e.bi_buf|=t<>y-e.bi_valid,e.bi_valid+=n-y):(e.bi_buf|=t<>>=1,n<<=1}while(--t>0);return n>>>1}function R(e){16===e.bi_valid?(N(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):e.bi_valid>=8&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}function T(e,t){var n,_,r,a,i,l,d=t.dyn_tree,f=t.max_code,o=t.stat_desc.static_tree,b=t.stat_desc.has_stree,s=t.stat_desc.extra_bits,u=t.stat_desc.extra_base,c=t.stat_desc.max_length,p=0;for(a=0;a<=v;a++)e.bl_count[a]=0;for(d[2*e.heap[e.heap_max]+1]=0,n=e.heap_max+1;nc&&(a=c,p++),d[2*_+1]=a,_>f||(e.bl_count[a]++,i=0,_>=u&&(i=s[_-u]),l=d[2*_],e.opt_len+=l*(a+i),b&&(e.static_len+=l*(o[2*_+1]+i)));if(0!==p){do{for(a=c-1;0===e.bl_count[a];)a--;e.bl_count[a]--,e.bl_count[a+1]+=2,e.bl_count[c]--,p-=2}while(p>0);for(a=c;0!==a;a--)for(_=e.bl_count[a];0!==_;)(r=e.heap[--n])>f||(d[2*r+1]!==a&&(e.opt_len+=(a-d[2*r+1])*d[2*r],d[2*r+1]=a),_--)}}function U(e,t,n){var _,r,a=new Array(v+1),i=0;for(_=1;_<=v;_++)a[_]=i=i+n[_-1]<<1;for(r=0;r<=t;r++){var l=e[2*r+1];0!==l&&(e[2*r]=Q(a[l]++,l))}}function V(){var e,t,n,_,r,a=new Array(v+1);for(n=0,_=0;_>=7;_8?N(e,e.bi_buf):e.bi_valid>0&&(e.pending_buf[e.pending++]=e.bi_buf),e.bi_buf=0,e.bi_valid=0}function Y(t,n,_,r){X(t),r&&(N(t,_),N(t,~_)),e.arraySet(t.pending_buf,t.window,n,_,t.pending),t.pending+=_}function Z(e,t,n,_){var r=2*t,a=2*n;return e[r]>1;n>=1;n--)$(e,a,n);r=d;do{n=e.heap[1],e.heap[1]=e.heap[e.heap_len--],$(e,a,1),_=e.heap[1],e.heap[--e.heap_max]=n,e.heap[--e.heap_max]=_,a[2*r]=a[2*n]+a[2*_],e.depth[r]=(e.depth[n]>=e.depth[_]?e.depth[n]:e.depth[_])+1,a[2*n+1]=a[2*_+1]=r,e.heap[1]=r++,$(e,a,1)}while(e.heap_len>=2);e.heap[--e.heap_max]=e.heap[1],T(e,t),U(a,f,e.bl_count)}function ne(e,t,n){var _,r,a=-1,i=t[1],l=0,d=7,f=4;for(0===i&&(d=138,f=3),t[2*(n+1)+1]=65535,_=0;_<=n;_++)r=i,i=t[2*(_+1)+1],++l=3&&0===e.bl_tree[2*S[t]+1];t--);return e.opt_len+=3*(t+1)+5+5+4,t}function ae(e,t,n,_){var r;for(O(e,t-257,5),O(e,n-1,5),O(e,_-4,4),r=0;r<_;r++)O(e,e.bl_tree[2*S[r]+1],3);_e(e,e.dyn_ltree,t-1),_e(e,e.dyn_dtree,n-1)}function ie(e){var t,r=4093624447;for(t=0;t<=31;t++,r>>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return n;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return _;for(t=32;t0?(e.strm.data_type===r&&(e.strm.data_type=ie(e)),te(e,e.l_desc),te(e,e.d_desc),o=re(e),i=e.opt_len+3+7>>>3,(f=e.static_len+3+7>>>3)<=i&&(i=f)):i=f=_+5,_+4<=i&&-1!==n?fe(e,n,_,a):e.strategy===t||f===i?(O(e,(l<<1)+(a?1:0),3),ee(e,B,C)):(O(e,(d<<1)+(a?1:0),3),ae(e,e.l_desc.max_code+1,e.d_desc.max_code+1,o+1),ee(e,e.dyn_ltree,e.dyn_dtree)),W(e),a&&X(e)}function se(e,t,n){return e.pending_buf[e.d_buf+2*e.last_lit]=t>>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&n,e.last_lit++,0===t?e.dyn_ltree[2*n]++:(e.matches++,t--,e.dyn_ltree[2*(E[n]+s+1)]++,e.dyn_dtree[2*M(t)]++),e.last_lit===e.lit_bufsize-1}exports._tr_init=de,exports._tr_stored_block=fe,exports._tr_flush_block=be,exports._tr_tally=se,exports._tr_align=oe; +},{"../utils/common":178}],191:[function(require,module,exports) { +"use strict";function e(e,r,o,t){for(var u=65535&e|0,i=e>>>16&65535|0,n=0;0!==o;){o-=n=o>2e3?2e3:o;do{i=i+(u=u+r[t++]|0)|0}while(--n);u%=65521,i%=65521}return u|i<<16|0}module.exports=e; +},{}],192:[function(require,module,exports) { +"use strict";function r(){for(var r,o=[],t=0;t<256;t++){r=t;for(var n=0;n<8;n++)r=1&r?3988292384^r>>>1:r>>>1;o[t]=r}return o}var o=r();function t(r,t,n,u){var a=o,e=u+n;r^=-1;for(var f=u;f>>8^a[255&(r^t[f])];return-1^r}module.exports=t; +},{}],185:[function(require,module,exports) { +"use strict";module.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}; +},{}],182:[function(require,module,exports) { +"use strict";var t,a=require("../utils/common"),e=require("./trees"),s=require("./adler32"),i=require("./crc32"),r=require("./messages"),n=0,h=1,l=3,_=4,d=5,o=0,u=1,g=-2,f=-3,c=-5,p=-1,m=1,w=2,v=3,k=4,z=0,b=2,x=8,y=9,B=15,S=8,q=29,I=256,A=I+1+q,C=30,R=19,j=2*A+1,D=15,E=3,H=258,K=H+E+1,N=32,F=42,G=69,J=73,L=91,M=103,O=113,P=666,Q=1,T=2,U=3,V=4,W=3;function X(t,a){return t.msg=r[a],a}function Y(t){return(t<<1)-(t>4?9:0)}function Z(t){for(var a=t.length;--a>=0;)t[a]=0}function $(t){var e=t.state,s=e.pending;s>t.avail_out&&(s=t.avail_out),0!==s&&(a.arraySet(t.output,e.pending_buf,e.pending_out,s,t.next_out),t.next_out+=s,e.pending_out+=s,t.total_out+=s,t.avail_out-=s,e.pending-=s,0===e.pending&&(e.pending_out=0))}function tt(t,a){e._tr_flush_block(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,a),t.block_start=t.strstart,$(t.strm)}function at(t,a){t.pending_buf[t.pending++]=a}function et(t,a){t.pending_buf[t.pending++]=a>>>8&255,t.pending_buf[t.pending++]=255&a}function st(t,e,r,n){var h=t.avail_in;return h>n&&(h=n),0===h?0:(t.avail_in-=h,a.arraySet(e,t.input,t.next_in,h,r),1===t.state.wrap?t.adler=s(t.adler,e,h,r):2===t.state.wrap&&(t.adler=i(t.adler,e,h,r)),t.next_in+=h,t.total_in+=h,h)}function it(t,a){var e,s,i=t.max_chain_length,r=t.strstart,n=t.prev_length,h=t.nice_match,l=t.strstart>t.w_size-K?t.strstart-(t.w_size-K):0,_=t.window,d=t.w_mask,o=t.prev,u=t.strstart+H,g=_[r+n-1],f=_[r+n];t.prev_length>=t.good_match&&(i>>=2),h>t.lookahead&&(h=t.lookahead);do{if(_[(e=a)+n]===f&&_[e+n-1]===g&&_[e]===_[r]&&_[++e]===_[r+1]){r+=2,e++;do{}while(_[++r]===_[++e]&&_[++r]===_[++e]&&_[++r]===_[++e]&&_[++r]===_[++e]&&_[++r]===_[++e]&&_[++r]===_[++e]&&_[++r]===_[++e]&&_[++r]===_[++e]&&rn){if(t.match_start=a,n=s,s>=h)break;g=_[r+n-1],f=_[r+n]}}}while((a=o[a&d])>l&&0!=--i);return n<=t.lookahead?n:t.lookahead}function rt(t){var e,s,i,r,n,h=t.w_size;do{if(r=t.window_size-t.lookahead-t.strstart,t.strstart>=h+(h-K)){a.arraySet(t.window,t.window,h,h,0),t.match_start-=h,t.strstart-=h,t.block_start-=h,e=s=t.hash_size;do{i=t.head[--e],t.head[e]=i>=h?i-h:0}while(--s);e=s=h;do{i=t.prev[--e],t.prev[e]=i>=h?i-h:0}while(--s);r+=h}if(0===t.strm.avail_in)break;if(s=st(t.strm,t.window,t.strstart+t.lookahead,r),t.lookahead+=s,t.lookahead+t.insert>=E)for(n=t.strstart-t.insert,t.ins_h=t.window[n],t.ins_h=(t.ins_h<t.pending_buf_size-5&&(e=t.pending_buf_size-5);;){if(t.lookahead<=1){if(rt(t),0===t.lookahead&&a===n)return Q;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var s=t.block_start+e;if((0===t.strstart||t.strstart>=s)&&(t.lookahead=t.strstart-s,t.strstart=s,tt(t,!1),0===t.strm.avail_out))return Q;if(t.strstart-t.block_start>=t.w_size-K&&(tt(t,!1),0===t.strm.avail_out))return Q}return t.insert=0,a===_?(tt(t,!0),0===t.strm.avail_out?U:V):(t.strstart>t.block_start&&(tt(t,!1),t.strm.avail_out),Q)}function ht(t,a){for(var s,i;;){if(t.lookahead=E&&(t.ins_h=(t.ins_h<=E)if(i=e._tr_tally(t,t.strstart-t.match_start,t.match_length-E),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=E){t.match_length--;do{t.strstart++,t.ins_h=(t.ins_h<=E&&(t.ins_h=(t.ins_h<4096)&&(t.match_length=E-1)),t.prev_length>=E&&t.match_length<=t.prev_length){r=t.strstart+t.lookahead-E,i=e._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-E),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=r&&(t.ins_h=(t.ins_h<=E&&t.strstart>0&&(i=l[r=t.strstart-1])===l[++r]&&i===l[++r]&&i===l[++r]){h=t.strstart+H;do{}while(i===l[++r]&&i===l[++r]&&i===l[++r]&&i===l[++r]&&i===l[++r]&&i===l[++r]&&i===l[++r]&&i===l[++r]&&rt.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=E?(s=e._tr_tally(t,1,t.match_length-E),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(s=e._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),s&&(tt(t,!1),0===t.strm.avail_out))return Q}return t.insert=0,a===_?(tt(t,!0),0===t.strm.avail_out?U:V):t.last_lit&&(tt(t,!1),0===t.strm.avail_out)?Q:T}function dt(t,a){for(var s;;){if(0===t.lookahead&&(rt(t),0===t.lookahead)){if(a===n)return Q;break}if(t.match_length=0,s=e._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,s&&(tt(t,!1),0===t.strm.avail_out))return Q}return t.insert=0,a===_?(tt(t,!0),0===t.strm.avail_out?U:V):t.last_lit&&(tt(t,!1),0===t.strm.avail_out)?Q:T}function ot(t,a,e,s,i){this.good_length=t,this.max_lazy=a,this.nice_length=e,this.max_chain=s,this.func=i}function ut(a){a.window_size=2*a.w_size,Z(a.head),a.max_lazy_match=t[a.level].max_lazy,a.good_match=t[a.level].good_length,a.nice_match=t[a.level].nice_length,a.max_chain_length=t[a.level].max_chain,a.strstart=0,a.block_start=0,a.lookahead=0,a.insert=0,a.match_length=a.prev_length=E-1,a.match_available=0,a.ins_h=0}function gt(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=x,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new a.Buf16(2*j),this.dyn_dtree=new a.Buf16(2*(2*C+1)),this.bl_tree=new a.Buf16(2*(2*R+1)),Z(this.dyn_ltree),Z(this.dyn_dtree),Z(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new a.Buf16(D+1),this.heap=new a.Buf16(2*A+1),Z(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new a.Buf16(2*A+1),Z(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function ft(t){var a;return t&&t.state?(t.total_in=t.total_out=0,t.data_type=b,(a=t.state).pending=0,a.pending_out=0,a.wrap<0&&(a.wrap=-a.wrap),a.status=a.wrap?F:O,t.adler=2===a.wrap?0:1,a.last_flush=n,e._tr_init(a),o):X(t,g)}function ct(t){var a=ft(t);return a===o&&ut(t.state),a}function pt(t,a){return t&&t.state?2!==t.state.wrap?g:(t.state.gzhead=a,o):g}function mt(t,e,s,i,r,n){if(!t)return g;var h=1;if(e===p&&(e=6),i<0?(h=0,i=-i):i>15&&(h=2,i-=16),r<1||r>y||s!==x||i<8||i>15||e<0||e>9||n<0||n>k)return X(t,g);8===i&&(i=9);var l=new gt;return t.state=l,l.strm=t,l.wrap=h,l.gzhead=null,l.w_bits=i,l.w_size=1<d||s<0)return a?X(a,g):g;if(f=a.state,!a.output||!a.input&&0!==a.avail_in||f.status===P&&s!==_)return X(a,0===a.avail_out?c:g);if(f.strm=a,r=f.last_flush,f.last_flush=s,f.status===F)if(2===f.wrap)a.adler=0,at(f,31),at(f,139),at(f,8),f.gzhead?(at(f,(f.gzhead.text?1:0)+(f.gzhead.hcrc?2:0)+(f.gzhead.extra?4:0)+(f.gzhead.name?8:0)+(f.gzhead.comment?16:0)),at(f,255&f.gzhead.time),at(f,f.gzhead.time>>8&255),at(f,f.gzhead.time>>16&255),at(f,f.gzhead.time>>24&255),at(f,9===f.level?2:f.strategy>=w||f.level<2?4:0),at(f,255&f.gzhead.os),f.gzhead.extra&&f.gzhead.extra.length&&(at(f,255&f.gzhead.extra.length),at(f,f.gzhead.extra.length>>8&255)),f.gzhead.hcrc&&(a.adler=i(a.adler,f.pending_buf,f.pending,0)),f.gzindex=0,f.status=G):(at(f,0),at(f,0),at(f,0),at(f,0),at(f,0),at(f,9===f.level?2:f.strategy>=w||f.level<2?4:0),at(f,W),f.status=O);else{var k=x+(f.w_bits-8<<4)<<8;k|=(f.strategy>=w||f.level<2?0:f.level<6?1:6===f.level?2:3)<<6,0!==f.strstart&&(k|=N),k+=31-k%31,f.status=O,et(f,k),0!==f.strstart&&(et(f,a.adler>>>16),et(f,65535&a.adler)),a.adler=1}if(f.status===G)if(f.gzhead.extra){for(p=f.pending;f.gzindex<(65535&f.gzhead.extra.length)&&(f.pending!==f.pending_buf_size||(f.gzhead.hcrc&&f.pending>p&&(a.adler=i(a.adler,f.pending_buf,f.pending-p,p)),$(a),p=f.pending,f.pending!==f.pending_buf_size));)at(f,255&f.gzhead.extra[f.gzindex]),f.gzindex++;f.gzhead.hcrc&&f.pending>p&&(a.adler=i(a.adler,f.pending_buf,f.pending-p,p)),f.gzindex===f.gzhead.extra.length&&(f.gzindex=0,f.status=J)}else f.status=J;if(f.status===J)if(f.gzhead.name){p=f.pending;do{if(f.pending===f.pending_buf_size&&(f.gzhead.hcrc&&f.pending>p&&(a.adler=i(a.adler,f.pending_buf,f.pending-p,p)),$(a),p=f.pending,f.pending===f.pending_buf_size)){m=1;break}m=f.gzindexp&&(a.adler=i(a.adler,f.pending_buf,f.pending-p,p)),0===m&&(f.gzindex=0,f.status=L)}else f.status=L;if(f.status===L)if(f.gzhead.comment){p=f.pending;do{if(f.pending===f.pending_buf_size&&(f.gzhead.hcrc&&f.pending>p&&(a.adler=i(a.adler,f.pending_buf,f.pending-p,p)),$(a),p=f.pending,f.pending===f.pending_buf_size)){m=1;break}m=f.gzindexp&&(a.adler=i(a.adler,f.pending_buf,f.pending-p,p)),0===m&&(f.status=M)}else f.status=M;if(f.status===M&&(f.gzhead.hcrc?(f.pending+2>f.pending_buf_size&&$(a),f.pending+2<=f.pending_buf_size&&(at(f,255&a.adler),at(f,a.adler>>8&255),a.adler=0,f.status=O)):f.status=O),0!==f.pending){if($(a),0===a.avail_out)return f.last_flush=-1,o}else if(0===a.avail_in&&Y(s)<=Y(r)&&s!==_)return X(a,c);if(f.status===P&&0!==a.avail_in)return X(a,c);if(0!==a.avail_in||0!==f.lookahead||s!==n&&f.status!==P){var z=f.strategy===w?dt(f,s):f.strategy===v?_t(f,s):t[f.level].func(f,s);if(z!==U&&z!==V||(f.status=P),z===Q||z===U)return 0===a.avail_out&&(f.last_flush=-1),o;if(z===T&&(s===h?e._tr_align(f):s!==d&&(e._tr_stored_block(f,0,0,!1),s===l&&(Z(f.head),0===f.lookahead&&(f.strstart=0,f.block_start=0,f.insert=0))),$(a),0===a.avail_out))return f.last_flush=-1,o}return s!==_?o:f.wrap<=0?u:(2===f.wrap?(at(f,255&a.adler),at(f,a.adler>>8&255),at(f,a.adler>>16&255),at(f,a.adler>>24&255),at(f,255&a.total_in),at(f,a.total_in>>8&255),at(f,a.total_in>>16&255),at(f,a.total_in>>24&255)):(et(f,a.adler>>>16),et(f,65535&a.adler)),$(a),f.wrap>0&&(f.wrap=-f.wrap),0!==f.pending?o:u)}function kt(t){var a;return t&&t.state?(a=t.state.status)!==F&&a!==G&&a!==J&&a!==L&&a!==M&&a!==O&&a!==P?X(t,g):(t.state=null,a===O?X(t,f):o):g}function zt(t,e){var i,r,n,h,l,_,d,u,f=e.length;if(!t||!t.state)return g;if(2===(h=(i=t.state).wrap)||1===h&&i.status!==F||i.lookahead)return g;for(1===h&&(t.adler=s(t.adler,e,f,0)),i.wrap=0,f>=i.w_size&&(0===h&&(Z(i.head),i.strstart=0,i.block_start=0,i.insert=0),u=new a.Buf8(i.w_size),a.arraySet(u,e,f-i.w_size,i.w_size,0),e=u,f=i.w_size),l=t.avail_in,_=t.next_in,d=t.input,t.avail_in=f,t.next_in=0,t.input=e,rt(i);i.lookahead>=E;){r=i.strstart,n=i.lookahead-(E-1);do{i.ins_h=(i.ins_h<=252?6:o>=248?5:o>=240?4:o>=224?3:o>=192?2:1;function f(e,o){if(o<65537&&(e.subarray&&t||!e.subarray&&n))return String.fromCharCode.apply(null,r.shrinkBuf(e,o));for(var f="",u=0;u>>6,t[u++]=128|63&e):e<65536?(t[u++]=224|e>>>12,t[u++]=128|e>>>6&63,t[u++]=128|63&e):(t[u++]=240|e>>>18,t[u++]=128|e>>>12&63,t[u++]=128|e>>>6&63,t[u++]=128|63&e);return t},exports.buf2binstring=function(r){return f(r,r.length)},exports.binstring2buf=function(n){for(var t=new r.Buf8(n.length),e=0,o=t.length;e4)h[o++]=65533,t+=a-1;else{for(u&=2===a?31:3===a?15:7;a>1&&t1?h[o++]=65533:u<65536?h[o++]=u:(u-=65536,h[o++]=55296|u>>10&1023,h[o++]=56320|1023&u)}return f(h,o)},exports.utf8border=function(r,n){var t;for((n=n||r.length)>r.length&&(n=r.length),t=n-1;t>=0&&128==(192&r[t]);)t--;return t<0?n:0===t?n:t+e[r[t]]>n?t:n}; +},{"./common":178}],184:[function(require,module,exports) { +"use strict";function t(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}module.exports=t; +},{}],176:[function(require,module,exports) { +"use strict";var t=require("./zlib/deflate"),i=require("./utils/common"),e=require("./utils/strings"),n=require("./zlib/messages"),r=require("./zlib/zstream"),s=Object.prototype.toString,o=0,a=4,u=0,h=1,d=2,l=-1,f=0,p=8;function w(o){if(!(this instanceof w))return new w(o);this.options=i.assign({level:l,method:p,chunkSize:16384,windowBits:15,memLevel:8,strategy:f,to:""},o||{});var a=this.options;a.raw&&a.windowBits>0?a.windowBits=-a.windowBits:a.gzip&&a.windowBits>0&&a.windowBits<16&&(a.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new r,this.strm.avail_out=0;var h=t.deflateInit2(this.strm,a.level,a.method,a.windowBits,a.memLevel,a.strategy);if(h!==u)throw new Error(n[h]);if(a.header&&t.deflateSetHeader(this.strm,a.header),a.dictionary){var d;if(d="string"==typeof a.dictionary?e.string2buf(a.dictionary):"[object ArrayBuffer]"===s.call(a.dictionary)?new Uint8Array(a.dictionary):a.dictionary,(h=t.deflateSetDictionary(this.strm,d))!==u)throw new Error(n[h]);this._dict_set=!0}}function c(t,i){var e=new w(i);if(e.push(t,!0),e.err)throw e.msg||n[e.err];return e.result}function m(t,i){return(i=i||{}).raw=!0,c(t,i)}function g(t,i){return(i=i||{}).gzip=!0,c(t,i)}w.prototype.push=function(n,r){var l,f,p=this.strm,w=this.options.chunkSize;if(this.ended)return!1;f=r===~~r?r:!0===r?a:o,"string"==typeof n?p.input=e.string2buf(n):"[object ArrayBuffer]"===s.call(n)?p.input=new Uint8Array(n):p.input=n,p.next_in=0,p.avail_in=p.input.length;do{if(0===p.avail_out&&(p.output=new i.Buf8(w),p.next_out=0,p.avail_out=w),(l=t.deflate(p,f))!==h&&l!==u)return this.onEnd(l),this.ended=!0,!1;0!==p.avail_out&&(0!==p.avail_in||f!==a&&f!==d)||("string"===this.options.to?this.onData(e.buf2binstring(i.shrinkBuf(p.output,p.next_out))):this.onData(i.shrinkBuf(p.output,p.next_out)))}while((p.avail_in>0||0===p.avail_out)&&l!==h);return f===a?(l=t.deflateEnd(this.strm),this.onEnd(l),this.ended=!0,l===u):f!==d||(this.onEnd(u),p.avail_out=0,!0)},w.prototype.onData=function(t){this.chunks.push(t)},w.prototype.onEnd=function(t){t===u&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=i.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},exports.Deflate=w,exports.deflate=c,exports.deflateRaw=m,exports.gzip=g; +},{"./zlib/deflate":182,"./utils/common":178,"./utils/strings":183,"./zlib/messages":185,"./zlib/zstream":184}],199:[function(require,module,exports) { +"use strict";var i=30,e=12;module.exports=function(o,a){var t,d,n,l,s,f,r,b,c,u,v,m,w,h,k,_,x,g,p,z,j,q,y,A,B;t=o.state,d=o.next_in,A=o.input,n=d+(o.avail_in-5),l=o.next_out,B=o.output,s=l-(a-o.avail_out),f=l+(o.avail_out-257),r=t.dmax,b=t.wsize,c=t.whave,u=t.wnext,v=t.window,m=t.hold,w=t.bits,h=t.lencode,k=t.distcode,_=(1<>>=p=g>>>24,w-=p,0===(p=g>>>16&255))B[l++]=65535&g;else{if(!(16&p)){if(0==(64&p)){g=h[(65535&g)+(m&(1<>>=p,w-=p),w<15&&(m+=A[d++]<>>=p=g>>>24,w-=p,!(16&(p=g>>>16&255))){if(0==(64&p)){g=k[(65535&g)+(m&(1<r){o.msg="invalid distance too far back",t.mode=i;break i}if(m>>>=p,w-=p,j>(p=l-s)){if((p=j-p)>c&&t.sane){o.msg="invalid distance too far back",t.mode=i;break i}if(q=0,y=v,0===u){if(q+=b-p,p2;)B[l++]=y[q++],B[l++]=y[q++],B[l++]=y[q++],z-=3;z&&(B[l++]=y[q++],z>1&&(B[l++]=y[q++]))}else{q=l-j;do{B[l++]=B[q++],B[l++]=B[q++],B[l++]=B[q++],z-=3}while(z>2);z&&(B[l++]=B[q++],z>1&&(B[l++]=B[q++]))}break}}break}}while(d>3,m&=(1<<(w-=z<<3))-1,o.next_in=d,o.next_out=l,o.avail_in=d=1&&0===P[G];G--);if(H>G&&(H=G),0===G)return d[v++]=20971520,d[v++]=20971520,h.bits=1,0;for(F=1;F0&&(a===e||1!==G))return-1;for(Q[1]=0,D=1;Di||a===t&&L>o)return 1;for(;;){y=D-J,B[E]j?(z=R[S+B[E]],A=N[O+B[E]]):(z=96,A=0),k=1<>J)+(p-=k)]=y<<24|z<<16|A|0}while(0!==p);for(k=1<>=1;if(0!==k?(M&=k-1,M+=k):M=0,E++,0==--P[D]){if(D===G)break;D=c[m+B[E]]}if(D>H&&(M&x)!==q){for(0===J&&(J=H),g+=F,K=1<<(I=D-J);I+Ji||a===t&&L>o)return 1;d[q=M&x]=H<<24|I<<16|g-v|0}}return 0!==M&&(d[g+M]=D-J<<24|64<<16|0),h.bits=H,0}; +},{"../utils/common":178}],187:[function(require,module,exports) { +"use strict";var e=require("../utils/common"),a=require("./adler32"),t=require("./crc32"),i=require("./inffast"),s=require("./inftrees"),n=0,r=1,o=2,d=4,l=5,f=6,c=0,h=1,k=2,b=-2,m=-3,w=-4,u=-5,g=8,v=1,x=2,p=3,_=4,y=5,z=6,B=7,S=8,q=9,C=10,I=11,R=12,j=13,A=14,D=15,E=16,G=17,H=18,K=19,N=20,F=21,J=22,L=23,M=24,O=25,P=26,Q=27,T=28,U=29,V=30,W=31,X=32,Y=852,Z=592,$=15,ee=$;function ae(e){return(e>>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function te(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new e.Buf16(320),this.work=new e.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function ie(a){var t;return a&&a.state?(t=a.state,a.total_in=a.total_out=t.total=0,a.msg="",t.wrap&&(a.adler=1&t.wrap),t.mode=v,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new e.Buf32(Y),t.distcode=t.distdyn=new e.Buf32(Z),t.sane=1,t.back=-1,c):b}function se(e){var a;return e&&e.state?((a=e.state).wsize=0,a.whave=0,a.wnext=0,ie(e)):b}function ne(e,a){var t,i;return e&&e.state?(i=e.state,a<0?(t=0,a=-a):(t=1+(a>>4),a<48&&(a&=15)),a&&(a<8||a>15)?b:(null!==i.window&&i.wbits!==a&&(i.window=null),i.wrap=t,i.wbits=a,se(e))):b}function re(e,a){var t,i;return e?(i=new te,e.state=i,i.window=null,(t=ne(e,a))!==c&&(e.state=null),t):b}function oe(e){return re(e,ee)}var de,le,fe=!0;function ce(a){if(fe){var t;for(de=new e.Buf32(512),le=new e.Buf32(32),t=0;t<144;)a.lens[t++]=8;for(;t<256;)a.lens[t++]=9;for(;t<280;)a.lens[t++]=7;for(;t<288;)a.lens[t++]=8;for(s(r,a.lens,0,288,de,0,a.work,{bits:9}),t=0;t<32;)a.lens[t++]=5;s(o,a.lens,0,32,le,0,a.work,{bits:5}),fe=!1}a.lencode=de,a.lenbits=9,a.distcode=le,a.distbits=5}function he(a,t,i,s){var n,r=a.state;return null===r.window&&(r.wsize=1<=r.wsize?(e.arraySet(r.window,t,i-r.wsize,r.wsize,0),r.wnext=0,r.whave=r.wsize):((n=r.wsize-r.wnext)>s&&(n=s),e.arraySet(r.window,t,i-s,n,r.wnext),(s-=n)?(e.arraySet(r.window,t,i-s,s,0),r.wnext=s,r.whave=r.wsize):(r.wnext+=n,r.wnext===r.wsize&&(r.wnext=0),r.whave>>8&255,$.check=t($.check,qe,2,0),oe=0,de=0,$.mode=x;break}if($.flags=0,$.head&&($.head.done=!1),!(1&$.wrap)||(((255&oe)<<8)+(oe>>8))%31){Y.msg="incorrect header check",$.mode=V;break}if((15&oe)!==g){Y.msg="unknown compression method",$.mode=V;break}if(de-=4,_e=8+(15&(oe>>>=4)),0===$.wbits)$.wbits=_e;else if(_e>$.wbits){Y.msg="invalid window size",$.mode=V;break}$.dmax=1<<_e,Y.adler=$.check=1,$.mode=512&oe?C:R,oe=0,de=0;break;case x:for(;de<16;){if(0===ne)break e;ne--,oe+=ee[ie++]<>8&1),512&$.flags&&(qe[0]=255&oe,qe[1]=oe>>>8&255,$.check=t($.check,qe,2,0)),oe=0,de=0,$.mode=p;case p:for(;de<32;){if(0===ne)break e;ne--,oe+=ee[ie++]<>>8&255,qe[2]=oe>>>16&255,qe[3]=oe>>>24&255,$.check=t($.check,qe,4,0)),oe=0,de=0,$.mode=_;case _:for(;de<16;){if(0===ne)break e;ne--,oe+=ee[ie++]<>8),512&$.flags&&(qe[0]=255&oe,qe[1]=oe>>>8&255,$.check=t($.check,qe,2,0)),oe=0,de=0,$.mode=y;case y:if(1024&$.flags){for(;de<16;){if(0===ne)break e;ne--,oe+=ee[ie++]<>>8&255,$.check=t($.check,qe,2,0)),oe=0,de=0}else $.head&&($.head.extra=null);$.mode=z;case z:if(1024&$.flags&&((ke=$.length)>ne&&(ke=ne),ke&&($.head&&(_e=$.head.extra_len-$.length,$.head.extra||($.head.extra=new Array($.head.extra_len)),e.arraySet($.head.extra,ee,ie,ke,_e)),512&$.flags&&($.check=t($.check,ee,ke,ie)),ne-=ke,ie+=ke,$.length-=ke),$.length))break e;$.length=0,$.mode=B;case B:if(2048&$.flags){if(0===ne)break e;ke=0;do{_e=ee[ie+ke++],$.head&&_e&&$.length<65536&&($.head.name+=String.fromCharCode(_e))}while(_e&&ke>9&1,$.head.done=!0),Y.adler=$.check=0,$.mode=R;break;case C:for(;de<32;){if(0===ne)break e;ne--,oe+=ee[ie++]<>>=7&de,de-=7&de,$.mode=Q;break}for(;de<3;){if(0===ne)break e;ne--,oe+=ee[ie++]<>>=1)){case 0:$.mode=A;break;case 1:if(ce($),$.mode=N,Z===f){oe>>>=2,de-=2;break e}break;case 2:$.mode=G;break;case 3:Y.msg="invalid block type",$.mode=V}oe>>>=2,de-=2;break;case A:for(oe>>>=7&de,de-=7&de;de<32;){if(0===ne)break e;ne--,oe+=ee[ie++]<>>16^65535)){Y.msg="invalid stored block lengths",$.mode=V;break}if($.length=65535&oe,oe=0,de=0,$.mode=D,Z===f)break e;case D:$.mode=E;case E:if(ke=$.length){if(ke>ne&&(ke=ne),ke>re&&(ke=re),0===ke)break e;e.arraySet(te,ee,ie,ke,se),ne-=ke,ie+=ke,re-=ke,se+=ke,$.length-=ke;break}$.mode=R;break;case G:for(;de<14;){if(0===ne)break e;ne--,oe+=ee[ie++]<>>=5,de-=5,$.ndist=1+(31&oe),oe>>>=5,de-=5,$.ncode=4+(15&oe),oe>>>=4,de-=4,$.nlen>286||$.ndist>30){Y.msg="too many length or distance symbols",$.mode=V;break}$.have=0,$.mode=H;case H:for(;$.have<$.ncode;){for(;de<3;){if(0===ne)break e;ne--,oe+=ee[ie++]<>>=3,de-=3}for(;$.have<19;)$.lens[Ce[$.have++]]=0;if($.lencode=$.lendyn,$.lenbits=7,ze={bits:$.lenbits},ye=s(n,$.lens,0,19,$.lencode,0,$.work,ze),$.lenbits=ze.bits,ye){Y.msg="invalid code lengths set",$.mode=V;break}$.have=0,$.mode=K;case K:for(;$.have<$.nlen+$.ndist;){for(;ue=(Se=$.lencode[oe&(1<<$.lenbits)-1])>>>16&255,ge=65535&Se,!((we=Se>>>24)<=de);){if(0===ne)break e;ne--,oe+=ee[ie++]<>>=we,de-=we,$.lens[$.have++]=ge;else{if(16===ge){for(Be=we+2;de>>=we,de-=we,0===$.have){Y.msg="invalid bit length repeat",$.mode=V;break}_e=$.lens[$.have-1],ke=3+(3&oe),oe>>>=2,de-=2}else if(17===ge){for(Be=we+3;de>>=we)),oe>>>=3,de-=3}else{for(Be=we+7;de>>=we)),oe>>>=7,de-=7}if($.have+ke>$.nlen+$.ndist){Y.msg="invalid bit length repeat",$.mode=V;break}for(;ke--;)$.lens[$.have++]=_e}}if($.mode===V)break;if(0===$.lens[256]){Y.msg="invalid code -- missing end-of-block",$.mode=V;break}if($.lenbits=9,ze={bits:$.lenbits},ye=s(r,$.lens,0,$.nlen,$.lencode,0,$.work,ze),$.lenbits=ze.bits,ye){Y.msg="invalid literal/lengths set",$.mode=V;break}if($.distbits=6,$.distcode=$.distdyn,ze={bits:$.distbits},ye=s(o,$.lens,$.nlen,$.ndist,$.distcode,0,$.work,ze),$.distbits=ze.bits,ye){Y.msg="invalid distances set",$.mode=V;break}if($.mode=N,Z===f)break e;case N:$.mode=F;case F:if(ne>=6&&re>=258){Y.next_out=se,Y.avail_out=re,Y.next_in=ie,Y.avail_in=ne,$.hold=oe,$.bits=de,i(Y,fe),se=Y.next_out,te=Y.output,re=Y.avail_out,ie=Y.next_in,ee=Y.input,ne=Y.avail_in,oe=$.hold,de=$.bits,$.mode===R&&($.back=-1);break}for($.back=0;ue=(Se=$.lencode[oe&(1<<$.lenbits)-1])>>>16&255,ge=65535&Se,!((we=Se>>>24)<=de);){if(0===ne)break e;ne--,oe+=ee[ie++]<>ve)])>>>16&255,ge=65535&Se,!(ve+(we=Se>>>24)<=de);){if(0===ne)break e;ne--,oe+=ee[ie++]<>>=ve,de-=ve,$.back+=ve}if(oe>>>=we,de-=we,$.back+=we,$.length=ge,0===ue){$.mode=P;break}if(32&ue){$.back=-1,$.mode=R;break}if(64&ue){Y.msg="invalid literal/length code",$.mode=V;break}$.extra=15&ue,$.mode=J;case J:if($.extra){for(Be=$.extra;de>>=$.extra,de-=$.extra,$.back+=$.extra}$.was=$.length,$.mode=L;case L:for(;ue=(Se=$.distcode[oe&(1<<$.distbits)-1])>>>16&255,ge=65535&Se,!((we=Se>>>24)<=de);){if(0===ne)break e;ne--,oe+=ee[ie++]<>ve)])>>>16&255,ge=65535&Se,!(ve+(we=Se>>>24)<=de);){if(0===ne)break e;ne--,oe+=ee[ie++]<>>=ve,de-=ve,$.back+=ve}if(oe>>>=we,de-=we,$.back+=we,64&ue){Y.msg="invalid distance code",$.mode=V;break}$.offset=ge,$.extra=15&ue,$.mode=M;case M:if($.extra){for(Be=$.extra;de>>=$.extra,de-=$.extra,$.back+=$.extra}if($.offset>$.dmax){Y.msg="invalid distance too far back",$.mode=V;break}$.mode=O;case O:if(0===re)break e;if(ke=fe-re,$.offset>ke){if((ke=$.offset-ke)>$.whave&&$.sane){Y.msg="invalid distance too far back",$.mode=V;break}ke>$.wnext?(ke-=$.wnext,be=$.wsize-ke):be=$.wnext-ke,ke>$.length&&(ke=$.length),me=$.window}else me=te,be=se-$.offset,ke=$.length;ke>re&&(ke=re),re-=ke,$.length-=ke;do{te[se++]=me[be++]}while(--ke);0===$.length&&($.mode=F);break;case P:if(0===re)break e;te[se++]=$.length,re--,$.mode=F;break;case Q:if($.wrap){for(;de<32;){if(0===ne)break e;ne--,oe|=ee[ie++]<=0&&u.windowBits<16&&(u.windowBits=-u.windowBits,0===u.windowBits&&(u.windowBits=-15)),!(u.windowBits>=0&&u.windowBits<16)||n&&n.windowBits||(u.windowBits+=32),u.windowBits>15&&u.windowBits<48&&0==(15&u.windowBits)&&(u.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new e,this.strm.avail_out=0;var h=t.inflateInit2(this.strm,u.windowBits);if(h!==s.Z_OK)throw new Error(r[h]);this.header=new o,t.inflateGetHeader(this.strm,this.header)}function h(t,i){var n=new a(i);if(n.push(t,!0),n.err)throw n.msg||r[n.err];return n.result}function _(t,i){return(i=i||{}).raw=!0,h(t,i)}a.prototype.push=function(r,e){var o,a,h,_,w,l,d=this.strm,f=this.options.chunkSize,p=this.options.dictionary,c=!1;if(this.ended)return!1;a=e===~~e?e:!0===e?s.Z_FINISH:s.Z_NO_FLUSH,"string"==typeof r?d.input=n.binstring2buf(r):"[object ArrayBuffer]"===u.call(r)?d.input=new Uint8Array(r):d.input=r,d.next_in=0,d.avail_in=d.input.length;do{if(0===d.avail_out&&(d.output=new i.Buf8(f),d.next_out=0,d.avail_out=f),(o=t.inflate(d,s.Z_NO_FLUSH))===s.Z_NEED_DICT&&p&&(l="string"==typeof p?n.string2buf(p):"[object ArrayBuffer]"===u.call(p)?new Uint8Array(p):p,o=t.inflateSetDictionary(this.strm,l)),o===s.Z_BUF_ERROR&&!0===c&&(o=s.Z_OK,c=!1),o!==s.Z_STREAM_END&&o!==s.Z_OK)return this.onEnd(o),this.ended=!0,!1;d.next_out&&(0!==d.avail_out&&o!==s.Z_STREAM_END&&(0!==d.avail_in||a!==s.Z_FINISH&&a!==s.Z_SYNC_FLUSH)||("string"===this.options.to?(h=n.utf8border(d.output,d.next_out),_=d.next_out-h,w=n.buf2string(d.output,h),d.next_out=_,d.avail_out=f-_,_&&i.arraySet(d.output,d.output,h,_,0),this.onData(w)):this.onData(i.shrinkBuf(d.output,d.next_out)))),0===d.avail_in&&0===d.avail_out&&(c=!0)}while((d.avail_in>0||0===d.avail_out)&&o!==s.Z_STREAM_END);return o===s.Z_STREAM_END&&(a=s.Z_FINISH),a===s.Z_FINISH?(o=t.inflateEnd(this.strm),this.onEnd(o),this.ended=!0,o===s.Z_OK):a!==s.Z_SYNC_FLUSH||(this.onEnd(s.Z_OK),d.avail_out=0,!0)},a.prototype.onData=function(t){this.chunks.push(t)},a.prototype.onEnd=function(t){t===s.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=i.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},exports.Inflate=a,exports.inflate=h,exports.inflateRaw=_,exports.ungzip=h; +},{"./zlib/inflate":187,"./utils/common":178,"./utils/strings":183,"./zlib/constants":179,"./zlib/messages":185,"./zlib/zstream":184,"./zlib/gzheader":188}],175:[function(require,module,exports) { +"use strict";var e=require("./lib/utils/common").assign,i=require("./lib/deflate"),r=require("./lib/inflate"),l=require("./lib/zlib/constants"),s={};e(s,i,r,l),module.exports=s; +},{"./lib/utils/common":178,"./lib/deflate":176,"./lib/inflate":177,"./lib/zlib/constants":179}],154:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.MaybeCompressedDataReader=exports.TextProfileDataSource=void 0;var e=require("pako"),r=t(e);function t(e){if(e&&e.__esModule)return e;var r={};if(null!=e)for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(r[t]=e[t]);return r.default=e,r}var n=function(e,r,t,n){return new(t||(t=Promise))(function(o,i){function s(e){try{u(n.next(e))}catch(e){i(e)}}function a(e){try{u(n.throw(e))}catch(e){i(e)}}function u(e){e.done?o(e.value):new t(function(r){r(e.value)}).then(s,a)}u((n=n.apply(e,r||[])).next())})};class o{constructor(e,r){this.fileName=e,this.contents=r}name(){return n(this,void 0,void 0,function*(){return this.fileName})}readAsArrayBuffer(){return n(this,void 0,void 0,function*(){return new ArrayBuffer(0)})}readAsText(){return n(this,void 0,void 0,function*(){return this.contents})}}exports.TextProfileDataSource=o;class i{constructor(e,t){this.namePromise=e,this.uncompressedData=t.then(e=>n(this,void 0,void 0,function*(){try{return r.inflate(new Uint8Array(e)).buffer}catch(r){return e}}))}name(){return n(this,void 0,void 0,function*(){return yield this.namePromise})}readAsArrayBuffer(){return n(this,void 0,void 0,function*(){return yield this.uncompressedData})}readAsText(){return n(this,void 0,void 0,function*(){const e=yield this.readAsArrayBuffer();let r="";if("undefined"!=typeof TextDecoder){return(new TextDecoder).decode(e)}{const t=new Uint8Array(e);for(let e=0;e{const t=new FileReader;t.addEventListener("loadend",()=>{if(!(t.result instanceof ArrayBuffer))throw new Error("Expected reader.result to be an instance of ArrayBuffer");r(t.result)}),t.readAsArrayBuffer(e)});return new i(Promise.resolve(e.name),r)}static fromArrayBuffer(e,r){return new i(Promise.resolve(e),Promise.resolve(r))}}exports.MaybeCompressedDataReader=i; +},{"pako":175}],148:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.UID=void 0,exports.importFromInstrumentsDeepCopy=a,exports.importFromInstrumentsTrace=w,exports.importRunFromInstrumentsTrace=g,exports.importThreadFromInstrumentsTrace=b,exports.readInstrumentsKeyedArchive=y,exports.decodeUTF8=v;var e=require("../lib/profile"),t=require("../lib/utils"),r=require("../lib/value-formatters"),n=require("./utils"),s=function(e,t,r,n){return new(r||(r=Promise))(function(s,i){function o(e){try{l(n.next(e))}catch(e){i(e)}}function a(e){try{l(n.throw(e))}catch(e){i(e)}}function l(e){e.done?s(e.value):new r(function(t){t(e.value)}).then(o,a)}l((n=n.apply(e,t||[])).next())})};function i(e){const t=e.split("\n").map(e=>e.split("\t")),r=t.shift();if(!r)return[];const n=new Map;for(let e=0;e0;){const e=a.pop();l=Math.max(l,e.endValue),n.leaveFrame(e,l)}return"Bytes Used"in s[0]?n.setValueFormatter(new r.ByteFormatter):("Weight"in s[0]||"Running Time"in s[0])&&n.setValueFormatter(new r.TimeFormatter("milliseconds")),n.build()}function l(e){return s(this,void 0,void 0,function*(){const t={name:e.name,files:new Map,subdirectories:new Map},r=yield new Promise((t,r)=>{e.createReader().readEntries(e=>{t(e)},r)});for(let e of r)if(e.isDirectory){const r=yield l(e);t.subdirectories.set(r.name,r)}else{const r=yield new Promise((t,r)=>{e.file(t,r)});t.files.set(r.name,r)}return t})}function c(e){return n.MaybeCompressedDataReader.fromFile(e).readAsArrayBuffer()}function u(e){return n.MaybeCompressedDataReader.fromFile(e).readAsText()}function f(e,r){const n=(0,t.getOrThrow)(e.subdirectories,"corespace"),s=(0,t.getOrThrow)(n.subdirectories,`run${r}`);return(0,t.getOrThrow)(s.subdirectories,"core")}class h{constructor(e){this.bytePos=0,this.view=new DataView(e)}seek(e){this.bytePos=e}skip(e){this.bytePos+=e}hasMore(){return this.bytePosthis.view.byteLength?0:this.view.getUint8(this.bytePos-1)}readUint32(){return this.bytePos+=4,this.bytePos>this.view.byteLength?0:this.view.getUint32(this.bytePos-4,!0)}readUint48(){return this.bytePos+=6,this.bytePos>this.view.byteLength?0:this.view.getUint32(this.bytePos-6,!0)+this.view.getUint16(this.bytePos-2,!0)*Math.pow(2,32)}readUint64(){return this.bytePos+=8,this.bytePos>this.view.byteLength?0:this.view.getUint32(this.bytePos-8,!0)+this.view.getUint32(this.bytePos-4,!0)*Math.pow(2,32)}}function p(e){return s(this,void 0,void 0,function*(){const r=(0,t.getOrThrow)(e.subdirectories,"stores");for(let e of r.subdirectories.values()){const r=e.files.get("schema.xml");if(!r)continue;const n=yield u(r);if(!/name="time-profile"/.exec(n))continue;const s=new h(yield c((0,t.getOrThrow)(e.files,"bulkstore")));s.readUint32(),s.readUint32(),s.readUint32();const i=s.readUint32(),o=s.readUint32();s.seek(i);const a=[];for(;;){const e=s.readUint48();if(0===e)break;const t=s.readUint32();s.skip(o-6-4-4);const r=s.readUint32();a.push({timestamp:e,threadID:t,backtraceID:r})}return a}throw new Error("Could not find sample list")})}function d(e,r){return s(this,void 0,void 0,function*(){const e=(0,t.getOrThrow)(r.subdirectories,"uniquing"),n=(0,t.getOrThrow)(e.subdirectories,"arrayUniquer"),s=(0,t.getOrThrow)(n.files,"integeruniquer.index"),i=(0,t.getOrThrow)(n.files,"integeruniquer.data"),o=new h(yield c(s)),a=new h(yield c(i));o.seek(32);let l=[];for(;o.hasMore();){const e=o.readUint32()+1048576*o.readUint32();if(0===e)continue;a.seek(e);let t=a.readUint32(),r=[];for(;t--;)r.push(a.readUint64());l.push(r)}return l})}function m(e){return s(this,void 0,void 0,function*(){const r=y(yield c((0,t.getOrThrow)(e.files,"form.template"))),n=r["com.apple.xray.owner.template.version"];let s=1;"com.apple.xray.owner.template"in r&&(s=r["com.apple.xray.owner.template"].get("_selectedRunNumber"));let i=r.$1;"stubInfoByUUID"in r&&(i=Array.from(r.stubInfoByUUID.keys())[0]);const o=r["com.apple.xray.run.data"],a=[];for(let e of o.runNumbers){const r=(0,t.getOrThrow)(o.runData,e),n=(0,t.getOrThrow)(r,"symbolsByPid"),s=new Map;for(let r of n.values()){for(let e of r.symbols){if(!e)continue;const{sourcePath:r,symbolName:n,addressToLine:i}=e;for(let e of i.keys())(0,t.getOrInsert)(s,e,()=>{const s=n||`0x${(0,t.zeroPad)(e.toString(16),16)}`,i={key:`${r}:${s}`,name:s};return r&&(i.file=r),i})}a.push({number:e,addressToFrameMap:s})}}return{version:n,instrument:i,selectedRunNumber:s,runs:a}})}function w(e){return s(this,void 0,void 0,function*(){const t=yield l(e),{version:r,runs:n,instrument:s,selectedRunNumber:i}=yield m(t);if("com.apple.xray.instrument-type.coresampler2"!==s)throw new Error(`The only supported instrument from .trace import is "com.apple.xray.instrument-type.coresampler2". Got ${s}`);console.log("version: ",r),console.log("Importing time profile");const o=[];let a=0;for(let r of n){const{addressToFrameMap:n,number:s}=r,l=yield g({fileName:e.name,tree:t,addressToFrameMap:n,runNumber:s});r.number===i&&(a=o.length+l.indexToView),o.push(...l.profiles)}return{name:e.name,indexToView:a,profiles:o}})}function g(e){return s(this,void 0,void 0,function*(){const{fileName:r,tree:n,addressToFrameMap:s,runNumber:i}=e,o=f(n,i);let a=yield p(o);const l=yield d(a,o),c=new Map;for(let e of a)c.set(e.threadID,(0,t.getOrElse)(c,e.threadID,()=>0)+1);const u=Array.from(c.entries());(0,t.sortBy)(u,e=>-e[1]);const h=u.map(e=>e[0]);return{name:r,indexToView:0,profiles:h.map(e=>b({threadID:e,fileName:r,arrays:l,addressToFrameMap:s,samples:a}))}})}function b(n){let{fileName:s,addressToFrameMap:i,arrays:o,threadID:a,samples:l}=n;const c=new Map;l=l.filter(e=>e.threadID===a);const u=new e.StackListProfileBuilder((0,t.lastOf)(l).timestamp);function f(e,r){const n=i.get(e);if(n)r.push(n);else if(e in o)for(let t of o[e])f(t,r);else{const n={key:e,name:`0x${(0,t.zeroPad)(e.toString(16),16)}`};i.set(e,n),r.push(n)}}u.setName(`${s} - thread ${a}`);let h=null;for(let e of l){const r=(0,t.getOrInsert)(c,e.backtraceID,e=>{const t=[];return f(e,t),t.reverse(),t});if(null===h&&(u.appendSampleWithWeight([],e.timestamp),h=e.timestamp),e.timestamp{switch(e){case"NSTextStorage":case"NSParagraphStyle":case"NSFont":return null;case"PFTSymbolData":{const e=Object.create(null);e.symbolName=t.$0,e.sourcePath=t.$1,e.addressToLine=new Map;for(let r=3;;r+=2){const n=t["$"+r],s=t["$"+(r+1)];if(null==n||null==s)break;e.addressToLine.set(n,s)}return e}case"PFTOwnerData":{const e=Object.create(null);return e.ownerName=t.$0,e.ownerPath=t.$1,e}case"PFTPersistentSymbols":{const e=Object.create(null),r=t.$4;e.threadNames=t.$3,e.symbols=[];for(let n=1;ne)){if(1e5!==e.$version||"NSKeyedArchiver"!==e.$archiver||!S(e.$top)||!U(e.$objects))throw new Error("Invalid keyed archive");"$null"===e.$objects[0]&&(e.$objects[0]=null);for(let r=0;r{if(t instanceof x)return e.$objects[t.index];if(U(t))for(let e=0;ee)){if(S(t)&&t.$class){let n=N(e,t.$class).$classname;switch(n){case"NSDecimalNumberPlaceholder":{let e=t["NS.length"],r=t["NS.exponent"],n=t["NS.mantissa.bo"],s=t["NS.negative"],i=new Uint16Array(new Uint8Array(t["NS.mantissa"]).buffer),o=0;for(let t=0;t>8|(255&e)<<8),o+=e*Math.pow(65536,t)}return o*=Math.pow(10,r),s?-o:o}case"NSData":case"NSMutableData":return t["NS.bytes"]||t["NS.data"];case"NSString":case"NSMutableString":return t["NS.string"]?t["NS.string"]:t["NS.bytes"]?v(t["NS.bytes"]):(console.warn(`Unexpected ${n} format: `,t),null);case"NSArray":case"NSMutableArray":if("NS.objects"in t)return t["NS.objects"];let e=[];for(;;){let r="NS.object."+e.length;if(!(r in t))break;e.push(t[r])}return e;case"_NSKeyedCoderOldStyleArray":{const e=t["NS.count"];let r=[];for(let n=0;n>4){case 0:return this.parseSingleton(e,r);case 1:return this.parseInteger(e,1<(r.push({stack:t.split(";").map(e=>({key:e,name:e})),duration:parseInt(i,10)}),e)),r}function t(t){const i=r(t),n=i.reduce((e,r)=>e+r.duration,0),o=new e.StackListProfileBuilder(n);if(0===i.length)return null;for(let e of i)o.appendSampleWithWeight(e.stack,e.duration);return o.build()} +},{"../lib/profile":139}],150:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.importFromFirefox=l;var e=require("../lib/profile"),t=require("../lib/utils"),r=require("../lib/value-formatters");function l(l){const n=l.profile,s=1===n.threads.length?n.threads[0]:n.threads.filter(e=>"GeckoMain"===e.name)[0],a=new Map;function o(e){let r=e[0];const l=[];for(;null!=r;){const e=s.stackTable.data[r],[t,n]=e;l.push(n),r=t}return l.reverse(),l.map(e=>{const r=s.frameTable.data[e],l=s.stringTable[r[0]],n=/(.*)\s+\((.*?):?(\d+)?\)$/.exec(l);return n?n[2].startsWith("resource:")||"self-hosted"===n[2]||n[2].startsWith("self-hosted:")?null:(0,t.getOrInsert)(a,l,()=>({key:l,name:n[1],file:n[2],line:n[3]?parseInt(n[3]):void 0})):null}).filter(e=>null!=e)}const i=new e.CallTreeProfileBuilder(l.duration);let u=[];for(let e of s.samples.data){const t=o(e),r=e[1];let l=-1;for(let e=0;el;e--)i.leaveFrame(u[e],r);for(let e=l+1;e0?e[1]:"(anonymous)",file:e[2].length>0?e[2]:"(unknown file)",line:parseInt(e[3],10),col:parseInt(e[4],10)};break}case"CODE":switch(e.kind){case"LoadIC":case"StoreIC":case"KeyedStoreIC":case"KeyedLoadIC":case"LoadGlobalIC":case"Handler":r="(IC) "+r;break;case"BytecodeHandler":r="(bytecode) ~"+r;break;case"Stub":r="(stub) "+r;break;case"Builtin":r="(builtin) "+r;break;case"RegExp":r="(regexp) "+r}break;default:r=`(${e.type}) ${r}`}return{key:r,name:r}}function n(n){const s=new e.StackListProfileBuilder,o=new Map;let c=0;(0,t.sortBy)(n.ticks,e=>e.tm);for(let e of n.ticks){const r=[];for(let s=e.s.length-2;s>=0;s-=2){const c=e.s[s];-1!==c&&(c>n.code.length?r.push({key:c,name:`0x${c.toString(16)}`}):r.push((i=c,(0,t.getOrInsert)(o,i,e=>a(n.code[e],n)))))}s.appendSampleWithWeight(r,e.tm-c),c=e.tm}var i;return s.setValueFormatter(new r.TimeFormatter("microseconds")),s.build()} +},{"../lib/profile":139,"../lib/utils":70,"../lib/value-formatters":140}],152:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.importFromLinuxPerf=r;var e=require("../lib/profile"),t=require("../lib/utils"),n=require("../lib/value-formatters");function s(e){const t=e.split("\n").filter(e=>!/^\s*#/.exec(e)),n={command:null,processID:null,threadID:null,time:null,eventType:"",stack:[]},s=t.shift();if(!s)return null;const r=/^(\S.+?)\s+(\d+)(?:\/?(\d+))?\s+/.exec(s);if(!r)return null;n.command=r[1],r[3]?(n.processID=parseInt(r[2],10),n.threadID=parseInt(r[3],10)):n.threadID=parseInt(r[2],10);const l=/\s+(\d+\.\d+):\s+/.exec(s);l&&(n.time=parseFloat(l[1]));const i=/(\S+):\s*$/.exec(s);i&&(n.eventType=i[1]);for(let e of t){const t=/^\s*(\w+)\s*(.+) \((\S*)\)/.exec(e);if(!t)continue;let[,s,r,l]=t;r=r.replace(/\+0x[\da-f]+$/,""),n.stack.push({address:`0x${s}`,symbolName:r,file:l})}return n.stack.reverse(),n}function r(r){const l=new Map;let i=null;const o=r.split("\n\n").map(s);for(let s of o){if(null==s)continue;if(null!=i&&i!=s.eventType)continue;if(null==s.time)continue;i=s.eventType;let r=[];s.command&&r.push(s.command),s.processID&&r.push(`pid: ${s.processID}`),s.threadID&&r.push(`tid: ${s.threadID}`);const o=r.join(" ");(0,t.getOrInsert)(l,o,()=>{const t=new e.StackListProfileBuilder;return t.setName(o),t.setValueFormatter(new n.TimeFormatter("seconds")),t}).appendSampleWithTimestamp(s.stack.map(({symbolName:e,file:t})=>({key:`${e} (${t})`,name:"[unknown]"===e?`??? (${t})`:e,file:t})),s.time)}return 0===l.size?null:{name:1===l.size?Array.from(l.keys())[0]:"",indexToView:0,profiles:Array.from((0,t.itMap)(l.values(),e=>e.build()))}} +},{"../lib/profile":139,"../lib/utils":70,"../lib/value-formatters":140}],153:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.importFromHaskell=l;var e=require("../lib/profile"),r=require("../lib/value-formatters");function t(e,r,l,o,i){if(0===e.ticks&&0===e.entries&&0===e.alloc&&0===e.children.length)return r;let a=r,s=o.get(e.id);l.enterFrame(s,a);for(let r of e.children)a=t(r,a,l,o,i);return a+=i(e),l.leaveFrame(s,a),a}function l(l){const o=new Map;for(let e of l.cost_centres){const r={key:e.id,name:`${e.module}.${e.label}`};e.src_loc.startsWith("<")||(r.file=e.src_loc),o.set(e.id,r)}const i=new e.CallTreeProfileBuilder(l.total_ticks);t(l.profile,0,i,o,e=>e.ticks),i.setValueFormatter(new r.TimeFormatter("milliseconds")),i.setName(`${l.program} time`);const a=new e.CallTreeProfileBuilder(l.total_ticks);return t(l.profile,0,a,o,e=>e.alloc),a.setValueFormatter(new r.ByteFormatter),a.setName(`${l.program} allocation`),{name:l.program,indexToView:0,profiles:[i.build(),a.build()]}} +},{"../lib/profile":139,"../lib/value-formatters":140}],206:[function(require,module,exports) { +"use strict";function n(n,e){for(var r=new Array(arguments.length-1),t=0,l=2,o=!0;l1&&"="===r.charAt(e);)++a;return Math.ceil(3*r.length)/4-a};for(var e=new Array(64),a=new Array(123),t=0;t<64;)a[e[t]=t<26?t+65:t<52?t+71:t<62?t-4:t-59|43]=t++;r.encode=function(r,a,t){for(var n,i=null,o=[],c=0,s=0;a>2],n=(3&h)<<4,s=1;break;case 1:o[c++]=e[n|h>>4],n=(15&h)<<2,s=2;break;case 2:o[c++]=e[n|h>>6],o[c++]=e[63&h],s=0}c>8191&&((i||(i=[])).push(String.fromCharCode.apply(String,o)),c=0)}return s&&(o[c++]=e[n],o[c++]=61,1===s&&(o[c++]=61)),i?(c&&i.push(String.fromCharCode.apply(String,o.slice(0,c))),i.join("")):String.fromCharCode.apply(String,o.slice(0,c))};var n="invalid encoding";r.decode=function(r,e,t){for(var i,o=t,c=0,s=0;s1)break;if(void 0===(h=a[h]))throw Error(n);switch(c){case 0:i=h,c=1;break;case 1:e[t++]=i<<2|(48&h)>>4,i=h,c=2;break;case 2:e[t++]=(15&i)<<4|(60&h)>>2,i=h,c=3;break;case 3:e[t++]=(3&i)<<6|h,c=0}}if(1===c)throw Error(n);return t-o},r.test=function(r){return/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/.test(r)}; +},{}],207:[function(require,module,exports) { +"use strict";function t(){this._listeners={}}module.exports=t,t.prototype.on=function(t,s,e){return(this._listeners[t]||(this._listeners[t]=[])).push({fn:s,ctx:e||this}),this},t.prototype.off=function(t,s){if(void 0===t)this._listeners={};else if(void 0===s)this._listeners[t]=[];else for(var e=this._listeners[t],i=0;i0?0:2147483648,t,r);else if(isNaN(e))n(2143289344,t,r);else if(e>3.4028234663852886e38)n((o<<31|2139095040)>>>0,t,r);else if(e<1.1754943508222875e-38)n((o<<31|Math.round(e/1.401298464324817e-45))>>>0,t,r);else{var u=Math.floor(Math.log(e)/Math.LN2);n((o<<31|u+127<<23|8388607&Math.round(e*Math.pow(2,-u)*8388608))>>>0,t,r)}}function i(n,e,t){var r=n(e,t),o=2*(r>>31)+1,u=r>>>23&255,i=8388607&r;return 255===u?i?NaN:o*(1/0):0===u?1.401298464324817e-45*o*i:o*Math.pow(2,u-150)*(i+8388608)}n.writeFloatLE=u.bind(null,e),n.writeFloatBE=u.bind(null,t),n.readFloatLE=i.bind(null,r),n.readFloatBE=i.bind(null,o)}(),"undefined"!=typeof Float64Array?function(){var e=new Float64Array([-0]),t=new Uint8Array(e.buffer),r=128===t[7];function o(n,r,o){e[0]=n,r[o]=t[0],r[o+1]=t[1],r[o+2]=t[2],r[o+3]=t[3],r[o+4]=t[4],r[o+5]=t[5],r[o+6]=t[6],r[o+7]=t[7]}function u(n,r,o){e[0]=n,r[o]=t[7],r[o+1]=t[6],r[o+2]=t[5],r[o+3]=t[4],r[o+4]=t[3],r[o+5]=t[2],r[o+6]=t[1],r[o+7]=t[0]}function i(n,r){return t[0]=n[r],t[1]=n[r+1],t[2]=n[r+2],t[3]=n[r+3],t[4]=n[r+4],t[5]=n[r+5],t[6]=n[r+6],t[7]=n[r+7],e[0]}function a(n,r){return t[7]=n[r],t[6]=n[r+1],t[5]=n[r+2],t[4]=n[r+3],t[3]=n[r+4],t[2]=n[r+5],t[1]=n[r+6],t[0]=n[r+7],e[0]}n.writeDoubleLE=r?o:u,n.writeDoubleBE=r?u:o,n.readDoubleLE=r?i:a,n.readDoubleBE=r?a:i}():function(){function u(n,e,t,r,o,u){var i=r<0?1:0;if(i&&(r=-r),0===r)n(0,o,u+e),n(1/r>0?0:2147483648,o,u+t);else if(isNaN(r))n(0,o,u+e),n(2146959360,o,u+t);else if(r>1.7976931348623157e308)n(0,o,u+e),n((i<<31|2146435072)>>>0,o,u+t);else{var a;if(r<2.2250738585072014e-308)n((a=r/5e-324)>>>0,o,u+e),n((i<<31|a/4294967296)>>>0,o,u+t);else{var l=Math.floor(Math.log(r)/Math.LN2);1024===l&&(l=1023),n(4503599627370496*(a=r*Math.pow(2,-l))>>>0,o,u+e),n((i<<31|l+1023<<20|1048576*a&1048575)>>>0,o,u+t)}}}function i(n,e,t,r,o){var u=n(r,o+e),i=n(r,o+t),a=2*(i>>31)+1,l=i>>>20&2047,f=4294967296*(1048575&i)+u;return 2047===l?f?NaN:a*(1/0):0===l?5e-324*a*f:a*Math.pow(2,l-1075)*(f+4503599627370496)}n.writeDoubleLE=u.bind(null,e,0,4),n.writeDoubleBE=u.bind(null,t,4,0),n.readDoubleLE=i.bind(null,r,0,4),n.readDoubleBE=i.bind(null,o,4,0)}(),n}function e(n,e,t){e[t]=255&n,e[t+1]=n>>>8&255,e[t+2]=n>>>16&255,e[t+3]=n>>>24}function t(n,e,t){e[t]=n>>>24,e[t+1]=n>>>16&255,e[t+2]=n>>>8&255,e[t+3]=255&n}function r(n,e){return(n[e]|n[e+1]<<8|n[e+2]<<16|n[e+3]<<24)>>>0}function o(n,e){return(n[e]<<24|n[e+1]<<16|n[e+2]<<8|n[e+3])>>>0}module.exports=n(n); +},{}],209:[function(require,module,exports) { +"use strict";function inquire(moduleName){try{var mod=eval("quire".replace(/^/,"re"))(moduleName);if(mod&&(mod.length||Object.keys(mod).length))return mod}catch(e){}return null}module.exports=inquire; +},{}],210:[function(require,module,exports) { +"use strict";var r=exports;r.length=function(r){for(var t=0,n=0,e=0;e191&&e<224?a[i++]=(31&e)<<6|63&r[t++]:e>239&&e<365?(e=((7&e)<<18|(63&r[t++])<<12|(63&r[t++])<<6|63&r[t++])-65536,a[i++]=55296+(e>>10),a[i++]=56320+(1023&e)):a[i++]=(15&e)<<12|(63&r[t++])<<6|63&r[t++],i>8191&&((o||(o=[])).push(String.fromCharCode.apply(String,a)),i=0);return o?(i&&o.push(String.fromCharCode.apply(String,a.slice(0,i))),o.join("")):String.fromCharCode.apply(String,a.slice(0,i))},r.write=function(r,t,n){for(var e,o,a=n,i=0;i>6|192,t[n++]=63&e|128):55296==(64512&e)&&56320==(64512&(o=r.charCodeAt(i+1)))?(e=65536+((1023&e)<<10)+(1023&o),++i,t[n++]=e>>18|240,t[n++]=e>>12&63|128,t[n++]=e>>6&63|128,t[n++]=63&e|128):(t[n++]=e>>12|224,t[n++]=e>>6&63|128,t[n++]=63&e|128);return n-a}; +},{}],211:[function(require,module,exports) { +"use strict";function r(r,n,t){var u=t||8192,e=u>>>1,l=null,c=u;return function(t){if(t<1||t>e)return r(t);c+t>u&&(l=r(u),c=0);var i=n.call(l,c,c+=t);return 7&c&&(c=1+(7|c)),i}}module.exports=r; +},{}],203:[function(require,module,exports) { +"use strict";module.exports=i;var t=require("../util/minimal");function i(t,i){this.lo=t>>>0,this.hi=i>>>0}var o=i.zero=new i(0,0);o.toNumber=function(){return 0},o.zzEncode=o.zzDecode=function(){return this},o.length=function(){return 1};var r=i.zeroHash="\0\0\0\0\0\0\0\0";i.fromNumber=function(t){if(0===t)return o;var r=t<0;r&&(t=-t);var h=t>>>0,n=(t-h)/4294967296>>>0;return r&&(n=~n>>>0,h=~h>>>0,++h>4294967295&&(h=0,++n>4294967295&&(n=0))),new i(h,n)},i.from=function(r){if("number"==typeof r)return i.fromNumber(r);if(t.isString(r)){if(!t.Long)return i.fromNumber(parseInt(r,10));r=t.Long.fromString(r)}return r.low||r.high?new i(r.low>>>0,r.high>>>0):o},i.prototype.toNumber=function(t){if(!t&&this.hi>>>31){var i=1+~this.lo>>>0,o=~this.hi>>>0;return i||(o=o+1>>>0),-(i+4294967296*o)}return this.lo+4294967296*this.hi},i.prototype.toLong=function(i){return t.Long?new t.Long(0|this.lo,0|this.hi,Boolean(i)):{low:0|this.lo,high:0|this.hi,unsigned:Boolean(i)}};var h=String.prototype.charCodeAt;i.fromHash=function(t){return t===r?o:new i((h.call(t,0)|h.call(t,1)<<8|h.call(t,2)<<16|h.call(t,3)<<24)>>>0,(h.call(t,4)|h.call(t,5)<<8|h.call(t,6)<<16|h.call(t,7)<<24)>>>0)},i.prototype.toHash=function(){return String.fromCharCode(255&this.lo,this.lo>>>8&255,this.lo>>>16&255,this.lo>>>24,255&this.hi,this.hi>>>8&255,this.hi>>>16&255,this.hi>>>24)},i.prototype.zzEncode=function(){var t=this.hi>>31;return this.hi=((this.hi<<1|this.lo>>>31)^t)>>>0,this.lo=(this.lo<<1^t)>>>0,this},i.prototype.zzDecode=function(){var t=-(1&this.lo);return this.lo=((this.lo>>>1|this.hi<<31)^t)>>>0,this.hi=(this.hi>>>1^t)>>>0,this},i.prototype.length=function(){var t=this.lo,i=(this.lo>>>28|this.hi<<4)>>>0,o=this.hi>>>24;return 0===o?0===i?t<16384?t<128?1:2:t<2097152?3:4:i<16384?i<128?5:6:i<2097152?7:8:o<128?9:10}; +},{"../util/minimal":201}],212:[function(require,module,exports) { +"use strict";exports.byteLength=u,exports.toByteArray=i,exports.fromByteArray=d;for(var r=[],t=[],e="undefined"!=typeof Uint8Array?Uint8Array:Array,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",o=0,a=n.length;o0)throw new Error("Invalid string. Length must be a multiple of 4");var e=r.indexOf("=");return-1===e&&(e=t),[e,e===t?0:4-e%4]}function u(r){var t=h(r),e=t[0],n=t[1];return 3*(e+n)/4-n}function c(r,t,e){return 3*(t+e)/4-e}function i(r){for(var n,o=h(r),a=o[0],u=o[1],i=new e(c(r,a,u)),f=0,A=u>0?a-4:a,d=0;d>16&255,i[f++]=n>>8&255,i[f++]=255&n;return 2===u&&(n=t[r.charCodeAt(d)]<<2|t[r.charCodeAt(d+1)]>>4,i[f++]=255&n),1===u&&(n=t[r.charCodeAt(d)]<<10|t[r.charCodeAt(d+1)]<<4|t[r.charCodeAt(d+2)]>>2,i[f++]=n>>8&255,i[f++]=255&n),i}function f(t){return r[t>>18&63]+r[t>>12&63]+r[t>>6&63]+r[63&t]}function A(r,t,e){for(var n,o=[],a=t;au?u:h+16383));return 1===o?(e=t[n-1],a.push(r[e>>2]+r[e<<4&63]+"==")):2===o&&(e=(t[n-2]<<8)+t[n-1],a.push(r[e>>10]+r[e>>4&63]+r[e<<2&63]+"=")),a.join("")}t["-".charCodeAt(0)]=62,t["_".charCodeAt(0)]=63; +},{}],213:[function(require,module,exports) { +exports.read=function(a,o,t,r,h){var M,p,w=8*h-r-1,f=(1<>1,i=-7,N=t?h-1:0,n=t?-1:1,s=a[o+N];for(N+=n,M=s&(1<<-i)-1,s>>=-i,i+=w;i>0;M=256*M+a[o+N],N+=n,i-=8);for(p=M&(1<<-i)-1,M>>=-i,i+=r;i>0;p=256*p+a[o+N],N+=n,i-=8);if(0===M)M=1-e;else{if(M===f)return p?NaN:1/0*(s?-1:1);p+=Math.pow(2,r),M-=e}return(s?-1:1)*p*Math.pow(2,M-r)},exports.write=function(a,o,t,r,h,M){var p,w,f,e=8*M-h-1,i=(1<>1,n=23===h?Math.pow(2,-24)-Math.pow(2,-77):0,s=r?0:M-1,u=r?1:-1,l=o<0||0===o&&1/o<0?1:0;for(o=Math.abs(o),isNaN(o)||o===1/0?(w=isNaN(o)?1:0,p=i):(p=Math.floor(Math.log(o)/Math.LN2),o*(f=Math.pow(2,-p))<1&&(p--,f*=2),(o+=p+N>=1?n/f:n*Math.pow(2,1-N))*f>=2&&(p++,f/=2),p+N>=i?(w=0,p=i):p+N>=1?(w=(o*f-1)*Math.pow(2,h),p+=N):(w=o*Math.pow(2,N-1)*Math.pow(2,h),p=0));h>=8;a[t+s]=255&w,s+=u,w/=256,h-=8);for(p=p<0;a[t+s]=255&p,s+=u,p/=256,e-=8);a[t+s-u]|=128*l}; +},{}],214:[function(require,module,exports) { +var r={}.toString;module.exports=Array.isArray||function(t){return"[object Array]"==r.call(t)}; +},{}],204:[function(require,module,exports) { + +var global = arguments[3]; +var t=arguments[3],r=require("base64-js"),e=require("ieee754"),n=require("isarray");function i(){try{var t=new Uint8Array(1);return t.__proto__={__proto__:Uint8Array.prototype,foo:function(){return 42}},42===t.foo()&&"function"==typeof t.subarray&&0===t.subarray(1,1).byteLength}catch(t){return!1}}function o(){return f.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function u(t,r){if(o()=o())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+o().toString(16)+" bytes");return 0|t}function d(t){return+t!=t&&(t=0),f.alloc(+t)}function v(t,r){if(f.isBuffer(t))return t.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(t)||t instanceof ArrayBuffer))return t.byteLength;"string"!=typeof t&&(t=""+t);var e=t.length;if(0===e)return 0;for(var n=!1;;)switch(r){case"ascii":case"latin1":case"binary":return e;case"utf8":case"utf-8":case void 0:return $(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*e;case"hex":return e>>>1;case"base64":return K(t).length;default:if(n)return $(t).length;r=(""+r).toLowerCase(),n=!0}}function E(t,r,e){var n=!1;if((void 0===r||r<0)&&(r=0),r>this.length)return"";if((void 0===e||e>this.length)&&(e=this.length),e<=0)return"";if((e>>>=0)<=(r>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return x(this,r,e);case"utf8":case"utf-8":return Y(this,r,e);case"ascii":return L(this,r,e);case"latin1":case"binary":return D(this,r,e);case"base64":return S(this,r,e);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return C(this,r,e);default:if(n)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),n=!0}}function b(t,r,e){var n=t[r];t[r]=t[e],t[e]=n}function R(t,r,e,n,i){if(0===t.length)return-1;if("string"==typeof e?(n=e,e=0):e>2147483647?e=2147483647:e<-2147483648&&(e=-2147483648),e=+e,isNaN(e)&&(e=i?0:t.length-1),e<0&&(e=t.length+e),e>=t.length){if(i)return-1;e=t.length-1}else if(e<0){if(!i)return-1;e=0}if("string"==typeof r&&(r=f.from(r,n)),f.isBuffer(r))return 0===r.length?-1:_(t,r,e,n,i);if("number"==typeof r)return r&=255,f.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(t,r,e):Uint8Array.prototype.lastIndexOf.call(t,r,e):_(t,[r],e,n,i);throw new TypeError("val must be string, number or Buffer")}function _(t,r,e,n,i){var o,u=1,f=t.length,s=r.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(t.length<2||r.length<2)return-1;u=2,f/=2,s/=2,e/=2}function h(t,r){return 1===u?t[r]:t.readUInt16BE(r*u)}if(i){var a=-1;for(o=e;of&&(e=f-s),o=e;o>=0;o--){for(var c=!0,l=0;li&&(n=i):n=i;var o=r.length;if(o%2!=0)throw new TypeError("Invalid hex string");n>o/2&&(n=o/2);for(var u=0;u239?4:h>223?3:h>191?2:1;if(i+c<=e)switch(c){case 1:h<128&&(a=h);break;case 2:128==(192&(o=t[i+1]))&&(s=(31&h)<<6|63&o)>127&&(a=s);break;case 3:o=t[i+1],u=t[i+2],128==(192&o)&&128==(192&u)&&(s=(15&h)<<12|(63&o)<<6|63&u)>2047&&(s<55296||s>57343)&&(a=s);break;case 4:o=t[i+1],u=t[i+2],f=t[i+3],128==(192&o)&&128==(192&u)&&128==(192&f)&&(s=(15&h)<<18|(63&o)<<12|(63&u)<<6|63&f)>65535&&s<1114112&&(a=s)}null===a?(a=65533,c=1):a>65535&&(a-=65536,n.push(a>>>10&1023|55296),a=56320|1023&a),n.push(a),i+=c}return O(n)}exports.Buffer=f,exports.SlowBuffer=d,exports.INSPECT_MAX_BYTES=50,f.TYPED_ARRAY_SUPPORT=void 0!==t.TYPED_ARRAY_SUPPORT?t.TYPED_ARRAY_SUPPORT:i(),exports.kMaxLength=o(),f.poolSize=8192,f._augment=function(t){return t.__proto__=f.prototype,t},f.from=function(t,r,e){return s(null,t,r,e)},f.TYPED_ARRAY_SUPPORT&&(f.prototype.__proto__=Uint8Array.prototype,f.__proto__=Uint8Array,"undefined"!=typeof Symbol&&Symbol.species&&f[Symbol.species]===f&&Object.defineProperty(f,Symbol.species,{value:null,configurable:!0})),f.alloc=function(t,r,e){return a(null,t,r,e)},f.allocUnsafe=function(t){return c(null,t)},f.allocUnsafeSlow=function(t){return c(null,t)},f.isBuffer=function(t){return!(null==t||!t._isBuffer)},f.compare=function(t,r){if(!f.isBuffer(t)||!f.isBuffer(r))throw new TypeError("Arguments must be Buffers");if(t===r)return 0;for(var e=t.length,n=r.length,i=0,o=Math.min(e,n);i0&&(t=this.toString("hex",0,r).match(/.{2}/g).join(" "),this.length>r&&(t+=" ... ")),""},f.prototype.compare=function(t,r,e,n,i){if(!f.isBuffer(t))throw new TypeError("Argument must be a Buffer");if(void 0===r&&(r=0),void 0===e&&(e=t?t.length:0),void 0===n&&(n=0),void 0===i&&(i=this.length),r<0||e>t.length||n<0||i>this.length)throw new RangeError("out of range index");if(n>=i&&r>=e)return 0;if(n>=i)return-1;if(r>=e)return 1;if(this===t)return 0;for(var o=(i>>>=0)-(n>>>=0),u=(e>>>=0)-(r>>>=0),s=Math.min(o,u),h=this.slice(n,i),a=t.slice(r,e),c=0;ci)&&(e=i),t.length>0&&(e<0||r<0)||r>this.length)throw new RangeError("Attempt to write outside buffer bounds");n||(n="utf8");for(var o=!1;;)switch(n){case"hex":return A(this,t,r,e);case"utf8":case"utf-8":return m(this,t,r,e);case"ascii":return P(this,t,r,e);case"latin1":case"binary":return T(this,t,r,e);case"base64":return B(this,t,r,e);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return U(this,t,r,e);default:if(o)throw new TypeError("Unknown encoding: "+n);n=(""+n).toLowerCase(),o=!0}},f.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var I=4096;function O(t){var r=t.length;if(r<=I)return String.fromCharCode.apply(String,t);for(var e="",n=0;nn)&&(e=n);for(var i="",o=r;oe)throw new RangeError("Trying to access beyond buffer length")}function k(t,r,e,n,i,o){if(!f.isBuffer(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(r>i||rt.length)throw new RangeError("Index out of range")}function N(t,r,e,n){r<0&&(r=65535+r+1);for(var i=0,o=Math.min(t.length-e,2);i>>8*(n?i:1-i)}function z(t,r,e,n){r<0&&(r=4294967295+r+1);for(var i=0,o=Math.min(t.length-e,4);i>>8*(n?i:3-i)&255}function F(t,r,e,n,i,o){if(e+n>t.length)throw new RangeError("Index out of range");if(e<0)throw new RangeError("Index out of range")}function j(t,r,n,i,o){return o||F(t,r,n,4,3.4028234663852886e38,-3.4028234663852886e38),e.write(t,r,n,i,23,4),n+4}function q(t,r,n,i,o){return o||F(t,r,n,8,1.7976931348623157e308,-1.7976931348623157e308),e.write(t,r,n,i,52,8),n+8}f.prototype.slice=function(t,r){var e,n=this.length;if((t=~~t)<0?(t+=n)<0&&(t=0):t>n&&(t=n),(r=void 0===r?n:~~r)<0?(r+=n)<0&&(r=0):r>n&&(r=n),r0&&(i*=256);)n+=this[t+--r]*i;return n},f.prototype.readUInt8=function(t,r){return r||M(t,1,this.length),this[t]},f.prototype.readUInt16LE=function(t,r){return r||M(t,2,this.length),this[t]|this[t+1]<<8},f.prototype.readUInt16BE=function(t,r){return r||M(t,2,this.length),this[t]<<8|this[t+1]},f.prototype.readUInt32LE=function(t,r){return r||M(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},f.prototype.readUInt32BE=function(t,r){return r||M(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},f.prototype.readIntLE=function(t,r,e){t|=0,r|=0,e||M(t,r,this.length);for(var n=this[t],i=1,o=0;++o=(i*=128)&&(n-=Math.pow(2,8*r)),n},f.prototype.readIntBE=function(t,r,e){t|=0,r|=0,e||M(t,r,this.length);for(var n=r,i=1,o=this[t+--n];n>0&&(i*=256);)o+=this[t+--n]*i;return o>=(i*=128)&&(o-=Math.pow(2,8*r)),o},f.prototype.readInt8=function(t,r){return r||M(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},f.prototype.readInt16LE=function(t,r){r||M(t,2,this.length);var e=this[t]|this[t+1]<<8;return 32768&e?4294901760|e:e},f.prototype.readInt16BE=function(t,r){r||M(t,2,this.length);var e=this[t+1]|this[t]<<8;return 32768&e?4294901760|e:e},f.prototype.readInt32LE=function(t,r){return r||M(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},f.prototype.readInt32BE=function(t,r){return r||M(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},f.prototype.readFloatLE=function(t,r){return r||M(t,4,this.length),e.read(this,t,!0,23,4)},f.prototype.readFloatBE=function(t,r){return r||M(t,4,this.length),e.read(this,t,!1,23,4)},f.prototype.readDoubleLE=function(t,r){return r||M(t,8,this.length),e.read(this,t,!0,52,8)},f.prototype.readDoubleBE=function(t,r){return r||M(t,8,this.length),e.read(this,t,!1,52,8)},f.prototype.writeUIntLE=function(t,r,e,n){(t=+t,r|=0,e|=0,n)||k(this,t,r,e,Math.pow(2,8*e)-1,0);var i=1,o=0;for(this[r]=255&t;++o=0&&(o*=256);)this[r+i]=t/o&255;return r+e},f.prototype.writeUInt8=function(t,r,e){return t=+t,r|=0,e||k(this,t,r,1,255,0),f.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),this[r]=255&t,r+1},f.prototype.writeUInt16LE=function(t,r,e){return t=+t,r|=0,e||k(this,t,r,2,65535,0),f.TYPED_ARRAY_SUPPORT?(this[r]=255&t,this[r+1]=t>>>8):N(this,t,r,!0),r+2},f.prototype.writeUInt16BE=function(t,r,e){return t=+t,r|=0,e||k(this,t,r,2,65535,0),f.TYPED_ARRAY_SUPPORT?(this[r]=t>>>8,this[r+1]=255&t):N(this,t,r,!1),r+2},f.prototype.writeUInt32LE=function(t,r,e){return t=+t,r|=0,e||k(this,t,r,4,4294967295,0),f.TYPED_ARRAY_SUPPORT?(this[r+3]=t>>>24,this[r+2]=t>>>16,this[r+1]=t>>>8,this[r]=255&t):z(this,t,r,!0),r+4},f.prototype.writeUInt32BE=function(t,r,e){return t=+t,r|=0,e||k(this,t,r,4,4294967295,0),f.TYPED_ARRAY_SUPPORT?(this[r]=t>>>24,this[r+1]=t>>>16,this[r+2]=t>>>8,this[r+3]=255&t):z(this,t,r,!1),r+4},f.prototype.writeIntLE=function(t,r,e,n){if(t=+t,r|=0,!n){var i=Math.pow(2,8*e-1);k(this,t,r,e,i-1,-i)}var o=0,u=1,f=0;for(this[r]=255&t;++o>0)-f&255;return r+e},f.prototype.writeIntBE=function(t,r,e,n){if(t=+t,r|=0,!n){var i=Math.pow(2,8*e-1);k(this,t,r,e,i-1,-i)}var o=e-1,u=1,f=0;for(this[r+o]=255&t;--o>=0&&(u*=256);)t<0&&0===f&&0!==this[r+o+1]&&(f=1),this[r+o]=(t/u>>0)-f&255;return r+e},f.prototype.writeInt8=function(t,r,e){return t=+t,r|=0,e||k(this,t,r,1,127,-128),f.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),t<0&&(t=255+t+1),this[r]=255&t,r+1},f.prototype.writeInt16LE=function(t,r,e){return t=+t,r|=0,e||k(this,t,r,2,32767,-32768),f.TYPED_ARRAY_SUPPORT?(this[r]=255&t,this[r+1]=t>>>8):N(this,t,r,!0),r+2},f.prototype.writeInt16BE=function(t,r,e){return t=+t,r|=0,e||k(this,t,r,2,32767,-32768),f.TYPED_ARRAY_SUPPORT?(this[r]=t>>>8,this[r+1]=255&t):N(this,t,r,!1),r+2},f.prototype.writeInt32LE=function(t,r,e){return t=+t,r|=0,e||k(this,t,r,4,2147483647,-2147483648),f.TYPED_ARRAY_SUPPORT?(this[r]=255&t,this[r+1]=t>>>8,this[r+2]=t>>>16,this[r+3]=t>>>24):z(this,t,r,!0),r+4},f.prototype.writeInt32BE=function(t,r,e){return t=+t,r|=0,e||k(this,t,r,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),f.TYPED_ARRAY_SUPPORT?(this[r]=t>>>24,this[r+1]=t>>>16,this[r+2]=t>>>8,this[r+3]=255&t):z(this,t,r,!1),r+4},f.prototype.writeFloatLE=function(t,r,e){return j(this,t,r,!0,e)},f.prototype.writeFloatBE=function(t,r,e){return j(this,t,r,!1,e)},f.prototype.writeDoubleLE=function(t,r,e){return q(this,t,r,!0,e)},f.prototype.writeDoubleBE=function(t,r,e){return q(this,t,r,!1,e)},f.prototype.copy=function(t,r,e,n){if(e||(e=0),n||0===n||(n=this.length),r>=t.length&&(r=t.length),r||(r=0),n>0&&n=this.length)throw new RangeError("sourceStart out of bounds");if(n<0)throw new RangeError("sourceEnd out of bounds");n>this.length&&(n=this.length),t.length-r=0;--i)t[i+r]=this[i+e];else if(o<1e3||!f.TYPED_ARRAY_SUPPORT)for(i=0;i>>=0,e=void 0===e?this.length:e>>>0,t||(t=0),"number"==typeof t)for(o=r;o55295&&e<57344){if(!i){if(e>56319){(r-=3)>-1&&o.push(239,191,189);continue}if(u+1===n){(r-=3)>-1&&o.push(239,191,189);continue}i=e;continue}if(e<56320){(r-=3)>-1&&o.push(239,191,189),i=e;continue}e=65536+(i-55296<<10|e-56320)}else i&&(r-=3)>-1&&o.push(239,191,189);if(i=null,e<128){if((r-=1)<0)break;o.push(e)}else if(e<2048){if((r-=2)<0)break;o.push(e>>6|192,63&e|128)}else if(e<65536){if((r-=3)<0)break;o.push(e>>12|224,e>>6&63|128,63&e|128)}else{if(!(e<1114112))throw new Error("Invalid code point");if((r-=4)<0)break;o.push(e>>18|240,e>>12&63|128,e>>6&63|128,63&e|128)}}return o}function G(t){for(var r=[],e=0;e>8,i=e%256,o.push(i),o.push(n);return o}function K(t){return r.toByteArray(X(t))}function Q(t,r,e,n){for(var i=0;i=r.length||i>=t.length);++i)r[i+e]=t[i];return i}function W(t){return t!=t} +},{"base64-js":212,"ieee754":213,"isarray":214,"buffer":204}],201:[function(require,module,exports) { +var global = arguments[3]; +var Buffer = require("buffer").Buffer; +var e=arguments[3],r=require("buffer").Buffer,t=exports;function n(e,r,t){for(var n=Object.keys(r),o=0;o0)},t.Buffer=function(){try{var e=t.inquire("buffer").Buffer;return e.prototype.utf8Write?e:null}catch(e){return null}}(),t._Buffer_from=null,t._Buffer_allocUnsafe=null,t.newBuffer=function(e){return"number"==typeof e?t.Buffer?t._Buffer_allocUnsafe(e):new t.Array(e):t.Buffer?t._Buffer_from(e):"undefined"==typeof Uint8Array?e:new Uint8Array(e)},t.Array="undefined"!=typeof Uint8Array?Uint8Array:Array,t.Long=t.global.dcodeIO&&t.global.dcodeIO.Long||t.global.Long||t.inquire("long"),t.key2Re=/^true|false|0|1$/,t.key32Re=/^-?(?:0|[1-9][0-9]*)$/,t.key64Re=/^(?:[\\x00-\\xff]{8}|-?(?:0|[1-9][0-9]*))$/,t.longToHash=function(e){return e?t.LongBits.from(e).toHash():t.LongBits.zeroHash},t.longFromHash=function(e,r){var n=t.LongBits.fromHash(e);return t.Long?t.Long.fromBits(n.lo,n.hi,r):n.toNumber(Boolean(r))},t.merge=n,t.lcFirst=function(e){return e.charAt(0).toLowerCase()+e.substring(1)},t.newError=o,t.ProtocolError=o("ProtocolError"),t.oneOfGetter=function(e){for(var r={},t=0;t-1;--t)if(1===r[e[t]]&&void 0!==this[e[t]]&&null!==this[e[t]])return e[t]}},t.oneOfSetter=function(e){return function(r){for(var t=0;t127;)i[n++]=127&t|128,t>>>=7;i[n]=t}function a(t,i){this.len=t,this.next=void 0,this.val=i}function f(t,i,n){for(;t.hi;)i[n++]=127&t.lo|128,t.lo=(t.lo>>>7|t.hi<<25)>>>0,t.hi>>>=7;for(;t.lo>127;)i[n++]=127&t.lo|128,t.lo=t.lo>>>7;i[n++]=t.lo}function c(t,i,n){i[n]=255&t,i[n+1]=t>>>8&255,i[n+2]=t>>>16&255,i[n+3]=t>>>24}u.create=i.Buffer?function(){return(u.create=function(){return new t})()}:function(){return new u},u.alloc=function(t){return new i.Array(t)},i.Array!==Array&&(u.alloc=i.pool(u.alloc,i.Array.prototype.subarray)),u.prototype._push=function(t,i,n){return this.tail=this.tail.next=new r(t,i,n),this.len+=i,this},a.prototype=Object.create(r.prototype),a.prototype.fn=p,u.prototype.uint32=function(t){return this.len+=(this.tail=this.tail.next=new a((t>>>=0)<128?1:t<16384?2:t<2097152?3:t<268435456?4:5,t)).len,this},u.prototype.int32=function(t){return t<0?this._push(f,10,n.fromNumber(t)):this.uint32(t)},u.prototype.sint32=function(t){return this.uint32((t<<1^t>>31)>>>0)},u.prototype.uint64=function(t){var i=n.from(t);return this._push(f,i.length(),i)},u.prototype.int64=u.prototype.uint64,u.prototype.sint64=function(t){var i=n.from(t).zzEncode();return this._push(f,i.length(),i)},u.prototype.bool=function(t){return this._push(l,1,t?1:0)},u.prototype.fixed32=function(t){return this._push(c,4,t>>>0)},u.prototype.sfixed32=u.prototype.fixed32,u.prototype.fixed64=function(t){var i=n.from(t);return this._push(c,4,i.lo)._push(c,4,i.hi)},u.prototype.sfixed64=u.prototype.fixed64,u.prototype.float=function(t){return this._push(i.float.writeFloatLE,4,t)},u.prototype.double=function(t){return this._push(i.float.writeDoubleLE,8,t)};var y=i.Array.prototype.set?function(t,i,n){i.set(t,n)}:function(t,i,n){for(var e=0;e>>0;if(!n)return this._push(l,1,0);if(i.isString(t)){var o=u.alloc(n=e.length(t));e.decode(t,o,0),t=o}return this.uint32(n)._push(y,n,t)},u.prototype.string=function(t){var i=o.length(t);return i?this.uint32(i)._push(o.write,i,t):this._push(l,1,0)},u.prototype.fork=function(){return this.states=new h(this),this.head=this.tail=new r(s,0,0),this.len=0,this},u.prototype.reset=function(){return this.states?(this.head=this.states.head,this.tail=this.states.tail,this.len=this.states.len,this.states=this.states.next):(this.head=this.tail=new r(s,0,0),this.len=0),this},u.prototype.ldelim=function(){var t=this.head,i=this.tail,n=this.len;return this.reset().uint32(n),n&&(this.tail.next=t.next,this.tail=i,this.len+=n),this},u.prototype.finish=function(){for(var t=this.head.next,i=this.constructor.alloc(this.len),n=0;t;)t.fn(t.val,i,n),n+=t.len,t=t.next;return i},u._configure=function(i){t=i}; +},{"./util/minimal":201}],194:[function(require,module,exports) { + +"use strict";module.exports=n;var t=require("./writer");(n.prototype=Object.create(t.prototype)).constructor=n;var e=require("./util/minimal"),r=e.Buffer;function n(){t.call(this)}n.alloc=function(t){return(n.alloc=e._Buffer_allocUnsafe)(t)};var i=r&&r.prototype instanceof Uint8Array&&"set"===r.prototype.set.name?function(t,e,r){e.set(t,r)}:function(t,e,r){if(t.copy)t.copy(e,r,0,t.length);else for(var n=0;n>>0;return this.uint32(r),r&&this._push(i,r,t),this},n.prototype.string=function(t){var e=r.byteLength(t);return this.uint32(e),e&&this._push(o,e,t),this}; +},{"./writer":193,"./util/minimal":201}],195:[function(require,module,exports) { +"use strict";module.exports=h;var t,i=require("./util/minimal"),s=i.LongBits,r=i.utf8;function o(t,i){return RangeError("index out of range: "+t.pos+" + "+(i||1)+" > "+t.len)}function h(t){this.buf=t,this.pos=0,this.len=t.length}var n="undefined"!=typeof Uint8Array?function(t){if(t instanceof Uint8Array||Array.isArray(t))return new h(t);throw Error("illegal buffer")}:function(t){if(Array.isArray(t))return new h(t);throw Error("illegal buffer")};function e(){var t=new s(0,0),i=0;if(!(this.len-this.pos>4)){for(;i<3;++i){if(this.pos>=this.len)throw o(this);if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*i)>>>0,this.buf[this.pos++]<128)return t}return t.lo=(t.lo|(127&this.buf[this.pos++])<<7*i)>>>0,t}for(;i<4;++i)if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*i)>>>0,this.buf[this.pos++]<128)return t;if(t.lo=(t.lo|(127&this.buf[this.pos])<<28)>>>0,t.hi=(t.hi|(127&this.buf[this.pos])>>4)>>>0,this.buf[this.pos++]<128)return t;if(i=0,this.len-this.pos>4){for(;i<5;++i)if(t.hi=(t.hi|(127&this.buf[this.pos])<<7*i+3)>>>0,this.buf[this.pos++]<128)return t}else for(;i<5;++i){if(this.pos>=this.len)throw o(this);if(t.hi=(t.hi|(127&this.buf[this.pos])<<7*i+3)>>>0,this.buf[this.pos++]<128)return t}throw Error("invalid varint encoding")}function u(t,i){return(t[i-4]|t[i-3]<<8|t[i-2]<<16|t[i-1]<<24)>>>0}function f(){if(this.pos+8>this.len)throw o(this,8);return new s(u(this.buf,this.pos+=4),u(this.buf,this.pos+=4))}h.create=i.Buffer?function(s){return(h.create=function(s){return i.Buffer.isBuffer(s)?new t(s):n(s)})(s)}:n,h.prototype._slice=i.Array.prototype.subarray||i.Array.prototype.slice,h.prototype.uint32=function(){var t=4294967295;return function(){if(t=(127&this.buf[this.pos])>>>0,this.buf[this.pos++]<128)return t;if(t=(t|(127&this.buf[this.pos])<<7)>>>0,this.buf[this.pos++]<128)return t;if(t=(t|(127&this.buf[this.pos])<<14)>>>0,this.buf[this.pos++]<128)return t;if(t=(t|(127&this.buf[this.pos])<<21)>>>0,this.buf[this.pos++]<128)return t;if(t=(t|(15&this.buf[this.pos])<<28)>>>0,this.buf[this.pos++]<128)return t;if((this.pos+=5)>this.len)throw this.pos=this.len,o(this,10);return t}}(),h.prototype.int32=function(){return 0|this.uint32()},h.prototype.sint32=function(){var t=this.uint32();return t>>>1^-(1&t)|0},h.prototype.bool=function(){return 0!==this.uint32()},h.prototype.fixed32=function(){if(this.pos+4>this.len)throw o(this,4);return u(this.buf,this.pos+=4)},h.prototype.sfixed32=function(){if(this.pos+4>this.len)throw o(this,4);return 0|u(this.buf,this.pos+=4)},h.prototype.float=function(){if(this.pos+4>this.len)throw o(this,4);var t=i.float.readFloatLE(this.buf,this.pos);return this.pos+=4,t},h.prototype.double=function(){if(this.pos+8>this.len)throw o(this,4);var t=i.float.readDoubleLE(this.buf,this.pos);return this.pos+=8,t},h.prototype.bytes=function(){var t=this.uint32(),i=this.pos,s=this.pos+t;if(s>this.len)throw o(this,t);return this.pos+=t,Array.isArray(this.buf)?this.buf.slice(i,s):i===s?new this.buf.constructor(0):this._slice.call(this.buf,i,s)},h.prototype.string=function(){var t=this.bytes();return r.read(t,0,t.length)},h.prototype.skip=function(t){if("number"==typeof t){if(this.pos+t>this.len)throw o(this,t);this.pos+=t}else do{if(this.pos>=this.len)throw o(this)}while(128&this.buf[this.pos++]);return this},h.prototype.skipType=function(t){switch(t){case 0:this.skip();break;case 1:this.skip(8);break;case 2:this.skip(this.uint32());break;case 3:for(;4!=(t=7&this.uint32());)this.skipType(t);break;case 5:this.skip(4);break;default:throw Error("invalid wire type "+t+" at offset "+this.pos)}return this},h._configure=function(s){t=s;var r=i.Long?"toLong":"toNumber";i.merge(h.prototype,{int64:function(){return e.call(this)[r](!1)},uint64:function(){return e.call(this)[r](!0)},sint64:function(){return e.call(this).zzDecode()[r](!1)},fixed64:function(){return f.call(this)[r](!0)},sfixed64:function(){return f.call(this)[r](!1)}})}; +},{"./util/minimal":201}],196:[function(require,module,exports) { +"use strict";module.exports=r;var t=require("./reader");(r.prototype=Object.create(t.prototype)).constructor=r;var e=require("./util/minimal");function r(e){t.call(this,e)}e.Buffer&&(r.prototype._slice=e.Buffer.prototype.slice),r.prototype.string=function(){var t=this.uint32();return this.buf.utf8Slice(this.pos,this.pos=Math.min(this.pos+t,this.len))}; +},{"./reader":195,"./util/minimal":201}],202:[function(require,module,exports) { +"use strict";module.exports=t;var e=require("../util/minimal");function t(t,r,i){if("function"!=typeof t)throw TypeError("rpcImpl must be a function");e.EventEmitter.call(this),this.rpcImpl=t,this.requestDelimited=Boolean(r),this.responseDelimited=Boolean(i)}(t.prototype=Object.create(e.EventEmitter.prototype)).constructor=t,t.prototype.rpcCall=function t(r,i,n,o,l){if(!o)throw TypeError("request must be specified");var u=this;if(!l)return e.asPromise(t,u,r,i,n,o);if(u.rpcImpl)try{return u.rpcImpl(r,i[u.requestDelimited?"encodeDelimited":"encode"](o).finish(),function(e,t){if(e)return u.emit("error",e,r),l(e);if(null!==t){if(!(t instanceof n))try{t=n[u.responseDelimited?"decodeDelimited":"decode"](t)}catch(e){return u.emit("error",e,r),l(e)}return u.emit("data",t,r),l(null,t)}u.end(!0)})}catch(e){return u.emit("error",e,r),void setTimeout(function(){l(e)},0)}else setTimeout(function(){l(Error("already ended"))},0)},t.prototype.end=function(e){return this.rpcImpl&&(e||this.rpcImpl(null,null,null),this.rpcImpl=null,this.emit("end").off()),this}; +},{"../util/minimal":201}],197:[function(require,module,exports) { +"use strict";var e=exports;e.Service=require("./rpc/service"); +},{"./rpc/service":202}],198:[function(require,module,exports) { +"use strict";module.exports={}; +},{}],189:[function(require,module,exports) { +"use strict";var r=exports;function e(){r.Reader._configure(r.BufferReader),r.util._configure()}r.build="minimal",r.Writer=require("./writer"),r.BufferWriter=require("./writer_buffer"),r.Reader=require("./reader"),r.BufferReader=require("./reader_buffer"),r.util=require("./util/minimal"),r.rpc=require("./rpc"),r.roots=require("./roots"),r.configure=e,r.Writer._configure(r.BufferWriter),e(); +},{"./writer":193,"./writer_buffer":194,"./reader":195,"./reader_buffer":196,"./util/minimal":201,"./rpc":197,"./roots":198}],186:[function(require,module,exports) { +"use strict";module.exports=require("./src/index-minimal"); +},{"./src/index-minimal":189}],174:[function(require,module,exports) { +"use strict";var e=require("protobufjs/minimal"),n=e.Reader,t=e.Writer,o=e.util,r=e.roots.default||(e.roots.default={});r.perftools=function(){var i,l={};return l.profiles=((i={}).Profile=function(){function i(e){if(this.sampleType=[],this.sample=[],this.mapping=[],this.location=[],this.function=[],this.stringTable=[],this.comment=[],e)for(var n=Object.keys(e),t=0;t>>3){case 1:i.sampleType&&i.sampleType.length||(i.sampleType=[]),i.sampleType.push(r.perftools.profiles.ValueType.decode(e,e.uint32()));break;case 2:i.sample&&i.sample.length||(i.sample=[]),i.sample.push(r.perftools.profiles.Sample.decode(e,e.uint32()));break;case 3:i.mapping&&i.mapping.length||(i.mapping=[]),i.mapping.push(r.perftools.profiles.Mapping.decode(e,e.uint32()));break;case 4:i.location&&i.location.length||(i.location=[]),i.location.push(r.perftools.profiles.Location.decode(e,e.uint32()));break;case 5:i.function&&i.function.length||(i.function=[]),i.function.push(r.perftools.profiles.Function.decode(e,e.uint32()));break;case 6:i.stringTable&&i.stringTable.length||(i.stringTable=[]),i.stringTable.push(e.string());break;case 7:i.dropFrames=e.int64();break;case 8:i.keepFrames=e.int64();break;case 9:i.timeNanos=e.int64();break;case 10:i.durationNanos=e.int64();break;case 11:i.periodType=r.perftools.profiles.ValueType.decode(e,e.uint32());break;case 12:i.period=e.int64();break;case 13:if(i.comment&&i.comment.length||(i.comment=[]),2==(7&l))for(var s=e.uint32()+e.pos;e.pos>>0,e.dropFrames.high>>>0).toNumber())),null!=e.keepFrames&&(o.Long?(n.keepFrames=o.Long.fromValue(e.keepFrames)).unsigned=!1:"string"==typeof e.keepFrames?n.keepFrames=parseInt(e.keepFrames,10):"number"==typeof e.keepFrames?n.keepFrames=e.keepFrames:"object"==typeof e.keepFrames&&(n.keepFrames=new o.LongBits(e.keepFrames.low>>>0,e.keepFrames.high>>>0).toNumber())),null!=e.timeNanos&&(o.Long?(n.timeNanos=o.Long.fromValue(e.timeNanos)).unsigned=!1:"string"==typeof e.timeNanos?n.timeNanos=parseInt(e.timeNanos,10):"number"==typeof e.timeNanos?n.timeNanos=e.timeNanos:"object"==typeof e.timeNanos&&(n.timeNanos=new o.LongBits(e.timeNanos.low>>>0,e.timeNanos.high>>>0).toNumber())),null!=e.durationNanos&&(o.Long?(n.durationNanos=o.Long.fromValue(e.durationNanos)).unsigned=!1:"string"==typeof e.durationNanos?n.durationNanos=parseInt(e.durationNanos,10):"number"==typeof e.durationNanos?n.durationNanos=e.durationNanos:"object"==typeof e.durationNanos&&(n.durationNanos=new o.LongBits(e.durationNanos.low>>>0,e.durationNanos.high>>>0).toNumber())),null!=e.periodType){if("object"!=typeof e.periodType)throw TypeError(".perftools.profiles.Profile.periodType: object expected");n.periodType=r.perftools.profiles.ValueType.fromObject(e.periodType)}if(null!=e.period&&(o.Long?(n.period=o.Long.fromValue(e.period)).unsigned=!1:"string"==typeof e.period?n.period=parseInt(e.period,10):"number"==typeof e.period?n.period=e.period:"object"==typeof e.period&&(n.period=new o.LongBits(e.period.low>>>0,e.period.high>>>0).toNumber())),e.comment){if(!Array.isArray(e.comment))throw TypeError(".perftools.profiles.Profile.comment: array expected");for(n.comment=[],t=0;t>>0,e.comment[t].high>>>0).toNumber())}return null!=e.defaultSampleType&&(o.Long?(n.defaultSampleType=o.Long.fromValue(e.defaultSampleType)).unsigned=!1:"string"==typeof e.defaultSampleType?n.defaultSampleType=parseInt(e.defaultSampleType,10):"number"==typeof e.defaultSampleType?n.defaultSampleType=e.defaultSampleType:"object"==typeof e.defaultSampleType&&(n.defaultSampleType=new o.LongBits(e.defaultSampleType.low>>>0,e.defaultSampleType.high>>>0).toNumber())),n},i.toObject=function(e,n){n||(n={});var t={};if((n.arrays||n.defaults)&&(t.sampleType=[],t.sample=[],t.mapping=[],t.location=[],t.function=[],t.stringTable=[],t.comment=[]),n.defaults){if(o.Long){var i=new o.Long(0,0,!1);t.dropFrames=n.longs===String?i.toString():n.longs===Number?i.toNumber():i}else t.dropFrames=n.longs===String?"0":0;o.Long?(i=new o.Long(0,0,!1),t.keepFrames=n.longs===String?i.toString():n.longs===Number?i.toNumber():i):t.keepFrames=n.longs===String?"0":0,o.Long?(i=new o.Long(0,0,!1),t.timeNanos=n.longs===String?i.toString():n.longs===Number?i.toNumber():i):t.timeNanos=n.longs===String?"0":0,o.Long?(i=new o.Long(0,0,!1),t.durationNanos=n.longs===String?i.toString():n.longs===Number?i.toNumber():i):t.durationNanos=n.longs===String?"0":0,t.periodType=null,o.Long?(i=new o.Long(0,0,!1),t.period=n.longs===String?i.toString():n.longs===Number?i.toNumber():i):t.period=n.longs===String?"0":0,o.Long?(i=new o.Long(0,0,!1),t.defaultSampleType=n.longs===String?i.toString():n.longs===Number?i.toNumber():i):t.defaultSampleType=n.longs===String?"0":0}if(e.sampleType&&e.sampleType.length){t.sampleType=[];for(var l=0;l>>0,e.dropFrames.high>>>0).toNumber():e.dropFrames),null!=e.keepFrames&&e.hasOwnProperty("keepFrames")&&("number"==typeof e.keepFrames?t.keepFrames=n.longs===String?String(e.keepFrames):e.keepFrames:t.keepFrames=n.longs===String?o.Long.prototype.toString.call(e.keepFrames):n.longs===Number?new o.LongBits(e.keepFrames.low>>>0,e.keepFrames.high>>>0).toNumber():e.keepFrames),null!=e.timeNanos&&e.hasOwnProperty("timeNanos")&&("number"==typeof e.timeNanos?t.timeNanos=n.longs===String?String(e.timeNanos):e.timeNanos:t.timeNanos=n.longs===String?o.Long.prototype.toString.call(e.timeNanos):n.longs===Number?new o.LongBits(e.timeNanos.low>>>0,e.timeNanos.high>>>0).toNumber():e.timeNanos),null!=e.durationNanos&&e.hasOwnProperty("durationNanos")&&("number"==typeof e.durationNanos?t.durationNanos=n.longs===String?String(e.durationNanos):e.durationNanos:t.durationNanos=n.longs===String?o.Long.prototype.toString.call(e.durationNanos):n.longs===Number?new o.LongBits(e.durationNanos.low>>>0,e.durationNanos.high>>>0).toNumber():e.durationNanos),null!=e.periodType&&e.hasOwnProperty("periodType")&&(t.periodType=r.perftools.profiles.ValueType.toObject(e.periodType,n)),null!=e.period&&e.hasOwnProperty("period")&&("number"==typeof e.period?t.period=n.longs===String?String(e.period):e.period:t.period=n.longs===String?o.Long.prototype.toString.call(e.period):n.longs===Number?new o.LongBits(e.period.low>>>0,e.period.high>>>0).toNumber():e.period),e.comment&&e.comment.length)for(t.comment=[],l=0;l>>0,e.comment[l].high>>>0).toNumber():e.comment[l];return null!=e.defaultSampleType&&e.hasOwnProperty("defaultSampleType")&&("number"==typeof e.defaultSampleType?t.defaultSampleType=n.longs===String?String(e.defaultSampleType):e.defaultSampleType:t.defaultSampleType=n.longs===String?o.Long.prototype.toString.call(e.defaultSampleType):n.longs===Number?new o.LongBits(e.defaultSampleType.low>>>0,e.defaultSampleType.high>>>0).toNumber():e.defaultSampleType),t},i.prototype.toJSON=function(){return this.constructor.toObject(this,e.util.toJSONOptions)},i}(),i.ValueType=function(){function i(e){if(e)for(var n=Object.keys(e),t=0;t>>3){case 1:i.type=e.int64();break;case 2:i.unit=e.int64();break;default:e.skipType(7&l)}}return i},i.decodeDelimited=function(e){return e instanceof n||(e=new n(e)),this.decode(e,e.uint32())},i.verify=function(e){return"object"!=typeof e||null===e?"object expected":null!=e.type&&e.hasOwnProperty("type")&&!(o.isInteger(e.type)||e.type&&o.isInteger(e.type.low)&&o.isInteger(e.type.high))?"type: integer|Long expected":null!=e.unit&&e.hasOwnProperty("unit")&&!(o.isInteger(e.unit)||e.unit&&o.isInteger(e.unit.low)&&o.isInteger(e.unit.high))?"unit: integer|Long expected":null},i.fromObject=function(e){if(e instanceof r.perftools.profiles.ValueType)return e;var n=new r.perftools.profiles.ValueType;return null!=e.type&&(o.Long?(n.type=o.Long.fromValue(e.type)).unsigned=!1:"string"==typeof e.type?n.type=parseInt(e.type,10):"number"==typeof e.type?n.type=e.type:"object"==typeof e.type&&(n.type=new o.LongBits(e.type.low>>>0,e.type.high>>>0).toNumber())),null!=e.unit&&(o.Long?(n.unit=o.Long.fromValue(e.unit)).unsigned=!1:"string"==typeof e.unit?n.unit=parseInt(e.unit,10):"number"==typeof e.unit?n.unit=e.unit:"object"==typeof e.unit&&(n.unit=new o.LongBits(e.unit.low>>>0,e.unit.high>>>0).toNumber())),n},i.toObject=function(e,n){n||(n={});var t={};if(n.defaults){if(o.Long){var r=new o.Long(0,0,!1);t.type=n.longs===String?r.toString():n.longs===Number?r.toNumber():r}else t.type=n.longs===String?"0":0;o.Long?(r=new o.Long(0,0,!1),t.unit=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.unit=n.longs===String?"0":0}return null!=e.type&&e.hasOwnProperty("type")&&("number"==typeof e.type?t.type=n.longs===String?String(e.type):e.type:t.type=n.longs===String?o.Long.prototype.toString.call(e.type):n.longs===Number?new o.LongBits(e.type.low>>>0,e.type.high>>>0).toNumber():e.type),null!=e.unit&&e.hasOwnProperty("unit")&&("number"==typeof e.unit?t.unit=n.longs===String?String(e.unit):e.unit:t.unit=n.longs===String?o.Long.prototype.toString.call(e.unit):n.longs===Number?new o.LongBits(e.unit.low>>>0,e.unit.high>>>0).toNumber():e.unit),t},i.prototype.toJSON=function(){return this.constructor.toObject(this,e.util.toJSONOptions)},i}(),i.Sample=function(){function i(e){if(this.locationId=[],this.value=[],this.label=[],e)for(var n=Object.keys(e),t=0;t>>3){case 1:if(i.locationId&&i.locationId.length||(i.locationId=[]),2==(7&l))for(var s=e.uint32()+e.pos;e.pos>>0,e.locationId[t].high>>>0).toNumber(!0))}if(e.value){if(!Array.isArray(e.value))throw TypeError(".perftools.profiles.Sample.value: array expected");for(n.value=[],t=0;t>>0,e.value[t].high>>>0).toNumber())}if(e.label){if(!Array.isArray(e.label))throw TypeError(".perftools.profiles.Sample.label: array expected");for(n.label=[],t=0;t>>0,e.locationId[i].high>>>0).toNumber(!0):e.locationId[i]}if(e.value&&e.value.length)for(t.value=[],i=0;i>>0,e.value[i].high>>>0).toNumber():e.value[i];if(e.label&&e.label.length)for(t.label=[],i=0;i>>3){case 1:i.key=e.int64();break;case 2:i.str=e.int64();break;case 3:i.num=e.int64();break;case 4:i.numUnit=e.int64();break;default:e.skipType(7&l)}}return i},i.decodeDelimited=function(e){return e instanceof n||(e=new n(e)),this.decode(e,e.uint32())},i.verify=function(e){return"object"!=typeof e||null===e?"object expected":null!=e.key&&e.hasOwnProperty("key")&&!(o.isInteger(e.key)||e.key&&o.isInteger(e.key.low)&&o.isInteger(e.key.high))?"key: integer|Long expected":null!=e.str&&e.hasOwnProperty("str")&&!(o.isInteger(e.str)||e.str&&o.isInteger(e.str.low)&&o.isInteger(e.str.high))?"str: integer|Long expected":null!=e.num&&e.hasOwnProperty("num")&&!(o.isInteger(e.num)||e.num&&o.isInteger(e.num.low)&&o.isInteger(e.num.high))?"num: integer|Long expected":null!=e.numUnit&&e.hasOwnProperty("numUnit")&&!(o.isInteger(e.numUnit)||e.numUnit&&o.isInteger(e.numUnit.low)&&o.isInteger(e.numUnit.high))?"numUnit: integer|Long expected":null},i.fromObject=function(e){if(e instanceof r.perftools.profiles.Label)return e;var n=new r.perftools.profiles.Label;return null!=e.key&&(o.Long?(n.key=o.Long.fromValue(e.key)).unsigned=!1:"string"==typeof e.key?n.key=parseInt(e.key,10):"number"==typeof e.key?n.key=e.key:"object"==typeof e.key&&(n.key=new o.LongBits(e.key.low>>>0,e.key.high>>>0).toNumber())),null!=e.str&&(o.Long?(n.str=o.Long.fromValue(e.str)).unsigned=!1:"string"==typeof e.str?n.str=parseInt(e.str,10):"number"==typeof e.str?n.str=e.str:"object"==typeof e.str&&(n.str=new o.LongBits(e.str.low>>>0,e.str.high>>>0).toNumber())),null!=e.num&&(o.Long?(n.num=o.Long.fromValue(e.num)).unsigned=!1:"string"==typeof e.num?n.num=parseInt(e.num,10):"number"==typeof e.num?n.num=e.num:"object"==typeof e.num&&(n.num=new o.LongBits(e.num.low>>>0,e.num.high>>>0).toNumber())),null!=e.numUnit&&(o.Long?(n.numUnit=o.Long.fromValue(e.numUnit)).unsigned=!1:"string"==typeof e.numUnit?n.numUnit=parseInt(e.numUnit,10):"number"==typeof e.numUnit?n.numUnit=e.numUnit:"object"==typeof e.numUnit&&(n.numUnit=new o.LongBits(e.numUnit.low>>>0,e.numUnit.high>>>0).toNumber())),n},i.toObject=function(e,n){n||(n={});var t={};if(n.defaults){if(o.Long){var r=new o.Long(0,0,!1);t.key=n.longs===String?r.toString():n.longs===Number?r.toNumber():r}else t.key=n.longs===String?"0":0;o.Long?(r=new o.Long(0,0,!1),t.str=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.str=n.longs===String?"0":0,o.Long?(r=new o.Long(0,0,!1),t.num=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.num=n.longs===String?"0":0,o.Long?(r=new o.Long(0,0,!1),t.numUnit=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.numUnit=n.longs===String?"0":0}return null!=e.key&&e.hasOwnProperty("key")&&("number"==typeof e.key?t.key=n.longs===String?String(e.key):e.key:t.key=n.longs===String?o.Long.prototype.toString.call(e.key):n.longs===Number?new o.LongBits(e.key.low>>>0,e.key.high>>>0).toNumber():e.key),null!=e.str&&e.hasOwnProperty("str")&&("number"==typeof e.str?t.str=n.longs===String?String(e.str):e.str:t.str=n.longs===String?o.Long.prototype.toString.call(e.str):n.longs===Number?new o.LongBits(e.str.low>>>0,e.str.high>>>0).toNumber():e.str),null!=e.num&&e.hasOwnProperty("num")&&("number"==typeof e.num?t.num=n.longs===String?String(e.num):e.num:t.num=n.longs===String?o.Long.prototype.toString.call(e.num):n.longs===Number?new o.LongBits(e.num.low>>>0,e.num.high>>>0).toNumber():e.num),null!=e.numUnit&&e.hasOwnProperty("numUnit")&&("number"==typeof e.numUnit?t.numUnit=n.longs===String?String(e.numUnit):e.numUnit:t.numUnit=n.longs===String?o.Long.prototype.toString.call(e.numUnit):n.longs===Number?new o.LongBits(e.numUnit.low>>>0,e.numUnit.high>>>0).toNumber():e.numUnit),t},i.prototype.toJSON=function(){return this.constructor.toObject(this,e.util.toJSONOptions)},i}(),i.Mapping=function(){function i(e){if(e)for(var n=Object.keys(e),t=0;t>>3){case 1:i.id=e.uint64();break;case 2:i.memoryStart=e.uint64();break;case 3:i.memoryLimit=e.uint64();break;case 4:i.fileOffset=e.uint64();break;case 5:i.filename=e.int64();break;case 6:i.buildId=e.int64();break;case 7:i.hasFunctions=e.bool();break;case 8:i.hasFilenames=e.bool();break;case 9:i.hasLineNumbers=e.bool();break;case 10:i.hasInlineFrames=e.bool();break;default:e.skipType(7&l)}}return i},i.decodeDelimited=function(e){return e instanceof n||(e=new n(e)),this.decode(e,e.uint32())},i.verify=function(e){return"object"!=typeof e||null===e?"object expected":null!=e.id&&e.hasOwnProperty("id")&&!(o.isInteger(e.id)||e.id&&o.isInteger(e.id.low)&&o.isInteger(e.id.high))?"id: integer|Long expected":null!=e.memoryStart&&e.hasOwnProperty("memoryStart")&&!(o.isInteger(e.memoryStart)||e.memoryStart&&o.isInteger(e.memoryStart.low)&&o.isInteger(e.memoryStart.high))?"memoryStart: integer|Long expected":null!=e.memoryLimit&&e.hasOwnProperty("memoryLimit")&&!(o.isInteger(e.memoryLimit)||e.memoryLimit&&o.isInteger(e.memoryLimit.low)&&o.isInteger(e.memoryLimit.high))?"memoryLimit: integer|Long expected":null!=e.fileOffset&&e.hasOwnProperty("fileOffset")&&!(o.isInteger(e.fileOffset)||e.fileOffset&&o.isInteger(e.fileOffset.low)&&o.isInteger(e.fileOffset.high))?"fileOffset: integer|Long expected":null!=e.filename&&e.hasOwnProperty("filename")&&!(o.isInteger(e.filename)||e.filename&&o.isInteger(e.filename.low)&&o.isInteger(e.filename.high))?"filename: integer|Long expected":null!=e.buildId&&e.hasOwnProperty("buildId")&&!(o.isInteger(e.buildId)||e.buildId&&o.isInteger(e.buildId.low)&&o.isInteger(e.buildId.high))?"buildId: integer|Long expected":null!=e.hasFunctions&&e.hasOwnProperty("hasFunctions")&&"boolean"!=typeof e.hasFunctions?"hasFunctions: boolean expected":null!=e.hasFilenames&&e.hasOwnProperty("hasFilenames")&&"boolean"!=typeof e.hasFilenames?"hasFilenames: boolean expected":null!=e.hasLineNumbers&&e.hasOwnProperty("hasLineNumbers")&&"boolean"!=typeof e.hasLineNumbers?"hasLineNumbers: boolean expected":null!=e.hasInlineFrames&&e.hasOwnProperty("hasInlineFrames")&&"boolean"!=typeof e.hasInlineFrames?"hasInlineFrames: boolean expected":null},i.fromObject=function(e){if(e instanceof r.perftools.profiles.Mapping)return e;var n=new r.perftools.profiles.Mapping;return null!=e.id&&(o.Long?(n.id=o.Long.fromValue(e.id)).unsigned=!0:"string"==typeof e.id?n.id=parseInt(e.id,10):"number"==typeof e.id?n.id=e.id:"object"==typeof e.id&&(n.id=new o.LongBits(e.id.low>>>0,e.id.high>>>0).toNumber(!0))),null!=e.memoryStart&&(o.Long?(n.memoryStart=o.Long.fromValue(e.memoryStart)).unsigned=!0:"string"==typeof e.memoryStart?n.memoryStart=parseInt(e.memoryStart,10):"number"==typeof e.memoryStart?n.memoryStart=e.memoryStart:"object"==typeof e.memoryStart&&(n.memoryStart=new o.LongBits(e.memoryStart.low>>>0,e.memoryStart.high>>>0).toNumber(!0))),null!=e.memoryLimit&&(o.Long?(n.memoryLimit=o.Long.fromValue(e.memoryLimit)).unsigned=!0:"string"==typeof e.memoryLimit?n.memoryLimit=parseInt(e.memoryLimit,10):"number"==typeof e.memoryLimit?n.memoryLimit=e.memoryLimit:"object"==typeof e.memoryLimit&&(n.memoryLimit=new o.LongBits(e.memoryLimit.low>>>0,e.memoryLimit.high>>>0).toNumber(!0))),null!=e.fileOffset&&(o.Long?(n.fileOffset=o.Long.fromValue(e.fileOffset)).unsigned=!0:"string"==typeof e.fileOffset?n.fileOffset=parseInt(e.fileOffset,10):"number"==typeof e.fileOffset?n.fileOffset=e.fileOffset:"object"==typeof e.fileOffset&&(n.fileOffset=new o.LongBits(e.fileOffset.low>>>0,e.fileOffset.high>>>0).toNumber(!0))),null!=e.filename&&(o.Long?(n.filename=o.Long.fromValue(e.filename)).unsigned=!1:"string"==typeof e.filename?n.filename=parseInt(e.filename,10):"number"==typeof e.filename?n.filename=e.filename:"object"==typeof e.filename&&(n.filename=new o.LongBits(e.filename.low>>>0,e.filename.high>>>0).toNumber())),null!=e.buildId&&(o.Long?(n.buildId=o.Long.fromValue(e.buildId)).unsigned=!1:"string"==typeof e.buildId?n.buildId=parseInt(e.buildId,10):"number"==typeof e.buildId?n.buildId=e.buildId:"object"==typeof e.buildId&&(n.buildId=new o.LongBits(e.buildId.low>>>0,e.buildId.high>>>0).toNumber())),null!=e.hasFunctions&&(n.hasFunctions=Boolean(e.hasFunctions)),null!=e.hasFilenames&&(n.hasFilenames=Boolean(e.hasFilenames)),null!=e.hasLineNumbers&&(n.hasLineNumbers=Boolean(e.hasLineNumbers)),null!=e.hasInlineFrames&&(n.hasInlineFrames=Boolean(e.hasInlineFrames)),n},i.toObject=function(e,n){n||(n={});var t={};if(n.defaults){if(o.Long){var r=new o.Long(0,0,!0);t.id=n.longs===String?r.toString():n.longs===Number?r.toNumber():r}else t.id=n.longs===String?"0":0;o.Long?(r=new o.Long(0,0,!0),t.memoryStart=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.memoryStart=n.longs===String?"0":0,o.Long?(r=new o.Long(0,0,!0),t.memoryLimit=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.memoryLimit=n.longs===String?"0":0,o.Long?(r=new o.Long(0,0,!0),t.fileOffset=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.fileOffset=n.longs===String?"0":0,o.Long?(r=new o.Long(0,0,!1),t.filename=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.filename=n.longs===String?"0":0,o.Long?(r=new o.Long(0,0,!1),t.buildId=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.buildId=n.longs===String?"0":0,t.hasFunctions=!1,t.hasFilenames=!1,t.hasLineNumbers=!1,t.hasInlineFrames=!1}return null!=e.id&&e.hasOwnProperty("id")&&("number"==typeof e.id?t.id=n.longs===String?String(e.id):e.id:t.id=n.longs===String?o.Long.prototype.toString.call(e.id):n.longs===Number?new o.LongBits(e.id.low>>>0,e.id.high>>>0).toNumber(!0):e.id),null!=e.memoryStart&&e.hasOwnProperty("memoryStart")&&("number"==typeof e.memoryStart?t.memoryStart=n.longs===String?String(e.memoryStart):e.memoryStart:t.memoryStart=n.longs===String?o.Long.prototype.toString.call(e.memoryStart):n.longs===Number?new o.LongBits(e.memoryStart.low>>>0,e.memoryStart.high>>>0).toNumber(!0):e.memoryStart),null!=e.memoryLimit&&e.hasOwnProperty("memoryLimit")&&("number"==typeof e.memoryLimit?t.memoryLimit=n.longs===String?String(e.memoryLimit):e.memoryLimit:t.memoryLimit=n.longs===String?o.Long.prototype.toString.call(e.memoryLimit):n.longs===Number?new o.LongBits(e.memoryLimit.low>>>0,e.memoryLimit.high>>>0).toNumber(!0):e.memoryLimit),null!=e.fileOffset&&e.hasOwnProperty("fileOffset")&&("number"==typeof e.fileOffset?t.fileOffset=n.longs===String?String(e.fileOffset):e.fileOffset:t.fileOffset=n.longs===String?o.Long.prototype.toString.call(e.fileOffset):n.longs===Number?new o.LongBits(e.fileOffset.low>>>0,e.fileOffset.high>>>0).toNumber(!0):e.fileOffset),null!=e.filename&&e.hasOwnProperty("filename")&&("number"==typeof e.filename?t.filename=n.longs===String?String(e.filename):e.filename:t.filename=n.longs===String?o.Long.prototype.toString.call(e.filename):n.longs===Number?new o.LongBits(e.filename.low>>>0,e.filename.high>>>0).toNumber():e.filename),null!=e.buildId&&e.hasOwnProperty("buildId")&&("number"==typeof e.buildId?t.buildId=n.longs===String?String(e.buildId):e.buildId:t.buildId=n.longs===String?o.Long.prototype.toString.call(e.buildId):n.longs===Number?new o.LongBits(e.buildId.low>>>0,e.buildId.high>>>0).toNumber():e.buildId),null!=e.hasFunctions&&e.hasOwnProperty("hasFunctions")&&(t.hasFunctions=e.hasFunctions),null!=e.hasFilenames&&e.hasOwnProperty("hasFilenames")&&(t.hasFilenames=e.hasFilenames),null!=e.hasLineNumbers&&e.hasOwnProperty("hasLineNumbers")&&(t.hasLineNumbers=e.hasLineNumbers),null!=e.hasInlineFrames&&e.hasOwnProperty("hasInlineFrames")&&(t.hasInlineFrames=e.hasInlineFrames),t},i.prototype.toJSON=function(){return this.constructor.toObject(this,e.util.toJSONOptions)},i}(),i.Location=function(){function i(e){if(this.line=[],e)for(var n=Object.keys(e),t=0;t>>3){case 1:i.id=e.uint64();break;case 2:i.mappingId=e.uint64();break;case 3:i.address=e.uint64();break;case 4:i.line&&i.line.length||(i.line=[]),i.line.push(r.perftools.profiles.Line.decode(e,e.uint32()));break;case 5:i.isFolded=e.bool();break;default:e.skipType(7&l)}}return i},i.decodeDelimited=function(e){return e instanceof n||(e=new n(e)),this.decode(e,e.uint32())},i.verify=function(e){if("object"!=typeof e||null===e)return"object expected";if(null!=e.id&&e.hasOwnProperty("id")&&!(o.isInteger(e.id)||e.id&&o.isInteger(e.id.low)&&o.isInteger(e.id.high)))return"id: integer|Long expected";if(null!=e.mappingId&&e.hasOwnProperty("mappingId")&&!(o.isInteger(e.mappingId)||e.mappingId&&o.isInteger(e.mappingId.low)&&o.isInteger(e.mappingId.high)))return"mappingId: integer|Long expected";if(null!=e.address&&e.hasOwnProperty("address")&&!(o.isInteger(e.address)||e.address&&o.isInteger(e.address.low)&&o.isInteger(e.address.high)))return"address: integer|Long expected";if(null!=e.line&&e.hasOwnProperty("line")){if(!Array.isArray(e.line))return"line: array expected";for(var n=0;n>>0,e.id.high>>>0).toNumber(!0))),null!=e.mappingId&&(o.Long?(n.mappingId=o.Long.fromValue(e.mappingId)).unsigned=!0:"string"==typeof e.mappingId?n.mappingId=parseInt(e.mappingId,10):"number"==typeof e.mappingId?n.mappingId=e.mappingId:"object"==typeof e.mappingId&&(n.mappingId=new o.LongBits(e.mappingId.low>>>0,e.mappingId.high>>>0).toNumber(!0))),null!=e.address&&(o.Long?(n.address=o.Long.fromValue(e.address)).unsigned=!0:"string"==typeof e.address?n.address=parseInt(e.address,10):"number"==typeof e.address?n.address=e.address:"object"==typeof e.address&&(n.address=new o.LongBits(e.address.low>>>0,e.address.high>>>0).toNumber(!0))),e.line){if(!Array.isArray(e.line))throw TypeError(".perftools.profiles.Location.line: array expected");n.line=[];for(var t=0;t>>0,e.id.high>>>0).toNumber(!0):e.id),null!=e.mappingId&&e.hasOwnProperty("mappingId")&&("number"==typeof e.mappingId?t.mappingId=n.longs===String?String(e.mappingId):e.mappingId:t.mappingId=n.longs===String?o.Long.prototype.toString.call(e.mappingId):n.longs===Number?new o.LongBits(e.mappingId.low>>>0,e.mappingId.high>>>0).toNumber(!0):e.mappingId),null!=e.address&&e.hasOwnProperty("address")&&("number"==typeof e.address?t.address=n.longs===String?String(e.address):e.address:t.address=n.longs===String?o.Long.prototype.toString.call(e.address):n.longs===Number?new o.LongBits(e.address.low>>>0,e.address.high>>>0).toNumber(!0):e.address),e.line&&e.line.length){t.line=[];for(var l=0;l>>3){case 1:i.functionId=e.uint64();break;case 2:i.line=e.int64();break;default:e.skipType(7&l)}}return i},i.decodeDelimited=function(e){return e instanceof n||(e=new n(e)),this.decode(e,e.uint32())},i.verify=function(e){return"object"!=typeof e||null===e?"object expected":null!=e.functionId&&e.hasOwnProperty("functionId")&&!(o.isInteger(e.functionId)||e.functionId&&o.isInteger(e.functionId.low)&&o.isInteger(e.functionId.high))?"functionId: integer|Long expected":null!=e.line&&e.hasOwnProperty("line")&&!(o.isInteger(e.line)||e.line&&o.isInteger(e.line.low)&&o.isInteger(e.line.high))?"line: integer|Long expected":null},i.fromObject=function(e){if(e instanceof r.perftools.profiles.Line)return e;var n=new r.perftools.profiles.Line;return null!=e.functionId&&(o.Long?(n.functionId=o.Long.fromValue(e.functionId)).unsigned=!0:"string"==typeof e.functionId?n.functionId=parseInt(e.functionId,10):"number"==typeof e.functionId?n.functionId=e.functionId:"object"==typeof e.functionId&&(n.functionId=new o.LongBits(e.functionId.low>>>0,e.functionId.high>>>0).toNumber(!0))),null!=e.line&&(o.Long?(n.line=o.Long.fromValue(e.line)).unsigned=!1:"string"==typeof e.line?n.line=parseInt(e.line,10):"number"==typeof e.line?n.line=e.line:"object"==typeof e.line&&(n.line=new o.LongBits(e.line.low>>>0,e.line.high>>>0).toNumber())),n},i.toObject=function(e,n){n||(n={});var t={};if(n.defaults){if(o.Long){var r=new o.Long(0,0,!0);t.functionId=n.longs===String?r.toString():n.longs===Number?r.toNumber():r}else t.functionId=n.longs===String?"0":0;o.Long?(r=new o.Long(0,0,!1),t.line=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.line=n.longs===String?"0":0}return null!=e.functionId&&e.hasOwnProperty("functionId")&&("number"==typeof e.functionId?t.functionId=n.longs===String?String(e.functionId):e.functionId:t.functionId=n.longs===String?o.Long.prototype.toString.call(e.functionId):n.longs===Number?new o.LongBits(e.functionId.low>>>0,e.functionId.high>>>0).toNumber(!0):e.functionId),null!=e.line&&e.hasOwnProperty("line")&&("number"==typeof e.line?t.line=n.longs===String?String(e.line):e.line:t.line=n.longs===String?o.Long.prototype.toString.call(e.line):n.longs===Number?new o.LongBits(e.line.low>>>0,e.line.high>>>0).toNumber():e.line),t},i.prototype.toJSON=function(){return this.constructor.toObject(this,e.util.toJSONOptions)},i}(),i.Function=function(){function i(e){if(e)for(var n=Object.keys(e),t=0;t>>3){case 1:i.id=e.uint64();break;case 2:i.name=e.int64();break;case 3:i.systemName=e.int64();break;case 4:i.filename=e.int64();break;case 5:i.startLine=e.int64();break;default:e.skipType(7&l)}}return i},i.decodeDelimited=function(e){return e instanceof n||(e=new n(e)),this.decode(e,e.uint32())},i.verify=function(e){return"object"!=typeof e||null===e?"object expected":null!=e.id&&e.hasOwnProperty("id")&&!(o.isInteger(e.id)||e.id&&o.isInteger(e.id.low)&&o.isInteger(e.id.high))?"id: integer|Long expected":null!=e.name&&e.hasOwnProperty("name")&&!(o.isInteger(e.name)||e.name&&o.isInteger(e.name.low)&&o.isInteger(e.name.high))?"name: integer|Long expected":null!=e.systemName&&e.hasOwnProperty("systemName")&&!(o.isInteger(e.systemName)||e.systemName&&o.isInteger(e.systemName.low)&&o.isInteger(e.systemName.high))?"systemName: integer|Long expected":null!=e.filename&&e.hasOwnProperty("filename")&&!(o.isInteger(e.filename)||e.filename&&o.isInteger(e.filename.low)&&o.isInteger(e.filename.high))?"filename: integer|Long expected":null!=e.startLine&&e.hasOwnProperty("startLine")&&!(o.isInteger(e.startLine)||e.startLine&&o.isInteger(e.startLine.low)&&o.isInteger(e.startLine.high))?"startLine: integer|Long expected":null},i.fromObject=function(e){if(e instanceof r.perftools.profiles.Function)return e;var n=new r.perftools.profiles.Function;return null!=e.id&&(o.Long?(n.id=o.Long.fromValue(e.id)).unsigned=!0:"string"==typeof e.id?n.id=parseInt(e.id,10):"number"==typeof e.id?n.id=e.id:"object"==typeof e.id&&(n.id=new o.LongBits(e.id.low>>>0,e.id.high>>>0).toNumber(!0))),null!=e.name&&(o.Long?(n.name=o.Long.fromValue(e.name)).unsigned=!1:"string"==typeof e.name?n.name=parseInt(e.name,10):"number"==typeof e.name?n.name=e.name:"object"==typeof e.name&&(n.name=new o.LongBits(e.name.low>>>0,e.name.high>>>0).toNumber())),null!=e.systemName&&(o.Long?(n.systemName=o.Long.fromValue(e.systemName)).unsigned=!1:"string"==typeof e.systemName?n.systemName=parseInt(e.systemName,10):"number"==typeof e.systemName?n.systemName=e.systemName:"object"==typeof e.systemName&&(n.systemName=new o.LongBits(e.systemName.low>>>0,e.systemName.high>>>0).toNumber())),null!=e.filename&&(o.Long?(n.filename=o.Long.fromValue(e.filename)).unsigned=!1:"string"==typeof e.filename?n.filename=parseInt(e.filename,10):"number"==typeof e.filename?n.filename=e.filename:"object"==typeof e.filename&&(n.filename=new o.LongBits(e.filename.low>>>0,e.filename.high>>>0).toNumber())),null!=e.startLine&&(o.Long?(n.startLine=o.Long.fromValue(e.startLine)).unsigned=!1:"string"==typeof e.startLine?n.startLine=parseInt(e.startLine,10):"number"==typeof e.startLine?n.startLine=e.startLine:"object"==typeof e.startLine&&(n.startLine=new o.LongBits(e.startLine.low>>>0,e.startLine.high>>>0).toNumber())),n},i.toObject=function(e,n){n||(n={});var t={};if(n.defaults){if(o.Long){var r=new o.Long(0,0,!0);t.id=n.longs===String?r.toString():n.longs===Number?r.toNumber():r}else t.id=n.longs===String?"0":0;o.Long?(r=new o.Long(0,0,!1),t.name=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.name=n.longs===String?"0":0,o.Long?(r=new o.Long(0,0,!1),t.systemName=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.systemName=n.longs===String?"0":0,o.Long?(r=new o.Long(0,0,!1),t.filename=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.filename=n.longs===String?"0":0,o.Long?(r=new o.Long(0,0,!1),t.startLine=n.longs===String?r.toString():n.longs===Number?r.toNumber():r):t.startLine=n.longs===String?"0":0}return null!=e.id&&e.hasOwnProperty("id")&&("number"==typeof e.id?t.id=n.longs===String?String(e.id):e.id:t.id=n.longs===String?o.Long.prototype.toString.call(e.id):n.longs===Number?new o.LongBits(e.id.low>>>0,e.id.high>>>0).toNumber(!0):e.id),null!=e.name&&e.hasOwnProperty("name")&&("number"==typeof e.name?t.name=n.longs===String?String(e.name):e.name:t.name=n.longs===String?o.Long.prototype.toString.call(e.name):n.longs===Number?new o.LongBits(e.name.low>>>0,e.name.high>>>0).toNumber():e.name),null!=e.systemName&&e.hasOwnProperty("systemName")&&("number"==typeof e.systemName?t.systemName=n.longs===String?String(e.systemName):e.systemName:t.systemName=n.longs===String?o.Long.prototype.toString.call(e.systemName):n.longs===Number?new o.LongBits(e.systemName.low>>>0,e.systemName.high>>>0).toNumber():e.systemName),null!=e.filename&&e.hasOwnProperty("filename")&&("number"==typeof e.filename?t.filename=n.longs===String?String(e.filename):e.filename:t.filename=n.longs===String?o.Long.prototype.toString.call(e.filename):n.longs===Number?new o.LongBits(e.filename.low>>>0,e.filename.high>>>0).toNumber():e.filename),null!=e.startLine&&e.hasOwnProperty("startLine")&&("number"==typeof e.startLine?t.startLine=n.longs===String?String(e.startLine):e.startLine:t.startLine=n.longs===String?o.Long.prototype.toString.call(e.startLine):n.longs===Number?new o.LongBits(e.startLine.low>>>0,e.startLine.high>>>0).toNumber():e.startLine),t},i.prototype.toJSON=function(){return this.constructor.toObject(this,e.util.toJSONOptions)},i}(),i),l}(),module.exports=r; +},{"protobufjs/minimal":186}],155:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.importAsPprofProfile=r;var e=require("./profile.proto.js"),n=require("../lib/profile"),t=require("../lib/utils"),l=require("../lib/value-formatters");function r(r){if(0===r.byteLength)return null;let o;try{o=e.perftools.profiles.Profile.decode(new Uint8Array(r))}catch(e){return null}function i(e){return"number"==typeof e?e:e.low}function u(e){return o.stringTable[i(e)]||null}const s=new Map;function a(e){const{name:n,filename:t,startLine:l}=e,r=null!=n&&u(n)||"(unknown)",o=null!=t?u(t):null,i=null!=l?+l:null,s={key:`${r}:${o}:${i}`,name:r};return null!=o&&(s.file=o),null!=i&&(s.line=i),s}for(let e of o.function)if(e.id){const n=a(e);null!=n&&s.set(i(e.id),n)}function c(e){const{line:n}=e;if(null==n)return null;const l=(0,t.lastOf)(n);return null==l?null:l.functionId&&s.get(i(l.functionId))||null}const f=new Map;for(let e of o.location)if(null!=e.id){const n=c(e);n&&f.set(i(e.id),n)}const p=o.sampleType.map(e=>({type:e.type&&u(e.type)||"samples",unit:e.unit&&u(e.unit)||"count"})),d=o.defaultSampleType?+o.defaultSampleType:p.length-1,m=p[d],y=new n.StackListProfileBuilder;switch(m.unit){case"nanoseconds":case"microseconds":case"milliseconds":case"seconds":y.setValueFormatter(new l.TimeFormatter(m.unit));break;case"bytes":y.setValueFormatter(new l.ByteFormatter)}for(let e of o.sample){const n=e.locationId?e.locationId.map(e=>f.get(i(e))):[];n.reverse();const t=e.value[d];y.appendSampleWithWeight(n.filter(e=>null!=e),+t)}return y.build()} +},{"./profile.proto.js":174,"../lib/profile":139,"../lib/utils":70,"../lib/value-formatters":140}],156:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.importFromChromeHeapProfile=o;var e=require("../lib/profile"),r=require("../lib/utils"),t=require("../lib/value-formatters");const n=new Map;function i(e){return(0,r.getOrInsert)(n,e,e=>{const r=e.functionName||"(anonymous)",t=e.url,n=e.lineNumber,i=e.columnNumber;return{key:`${r}:${t}:${n}:${i}`,name:r,file:t,line:n,col:i}})}function o(r){const n=new Map;let o=0;const l=(e,r)=>{e.id=o++,n.set(e.id,e),r&&(e.parent=r.id),e.children.forEach(r=>l(r,e))};l(r.head);const u=e=>{if(0===e.children.length)return e.selfSize||0;const r=e.children.reduce((e,r)=>e+=u(r),e.selfSize);return e.totalSize=r,r},a=u(r.head),s=[];for(let e of n.values()){let r=[];for(r.push(e);void 0!==e.parent;){const t=n.get(e.parent);if(void 0===t)break;r.unshift(t),e=t}s.push(r)}const c=new e.StackListProfileBuilder(a);for(let e of s){const r=e[e.length-1];c.appendSampleWithWeight(e.map(e=>i(e.callFrame)),r.selfSize)}return c.setValueFormatter(new t.ByteFormatter),c.build()} +},{"../lib/profile":139,"../lib/utils":70,"../lib/value-formatters":140}],157:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.isTraceEventFormatted=l,exports.importTraceEvents=p;var e=require("../lib/utils"),t=require("../lib/profile"),r=require("../lib/value-formatters");function n(e){const t=[];for(let r of e)switch(r.ph){case"B":case"E":case"X":t.push(r)}return t}function i(e){const t=[];for(let r of e)switch(r.ph){case"B":case"E":t.push(r);break;case"X":let e=null;if(null!=r.dur?e=r.dur:null!=r.tdur&&(e=r.tdur),null==e){console.warn("Found a complete event (X) with no duration. Skipping: ",r);continue}t.push(Object.assign({},r,{ph:"B"})),t.push(Object.assign({},r,{ph:"E",ts:r.ts+e}));break;default:return r}return t}function s(e){const t=new Map;for(let r of e)"M"===r.ph&&"process_name"===r.name&&r.args&&r.args.name&&t.set(r.pid,r.args.name);return t}function a(e){const t=new Map;for(let r of e)if("M"===r.ph&&"thread_name"===r.name&&r.args&&r.args.name){const e=`${r.pid}:${r.tid}`;t.set(e,r.args.name)}return t}function u(e){let t=`${e.name||"(unnamed)"}`;return e.args&&(t+=` ${JSON.stringify(e.args)}`),t}function o(o){const c=new Map,f=i(n(o)),l=s(o),p=a(o);if(f.sort((e,t)=>{if(e.tst.ts)return 1;if(e.pidt.pid)return 1;if(e.tidt.tid)return 1;if(u(e)===u(t)){if("B"===e.ph&&"E"===t.ph)return-1;if("E"===e.ph&&"B"===t.ph)return 1}else{if("B"===e.ph&&"E"===t.ph)return 1;if("E"===e.ph&&"B"===t.ph)return-1}return 0}),f.length>0){const e=f[0].ts;for(let t of f)t.ts-=e}function d(n,i){const s=`${(0,e.zeroPad)(""+n,10)}:${(0,e.zeroPad)(""+i,10)}`;let a=c.get(s);if(null!=a)return a;let u=new t.CallTreeProfileBuilder;a={profile:u,eventStack:[]},u.setValueFormatter(new r.TimeFormatter("microseconds")),c.set(s,a);const o=l.get(n),f=p.get(`${n}:${i}`);return null!=o&&null!=f?u.setName(`${o} (pid ${n}), ${f} (tid ${i})`):null!=o?u.setName(`${o} (pid ${n}, tid ${i})`):null!=f?u.setName(`${f} (pid ${n}, tid ${i})`):u.setName(`pid ${n}, tid ${i}`),a}for(let t of f){const{profile:r,eventStack:n}=d(t.pid,t.tid),i=u(t),s={key:i,name:i};switch(t.ph){case"B":n.push(t),r.enterFrame(s,t.ts);break;case"E":const i=(0,e.lastOf)(n);null!=i&&i.name===t.name?(r.leaveFrame(s,t.ts),n.pop()):console.warn("Event discarded because it did not match top-of-stack. Discarded event:",t,"Top of stack:",i);break;default:return t}}const h=Array.from(c.entries());return(0,e.sortBy)(h,e=>e[0]),{name:"",indexToView:0,profiles:h.map(e=>e[1].profile)}}function c(e){if(!Array.isArray(e))return!1;if(0===e.length)return!1;for(let t of e){if(!("ph"in t))return!1;switch(t.ph){case"B":case"E":case"X":if(!("ts"in t))return!1}}return!0}function f(e){return"traceEvents"in e&&c(e.traceEvents)}function l(e){return f(e)||c(e)}function p(e){if(f(e))return o(e.traceEvents);if(c(e))return o(e);return e} +},{"../lib/utils":70,"../lib/profile":139,"../lib/value-formatters":140}],104:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.importProfileGroupFromText=g,exports.importProfileGroupFromBase64=h,exports.importProfilesFromFile=F,exports.importProfilesFromArrayBuffer=y,exports.importFromFileSystemDirectoryEntry=C;var e=require("./chrome"),r=require("./stackprof"),o=require("./instruments"),t=require("./bg-flamegraph"),i=require("./firefox"),n=require("../lib/file-format"),s=require("./v8proflog"),p=require("./linux-tools-perf"),l=require("./haskell"),m=require("./utils"),a=require("./pprof"),f=require("../lib/utils"),u=require("./v8heapalloc"),c=require("./trace-event"),d=function(e,r,o,t){return new(o||(o=Promise))(function(i,n){function s(e){try{l(t.next(e))}catch(e){n(e)}}function p(e){try{l(t.throw(e))}catch(e){n(e)}}function l(e){e.done?i(e.value):new o(function(r){r(e.value)}).then(s,p)}l((t=t.apply(e,r||[])).next())})};function g(e,r){return d(this,void 0,void 0,function*(){return yield P(new m.TextProfileDataSource(e,r))})}function h(e,r){return d(this,void 0,void 0,function*(){return yield P(m.MaybeCompressedDataReader.fromArrayBuffer(e,(0,f.decodeBase64)(r).buffer))})}function F(e){return d(this,void 0,void 0,function*(){return P(m.MaybeCompressedDataReader.fromFile(e))})}function y(e,r){return d(this,void 0,void 0,function*(){return P(m.MaybeCompressedDataReader.fromArrayBuffer(e,r))})}function P(e){return d(this,void 0,void 0,function*(){const r=yield e.name(),o=yield I(e);if(o){o.name||(o.name=r);for(let e of o.profiles)e&&!e.getName()&&e.setName(r);return o}return null})}function v(e){return e?{name:e.getName(),indexToView:0,profiles:[e]}:null}function x(e){return"["===(e=e.trim())[0]&&"]"!==(e=e.replace(/,\s*$/,""))[e.length-1]&&(e+="]"),e}function I(m){return d(this,void 0,void 0,function*(){const f=yield m.name(),d=yield m.readAsArrayBuffer();{const e=(0,a.importAsPprofProfile)(d);if(e)return console.log("Importing as protobuf encoded pprof file"),v(e)}const g=yield m.readAsText();if(f.endsWith(".speedscope.json"))return console.log("Importing as speedscope json file"),(0,n.importSpeedscopeProfiles)(JSON.parse(g));if(f.endsWith(".chrome.json")||/Profile-\d{8}T\d{6}/.exec(f))return console.log("Importing as Chrome Timeline"),(0,e.importFromChromeTimeline)(JSON.parse(g),f);if(f.endsWith(".stackprof.json"))return console.log("Importing as stackprof profile"),v((0,r.importFromStackprof)(JSON.parse(g)));if(f.endsWith(".instruments.txt"))return console.log("Importing as Instruments.app deep copy"),v((0,o.importFromInstrumentsDeepCopy)(g));if(f.endsWith(".linux-perf.txt"))return console.log("Importing as output of linux perf script"),(0,p.importFromLinuxPerf)(g);if(f.endsWith(".collapsedstack.txt"))return console.log("Importing as collapsed stack format"),v((0,t.importFromBGFlameGraph)(g));if(f.endsWith(".v8log.json"))return console.log("Importing as --prof-process v8 log"),v((0,s.importFromV8ProfLog)(JSON.parse(g)));if(f.endsWith(".heapprofile"))return console.log("Importing as Chrome Heap Profile"),v((0,u.importFromChromeHeapProfile)(JSON.parse(g)));let h;try{h=JSON.parse(x(g))}catch(e){}if(h){if("https://www.speedscope.app/file-format-schema.json"===h.$schema)return console.log("Importing as speedscope json file"),(0,n.importSpeedscopeProfiles)(JSON.parse(g));if(h.systemHost&&"Firefox"==h.systemHost.name)return console.log("Importing as Firefox profile"),v((0,i.importFromFirefox)(h));if((0,e.isChromeTimeline)(h))return console.log("Importing as Chrome Timeline"),(0,e.importFromChromeTimeline)(h,f);if("nodes"in h&&"samples"in h&&"timeDeltas"in h)return console.log("Importing as Chrome CPU Profile"),v((0,e.importFromChromeCPUProfile)(h));if((0,c.isTraceEventFormatted)(h))return console.log("Importing as Trace Event Format profile"),(0,c.importTraceEvents)(h);if("head"in h&&"samples"in h&&"timestamps"in h)return console.log("Importing as Chrome CPU Profile (old format)"),v((0,e.importFromOldV8CPUProfile)(h));if("mode"in h&&"frames"in h&&"raw_timestamp_deltas"in h)return console.log("Importing as stackprof profile"),v((0,r.importFromStackprof)(h));if("code"in h&&"functions"in h&&"ticks"in h)return console.log("Importing as --prof-process v8 log"),v((0,s.importFromV8ProfLog)(h));if("head"in h&&"selfSize"in h.head)return console.log("Importing as Chrome Heap Profile"),v((0,u.importFromChromeHeapProfile)(JSON.parse(g)));if("rts_arguments"in h&&"initial_capabilities"in h)return console.log("Importing as Haskell GHC JSON Profile"),(0,l.importFromHaskell)(h)}else{if(/^[\w \t\(\)]*\tSymbol Name/.exec(g))return console.log("Importing as Instruments.app deep copy"),v((0,o.importFromInstrumentsDeepCopy)(g));const e=g.split(/\n/).length;if(e>=1&&e===g.split(/ \d+\r?\n/).length)return console.log("Importing as collapsed stack format"),v((0,t.importFromBGFlameGraph)(g));const r=(0,p.importFromLinuxPerf)(g);if(r)return console.log("Importing from linux perf script output"),r}return null})}function C(e){return d(this,void 0,void 0,function*(){return(0,o.importFromInstrumentsTrace)(e)})} +},{"./chrome":146,"./stackprof":147,"./instruments":148,"./bg-flamegraph":149,"./firefox":150,"../lib/file-format":83,"./v8proflog":151,"./linux-tools-perf":152,"./haskell":153,"./utils":154,"./pprof":155,"../lib/utils":70,"./v8heapalloc":156,"./trace-event":157}]},{},[104], null) +//# sourceMappingURL=import.a03c2bef.map \ No newline at end of file diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/index.html b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/index.html new file mode 100644 index 0000000000000000000000000000000000000000..3db6f9064ca2689094c6aed55b7abd4426a5dbad --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/index.html @@ -0,0 +1 @@ + speedscope \ No newline at end of file diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/perf-vertx-stacks-01-collapsed-all.3e0a632c.txt b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/perf-vertx-stacks-01-collapsed-all.3e0a632c.txt new file mode 100644 index 0000000000000000000000000000000000000000..4b5f79febeeeff7e439b6e6e2b4af9baa6d1dccf --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/perf-vertx-stacks-01-collapsed-all.3e0a632c.txt @@ -0,0 +1,199 @@ +java;read;check_events_[k];hypercall_page_[k] 1 +java;start_thread;java_start;GCTaskThread::run;ScavengeRootsTask::do_it;ClassLoaderDataGraph::oops_do;ClassLoaderData::oops_do;PSScavengeKlassClosure::do_klass 1 +java;start_thread;java_start;GCTaskThread::run;StealTask::do_it;PSPromotionManager::drain_stacks_depth;oopDesc* PSPromotionManager::copy_to_survivor_space;InstanceKlass::oop_push_contents 1 +java;start_thread;java_start;GCTaskThread::run;StealTask::do_it;ParallelTaskTerminator::offer_termination 5 +java;start_thread;java_start;GCTaskThread::run;StealTask::do_it;SpinPause 7 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/buffer/AbstractByteBufAllocator:.directBuffer_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/AbstractReferenceCountedByteBuf:.release_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];io/netty/buffer/PooledByteBuf:.internalNioBuffer_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/NativeThread:.current_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j]; 3 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];Java_sun_nio_ch_FileDispatcherImpl_write0 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;sys_write_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];fget_light_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];__srcu_read_lock_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];__tcp_push_pending_frames_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];ktime_get_real_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];skb_clone_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_set_skb_tso_segs_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_hard_start_xmit_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_pick_tx_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];dev_hard_start_xmit_[k];dev_queue_xmit_nit_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];dev_hard_start_xmit_[k];loopback_xmit_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];dev_hard_start_xmit_[k];loopback_xmit_[k];netif_rx_[k];netif_rx.part.82_[k];xen_restore_fl_direct_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];dev_hard_start_xmit_[k];loopback_xmit_[k];netif_rx_[k];netif_rx.part.82_[k];xen_restore_fl_direct_end_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];dma_issue_pending_all_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k];__inet_lookup_established_[k] 3 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k];tcp_v4_do_rcv_[k];tcp_event_data_recv_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k];tcp_v4_do_rcv_[k];tcp_rcv_established_[k];sock_def_readable_[k];__wake_up_sync_key_[k];check_events_[k];hypercall_page_[k] 19 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k];tcp_v4_do_rcv_[k];tcp_rcv_established_[k];tcp_ack_[k] 3 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k];tcp_v4_do_rcv_[k];tcp_rcv_established_[k];tcp_ack_[k];tcp_clean_rtx_queue_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k];tcp_v4_do_rcv_[k];tcp_rcv_established_[k];tcp_ack_[k];tcp_clean_rtx_queue_[k];bictcp_acked_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k];tcp_v4_do_rcv_[k];tcp_rcv_established_[k];tcp_ack_[k];tcp_clean_rtx_queue_[k];ktime_get_real_[k];getnstimeofday_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k];tcp_v4_do_rcv_[k];tcp_rcv_established_[k];tcp_ack_[k];tcp_clean_rtx_queue_[k];ktime_get_real_[k];getnstimeofday_[k];xen_clocksource_get_cycles_[k];xen_clocksource_read_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k];tcp_v4_do_rcv_[k];tcp_rcv_established_[k];tcp_ack_[k];tcp_clean_rtx_queue_[k];tcp_rtt_estimator_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_[k];ip_local_deliver_finish_[k];tcp_v4_rcv_[k];tcp_v4_do_rcv_[k];tcp_rcv_established_[k];tcp_ack_[k];tcp_clean_rtx_queue_[k];tcp_valid_rtt_meas_[k];tcp_rtt_estimator_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];__do_softirq_[k];net_rx_action_[k];process_backlog_[k];__netif_receive_skb_[k];ip_rcv_[k];ip_rcv_finish_[k];ip_local_deliver_finish_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];local_bh_enable_[k];do_softirq_[k];call_softirq_[k];rcu_bh_qs_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_local_out_[k];ip_output_[k];ip_finish_output_[k];dev_queue_xmit_[k];netif_skb_features_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ip_queue_xmit_[k];ip_output_[k] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ktime_get_real_[k];getnstimeofday_[k];xen_clocksource_get_cycles_[k];pvclock_clocksource_read_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ktime_get_real_[k];getnstimeofday_[k];xen_clocksource_get_cycles_[k];xen_clocksource_read_[k];pvclock_clocksource_read_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];ktime_get_real_[k];xen_clocksource_get_cycles_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];__tcp_push_pending_frames_[k];tcp_write_xmit_[k];tcp_transmit_skb_[k];skb_dst_set_noref_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];lock_sock_nested_[k];_raw_spin_lock_bh_[k];local_bh_disable_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];sk_stream_alloc_skb_[k];__alloc_skb_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];sk_stream_alloc_skb_[k];__alloc_skb_[k];__kmalloc_node_track_caller_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];sk_stream_alloc_skb_[k];__alloc_skb_[k];__kmalloc_node_track_caller_[k];arch_local_irq_save_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];sk_stream_alloc_skb_[k];__alloc_skb_[k];__phys_addr_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];sk_stream_alloc_skb_[k];__alloc_skb_[k];get_slab_[k] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];sk_stream_alloc_skb_[k];__alloc_skb_[k];kmem_cache_alloc_node_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];sk_stream_alloc_skb_[k];ksize_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];tcp_send_mss_[k];tcp_current_mss_[k];ipv4_mtu_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];tcp_send_mss_[k];tcp_current_mss_[k];tcp_established_options_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];tcp_send_mss_[k];tcp_xmit_size_goal_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];do_sync_write_[k];sock_aio_write_[k];do_sock_write.isra.10_[k];inet_sendmsg_[k];tcp_sendmsg_[k];tcp_xmit_size_goal_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];fsnotify_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];fsnotify_[k];__srcu_read_lock_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];rw_verify_area_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];rw_verify_area_[k];apparmor_file_permission_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];rw_verify_area_[k];security_file_permission_[k];apparmor_file_permission_[k];common_file_perm_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/FileDispatcherImpl:.write0_[j];write;system_call_fastpath_[k];sys_write_[k];vfs_write_[k];sock_aio_write_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.write_[j];sun/nio/ch/SocketChannelImpl:.writerCleanup_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/buffer/PooledUnsafeDirectByteBuf:.readBytes_[j];sun/nio/ch/SocketChannelImpl:.writerCleanup_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/ChannelOutboundHandlerAdapter:.flush_[j];io/netty/channel/AbstractChannelHandlerContext:.flush_[j];io/netty/channel/DefaultChannelPipeline$HeadContext:.flush_[j];io/netty/channel/AbstractChannel$AbstractUnsafe:.flush0_[j];io/netty/channel/nio/AbstractNioByteChannel:.doWrite_[j];io/netty/util/Recycler:.recycle_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j];io/netty/channel/ChannelDuplexHandler:.flush_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelReadComplete_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j];org/vertx/java/core/net/impl/VertxHandler:.channelReadComplete_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/buffer/AbstractReferenceCountedByteBuf:.release_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];java/util/concurrent/ConcurrentHashMap:.get_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/mozilla/javascript/Context:.getWrapFactory_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j] 3 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/ScriptableObject:.getParentScope_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/WrapFactory:.wrapAsJavaObject_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/WrapFactory:.wrapAsJavaObject_[j];java/util/HashMap:.get_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/WrapFactory:.wrap_[j];java/util/HashMap:.get_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.getObjectProp_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.getObjectProp_[j];org/mozilla/javascript/ScriptableObject$RelinkedSlot:.getValue_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.getObjectProp_[j];vtable chunks_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.nameOrFunction_[j];org/mozilla/javascript/ScriptableObject$Slot:.getValue_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.name_[j];org/mozilla/javascript/IdScriptableObject:.get_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.getPropFunctionAndThis_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/IdScriptableObject:.findInstanceIdInfo_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/IdScriptableObject:.has_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/IdScriptableObject:.has_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/IdScriptableObject:.put_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/IdScriptableObject:.setAttributes_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/MemberBox:.invoke_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/WrapFactory:.wrap_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.findFunction_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaObject:.get_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j];org/mozilla/javascript/IdScriptableObject:.get_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j];org/mozilla/javascript/IdScriptableObject:.put_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j];org/mozilla/javascript/ScriptableObject:.createSlot_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j];org/mozilla/javascript/IdScriptableObject:.setAttributes_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.getObjectProp_[j];org/mozilla/javascript/ScriptableObject$Slot:.getValue_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.getPropFunctionAndThis_[j];org/mozilla/javascript/NativeJavaObject:.get_[j];java/util/HashMap:.get_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];jint_disjoint_arraycopy_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/IdScriptableObject:.get_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/IdScriptableObject:.has_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j];org/mozilla/javascript/IdScriptableObject:.put_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j];org/mozilla/javascript/ScriptableObject:.createSlot_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j];org/mozilla/javascript/IdScriptableObject:.setAttributes_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.getObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.get_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.nameOrFunction_[j];org/mozilla/javascript/IdScriptableObject:.get_[j];org/mozilla/javascript/ScriptableObject$RelinkedSlot:.getValue_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.findInstanceIdInfo_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.put_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.put_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j];org/mozilla/javascript/ScriptableObject:.createSlot_[j] 3 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];vtable chunks_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/NativeFunction:.initScriptFunction_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j];org/mozilla/javascript/IdScriptableObject:.get_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.has_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.put_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.put_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j];org/mozilla/javascript/ScriptableObject:.createSlot_[j] 6 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.newObject_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptableObject:.getParentScope_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.has_[j] 4 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.has_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j] 5 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.put_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.put_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.put_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j];org/mozilla/javascript/ScriptableObject:.createSlot_[j] 6 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptableObject:.getPrototype_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptableObject:.getParentScope_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/TopLevel:.getBuiltinPrototype_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/ScriptRuntime:.name_[j];org/mozilla/javascript/ScriptRuntime:.nameOrFunction_[j];vtable chunks_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.has_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j];org/mozilla/javascript/IdScriptableObject:.setAttributes_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.createFunctionActivation_[j];org/mozilla/javascript/TopLevel:.getBuiltinPrototype_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/optimizer/OptRuntime:.call2_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.setObjectProp_[j];org/mozilla/javascript/IdScriptableObject:.put_[j];org/mozilla/javascript/ScriptableObject:.getSlot_[j];org/mozilla/javascript/ScriptableObject:.createSlot_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/ScriptRuntime:.indexFromString_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/ScriptRuntime:.setObjectElem_[j];org/mozilla/javascript/ScriptRuntime:.indexFromString_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];io/netty/handler/codec/http/DefaultHttpHeaders:.set_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j] 3 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/handler/codec/MessageToMessageEncoder:.write_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/handler/codec/MessageToMessageEncoder:.write_[j];io/netty/buffer/AbstractByteBuf:.writeBytes_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/handler/codec/MessageToMessageEncoder:.write_[j];io/netty/handler/codec/http/HttpObjectEncoder:.encode_[j];io/netty/buffer/AbstractByteBuf:.writeBytes_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/handler/codec/MessageToMessageEncoder:.write_[j];io/netty/handler/codec/http/HttpObjectEncoder:.encode_[j];io/netty/buffer/AbstractByteBufAllocator:.directBuffer_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/handler/codec/MessageToMessageEncoder:.write_[j];io/netty/handler/codec/http/HttpObjectEncoder:.encode_[j];io/netty/buffer/AbstractByteBufAllocator:.directBuffer_[j];io/netty/util/concurrent/FastThreadLocal:.get_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/handler/codec/MessageToMessageEncoder:.write_[j];io/netty/handler/codec/http/HttpObjectEncoder:.encode_[j];java/util/ArrayList:.add_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/handler/codec/MessageToMessageEncoder:.write_[j];io/netty/util/internal/RecyclableArrayList:.newInstance_[j];io/netty/util/concurrent/FastThreadLocal:.get_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/handler/codec/MessageToMessageEncoder:.write_[j];java/util/ArrayList:.ensureExplicitCapacity_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];io/netty/handler/codec/MessageToMessageEncoder:.write_[j];vtable chunks_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/channel/AbstractChannelHandlerContext:.write_[j];org/vertx/java/core/http/impl/VertxHttpHandler:.write_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/handler/codec/http/DefaultHttpHeaders:.add0_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];io/netty/handler/codec/http/DefaultHttpHeaders:.set_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/NativeJavaMethod:.call_[j];org/mozilla/javascript/MemberBox:.invoke_[j];sun/reflect/DelegatingMethodAccessorImpl:.invoke_[j];sun/nio/cs/UTF_8$Encoder:._[j];jbyte_disjoint_arraycopy_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];org/vertx/java/core/net/impl/VertxHandler:.channelRead_[j];org/vertx/java/core/http/impl/DefaultHttpServer$ServerHandler:.doMessageReceived_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vhello_js_1:.call_[j];org/mozilla/javascript/gen/file__home_bgregg_testtest_vert_x_2_1_4_sys_mods_io_vertx_lang_js_1_1_0_[j];org/mozilla/javascript/ScriptRuntime:.name_[j];org/mozilla/javascript/ScriptRuntime:.nameOrFunction_[j];org/mozilla/javascript/IdScriptableObject:.get_[j];org/mozilla/javascript/ScriptableObject$RelinkedSlot:.getValue_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/handler/codec/http/HttpObjectDecoder:.decode_[j];io/netty/buffer/AbstractByteBuf:.forEachByteAsc0_[j];io/netty/util/internal/AppendableCharSequence:.append_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/handler/codec/http/HttpObjectDecoder:.decode_[j];io/netty/handler/codec/http/HttpHeaders:.isTransferEncodingChunked_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/handler/codec/http/HttpObjectDecoder:.decode_[j];io/netty/handler/codec/http/HttpObjectDecoder:.findWhitespace_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/handler/codec/http/HttpObjectDecoder:.decode_[j];io/netty/handler/codec/http/HttpObjectDecoder:.readHeaders_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/handler/codec/http/HttpObjectDecoder:.decode_[j];io/netty/handler/codec/http/HttpObjectDecoder:.readHeaders_[j];io/netty/buffer/AbstractByteBuf:.forEachByteAsc0_[j] 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/handler/codec/http/HttpObjectDecoder:.decode_[j];io/netty/handler/codec/http/HttpObjectDecoder:.readHeaders_[j];io/netty/handler/codec/http/HttpHeaders:.hash_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/handler/codec/http/HttpObjectDecoder:.decode_[j];io/netty/handler/codec/http/HttpObjectDecoder:.readHeaders_[j];io/netty/handler/codec/http/HttpObjectDecoder:.splitHeader_[j] 5 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/AbstractChannelHandlerContext:.fireChannelRead_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelRead_[j];io/netty/handler/codec/http/HttpObjectDecoder:.decode_[j];io/netty/handler/codec/http/HttpObjectDecoder:.readHeaders_[j];java/util/Arrays:.fill_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];java/nio/channels/spi/AbstractInterruptibleChannel:.end_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read 2 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;sys_read_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];do_sync_read_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];vfs_read_[k];do_sync_read_[k];sock_aio_read_[k];sock_aio_read.part.13_[k];do_sock_read.isra.12_[k];inet_recvmsg_[k];__kfree_skb_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];vfs_read_[k];do_sync_read_[k];sock_aio_read_[k];sock_aio_read.part.13_[k];do_sock_read.isra.12_[k];inet_recvmsg_[k];tcp_rcv_space_adjust_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];vfs_read_[k];do_sync_read_[k];sock_aio_read_[k];sock_aio_read.part.13_[k];do_sock_read.isra.12_[k];inet_recvmsg_[k];tcp_recvmsg_[k];__kfree_skb_[k];skb_release_data_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];vfs_read_[k];do_sync_read_[k];sock_aio_read_[k];sock_aio_read.part.13_[k];do_sock_read.isra.12_[k];inet_recvmsg_[k];tcp_recvmsg_[k];__kfree_skb_[k];skb_release_head_state_[k];dst_release_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];vfs_read_[k];do_sync_read_[k];sock_aio_read_[k];sock_aio_read.part.13_[k];do_sock_read.isra.12_[k];inet_recvmsg_[k];tcp_recvmsg_[k];_raw_spin_lock_bh_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];vfs_read_[k];do_sync_read_[k];sock_aio_read_[k];sock_aio_read.part.13_[k];do_sock_read.isra.12_[k];inet_recvmsg_[k];tcp_recvmsg_[k];skb_copy_datagram_iovec_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];vfs_read_[k];do_sync_read_[k];sock_aio_read_[k];sock_aio_read.part.13_[k];do_sock_read.isra.12_[k];inet_recvmsg_[k];tcp_recvmsg_[k];skb_copy_datagram_iovec_[k];copy_user_enhanced_fast_string_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];vfs_read_[k];do_sync_read_[k];sock_aio_read_[k];sock_aio_read.part.13_[k];do_sock_read.isra.12_[k];inet_recvmsg_[k];tcp_recvmsg_[k];tcp_cleanup_rbuf_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];vfs_read_[k];do_sync_read_[k];sock_aio_read_[k];sock_aio_read.part.13_[k];do_sock_read.isra.12_[k];inet_recvmsg_[k];tcp_recvmsg_[k];tcp_cleanup_rbuf_[k];__tcp_select_window_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j];sun/nio/ch/SocketChannelImpl:.read_[j];sun/nio/ch/FileDispatcherImpl:.read0_[j];read;system_call_fastpath_[k];sys_read_[k];vfs_read_[k];rw_verify_area_[k] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/nio/AbstractNioByteChannel$NioByteUnsafe:.read_[j];io/netty/handler/codec/ByteToMessageDecoder:.channelReadComplete_[j] 1 +java;start_thread;java_start;JavaThread::run;JavaThread::thread_main_inner;thread_entry;JavaCalls::call_virtual;JavaCalls::call_virtual;JavaCalls::call_helper;call_stub_[j];Interpreter_[j];Interpreter_[j];io/netty/channel/nio/NioEventLoop:.run_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeys_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKeysOptimized_[j];io/netty/channel/nio/NioEventLoop:.processSelectedKey_[j];io/netty/channel/socket/nio/NioSocketChannel:.doReadBytes_[j] 1 +java;start_thread;java_start;VMThread::run;VMThread::loop;VMThread::evaluate_operation;VM_Operation::evaluate;VM_ParallelGCFailedAllocation::doit;ParallelScavengeHeap::failed_mem_allocate;PSScavenge::invoke;PSScavenge::invoke_no_policy;PSIsAliveClosure::do_object_b 1 +java;start_thread;java_start;VMThread::run;VMThread::loop;VMThread::evaluate_operation;VM_Operation::evaluate;VM_ParallelGCFailedAllocation::doit;ParallelScavengeHeap::failed_mem_allocate;PSScavenge::invoke;PSScavenge::invoke_no_policy;StringTable::unlink_or_oops_do 2 +java;start_thread;java_start;VMThread::run;VMThread::loop;VMThread::evaluate_operation;VM_Operation::evaluate;VM_ParallelGCFailedAllocation::doit;ParallelScavengeHeap::failed_mem_allocate;PSScavenge::invoke;PSScavenge::invoke_no_policy;pthread_cond_signal@@GLIBC_2.3.2;system_call_fastpath_[k];sys_futex_[k];do_futex_[k];futex_wake_op_[k] 1 +java;write;check_events_[k];hypercall_page_[k] 3 diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/release.txt b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/release.txt new file mode 100644 index 0000000000000000000000000000000000000000..b465301e73430b413e07fc382dc5f73be86c7769 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/release.txt @@ -0,0 +1,3 @@ +speedscope@1.5.3 +Thu Jan 16 00:10:56 PST 2020 +707462e9cffec2bda49587c39d621ba89d1b51cb diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/reset.7ae984ff.css b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/reset.7ae984ff.css new file mode 100644 index 0000000000000000000000000000000000000000..f73b094c15be95c0c8c38e6ef4b579e02d8ac40d --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/reset.7ae984ff.css @@ -0,0 +1 @@ +a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{overflow:hidden}body,html{height:100%}body{overflow:auto} \ No newline at end of file diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/speedscope.75eb7d8e.js b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/speedscope.75eb7d8e.js new file mode 100644 index 0000000000000000000000000000000000000000..97c9f10f32ffdbb65e990026b55e12d18392f8a0 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/speedscope-1.5.3/speedscope.75eb7d8e.js @@ -0,0 +1,173 @@ +parcelRequire=function(e,r,n,t){var i="function"==typeof parcelRequire&&parcelRequire,o="function"==typeof require&&require;function u(n,t){if(!r[n]){if(!e[n]){var f="function"==typeof parcelRequire&&parcelRequire;if(!t&&f)return f(n,!0);if(i)return i(n,!0);if(o&&"string"==typeof n)return o(n);var c=new Error("Cannot find module '"+n+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[n][1][r]||r};var l=r[n]=new u.Module(n);e[n][0].call(l.exports,p,l,l.exports,this)}return r[n].exports;function p(e){return u(p.resolve(e))}}u.isParcelRequire=!0,u.Module=function(e){this.id=e,this.bundle=u,this.exports={}},u.modules=e,u.cache=r,u.parent=i,u.register=function(r,n){e[r]=[function(e,r){r.exports=n},{}]};for(var f=0;f2;)n.push(arguments[s]);for(i&&null!=i.children&&(n.length||n.push(i.children),delete i.children);n.length;)if((a=n.pop())&&void 0!==a.pop)for(s=a.length;s--;)n.push(a[s]);else"boolean"==typeof a&&(a=null),(p="function"!=typeof r)&&(null==a?a="":"number"==typeof a?a=String(a):"string"!=typeof a&&(p=!1)),p&&l?c[c.length-1]+=a:c===o?c=[a]:c.push(a),l=p;var u=new e;return u.nodeName=r,u.children=c,u.attributes=null==i?void 0:i,u.key=null==i?void 0:i.key,void 0!==t.vnode&&t.vnode(u),u}function i(e,t){for(var n in t)e[n]=t[n];return e}var l="function"==typeof Promise?Promise.resolve().then.bind(Promise.resolve()):setTimeout;function a(e,t){return r(e.nodeName,i(i({},e.attributes),t),arguments.length>2?[].slice.call(arguments,2):e.children)}var p=/acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i,s=[];function c(e){!e._dirty&&(e._dirty=!0)&&1==s.push(e)&&(t.debounceRendering||l)(u)}function u(){var e,t=s;for(s=[];e=t.pop();)e._dirty&&A(e)}function f(e,t,n){return"string"==typeof t||"number"==typeof t?void 0!==e.splitText:"string"==typeof t.nodeName?!e._componentConstructor&&d(e,t.nodeName):n||e._componentConstructor===t.nodeName}function d(e,t){return e.normalizedNodeName===t||e.nodeName.toLowerCase()===t.toLowerCase()}function _(e){var t=i({},e.attributes);t.children=e.children;var n=e.nodeName.defaultProps;if(void 0!==n)for(var o in n)void 0===t[o]&&(t[o]=n[o]);return t}function v(e,t){var n=t?document.createElementNS("http://www.w3.org/2000/svg",e):document.createElement(e);return n.normalizedNodeName=e,n}function m(e){var t=e.parentNode;t&&t.removeChild(e)}function h(e,t,n,o,r){if("className"===t&&(t="class"),"key"===t);else if("ref"===t)n&&n(null),o&&o(e);else if("class"!==t||r)if("style"===t){if(o&&"string"!=typeof o&&"string"!=typeof n||(e.style.cssText=o||""),o&&"object"==typeof o){if("string"!=typeof n)for(var i in n)i in o||(e.style[i]="");for(var i in o)e.style[i]="number"==typeof o[i]&&!1===p.test(i)?o[i]+"px":o[i]}}else if("dangerouslySetInnerHTML"===t)o&&(e.innerHTML=o.__html||"");else if("o"==t[0]&&"n"==t[1]){var l=t!==(t=t.replace(/Capture$/,""));t=t.toLowerCase().substring(2),o?n||e.addEventListener(t,y,l):e.removeEventListener(t,y,l),(e._listeners||(e._listeners={}))[t]=o}else if("list"!==t&&"type"!==t&&!r&&t in e)b(e,t,null==o?"":o),null!=o&&!1!==o||e.removeAttribute(t);else{var a=r&&t!==(t=t.replace(/^xlink\:?/,""));null==o||!1===o?a?e.removeAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase()):e.removeAttribute(t):"function"!=typeof o&&(a?e.setAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase(),o):e.setAttribute(t,o))}else e.className=o||""}function b(e,t,n){try{e[t]=n}catch(e){}}function y(e){return this._listeners[e.type](t.event&&t.event(e)||e)}var x=[],C=0,g=!1,N=!1;function k(){for(var e;e=x.pop();)t.afterMount&&t.afterMount(e),e.componentDidMount&&e.componentDidMount()}function w(e,t,n,o,r,i){C++||(g=null!=r&&void 0!==r.ownerSVGElement,N=null!=e&&!("__preactattr_"in e));var l=S(e,t,n,o,i);return r&&l.parentNode!==r&&r.appendChild(l),--C||(N=!1,i||k()),l}function S(e,t,n,o,r){var i=e,l=g;if(null!=t&&"boolean"!=typeof t||(t=""),"string"==typeof t||"number"==typeof t)return e&&void 0!==e.splitText&&e.parentNode&&(!e._component||r)?e.nodeValue!=t&&(e.nodeValue=t):(i=document.createTextNode(t),e&&(e.parentNode&&e.parentNode.replaceChild(i,e),L(e,!0))),i.__preactattr_=!0,i;var a=t.nodeName;if("function"==typeof a)return D(e,t,n,o);if(g="svg"===a||"foreignObject"!==a&&g,a=String(a),(!e||!d(e,a))&&(i=v(a,g),e)){for(;e.firstChild;)i.appendChild(e.firstChild);e.parentNode&&e.parentNode.replaceChild(i,e),L(e,!0)}var p=i.firstChild,s=i.__preactattr_,c=t.children;if(null==s){s=i.__preactattr_={};for(var u=i.attributes,f=u.length;f--;)s[u[f].name]=u[f].value}return!N&&c&&1===c.length&&"string"==typeof c[0]&&null!=p&&void 0!==p.splitText&&null==p.nextSibling?p.nodeValue!=c[0]&&(p.nodeValue=c[0]):(c&&c.length||null!=p)&&U(i,c,n,o,N||null!=s.dangerouslySetInnerHTML),P(i,t.attributes,s),g=l,i}function U(e,t,n,o,r){var i,l,a,p,s,c=e.childNodes,u=[],d={},_=0,v=0,h=c.length,b=0,y=t?t.length:0;if(0!==h)for(var x=0;x0?"Unexpected "+(c.length>1?"keys":"key")+' "'+c.join('", "')+'" found in '+a+'. Expected to find one of the known reducer keys instead: "'+i.join('", "')+'". Unexpected keys will be ignored.':void 0}function f(e){Object.keys(e).forEach(function(t){var r=e[t];if(void 0===r(void 0,{type:n.INIT}))throw new Error('Reducer "'+t+"\" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.");if(void 0===r(void 0,{type:"@@redux/PROBE_UNKNOWN_ACTION_"+Math.random().toString(36).substring(7).split("").join(".")}))throw new Error('Reducer "'+t+"\" returned undefined when probed with a random type. Don't try to handle "+n.INIT+' or other actions in "redux/*" namespace. They are considered private. Instead, you must return the current state for any unknown actions, unless it is undefined, in which case you must return the initial state, regardless of the action type. The initial state may not be undefined, but can be null.')})}function l(e){for(var t=Object.keys(e),r={},n=0;n0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];if(u)throw u;for(var n=!1,o={},a=0;a=0||Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n},h=function(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e},b=!1;function y(){b||(b=!0,p(" does not support changing `store` on the fly. It is most likely that you see this error because you updated to Redux 2.x and React Redux 2.x which no longer hot reload reducers automatically. See https://github.com/reactjs/react-redux/releases/tag/v2.0.0 for the migration instructions."))}function v(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"store",o=arguments[1]||n+"Subscription",i=function(t){function e(r,o){a(this,e);var i=h(this,t.call(this,r,o));return i[n]=r.store,i}return f(e,t),e.prototype.getChildContext=function(){var t;return(t={})[n]=this[n],t[o]=null,t},e.prototype.render=function(){return r.only(this.props.children)},e}(e.Component);return i.prototype.componentWillReceiveProps=function(t){this[n]!==t.store&&y()},i.childContextTypes=((t={})[n]=u.isRequired,t[o]=s,t),i}var m=v(),P={childContextTypes:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},O={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},S=Object.defineProperty,g=Object.getOwnPropertyNames,w=Object.getOwnPropertySymbols,C=Object.getOwnPropertyDescriptor,j=Object.getPrototypeOf,T=j&&j(Object);function x(t,e,n){if("string"!=typeof e){if(T){var r=j(e);r&&r!==T&&x(t,r,n)}var o=g(e);w&&(o=o.concat(w(e)));for(var i=0;i1&&void 0!==arguments[1]?arguments[1]:{},i=o.getDisplayName,p=void 0===i?function(t){return"ConnectAdvanced("+t+")"}:i,c=o.methodName,b=void 0===c?"connectAdvanced":c,y=o.renderCountProp,v=void 0===y?void 0:y,m=o.shouldHandleStateChanges,P=void 0===m||m,O=o.storeKey,S=void 0===O?"store":O,g=o.withRef,w=void 0!==g&&g,C=l(o,["getDisplayName","methodName","renderCountProp","shouldHandleStateChanges","storeKey","withRef"]),j=S+"Subscription",T=R++,x=((n={})[S]=u,n[j]=s,n),E=((r={})[j]=s,r);return function(n){q("function"==typeof n,"You must pass a component to the function returned by "+b+". Instead received "+JSON.stringify(n));var r=n.displayName||n.name||"Component",o=p(r),i=d({},C,{getDisplayName:p,methodName:b,renderCountProp:v,shouldHandleStateChanges:P,storeKey:S,withRef:w,displayName:o,wrappedComponentName:r,WrappedComponent:n}),s=function(r){function s(t,e){a(this,s);var n=h(this,r.call(this,t,e));return n.version=T,n.state={},n.renderCount=0,n.store=t[S]||e[S],n.propsMode=Boolean(t[S]),n.setWrappedInstance=n.setWrappedInstance.bind(n),q(n.store,'Could not find "'+S+'" in either the context or props of "'+o+'". Either wrap the root component in a , or explicitly pass "'+S+'" as a prop to "'+o+'".'),n.initSelector(),n.initSubscription(),n}return f(s,r),s.prototype.getChildContext=function(){var t,e=this.propsMode?null:this.subscription;return(t={})[j]=e||this.context[j],t},s.prototype.componentDidMount=function(){P&&(this.subscription.trySubscribe(),this.selector.run(this.props),this.selector.shouldComponentUpdate&&this.forceUpdate())},s.prototype.componentWillReceiveProps=function(t){this.selector.run(t)},s.prototype.shouldComponentUpdate=function(){return this.selector.shouldComponentUpdate},s.prototype.componentWillUnmount=function(){this.subscription&&this.subscription.tryUnsubscribe(),this.subscription=null,this.notifyNestedSubs=W,this.store=null,this.selector.run=W,this.selector.shouldComponentUpdate=!1},s.prototype.getWrappedInstance=function(){return q(w,"To access the wrapped instance, you need to specify { withRef: true } in the options argument of the "+b+"() call."),this.wrappedInstance},s.prototype.setWrappedInstance=function(t){this.wrappedInstance=t},s.prototype.initSelector=function(){var e=t(this.store.dispatch,i);this.selector=F(e,this.store),this.selector.run(this.props)},s.prototype.initSubscription=function(){if(P){var t=(this.propsMode?this.props:this.context)[j];this.subscription=new M(this.store,t,this.onStateChange.bind(this)),this.notifyNestedSubs=this.subscription.notifyNestedSubs.bind(this.subscription)}},s.prototype.onStateChange=function(){this.selector.run(this.props),this.selector.shouldComponentUpdate?(this.componentDidUpdate=this.notifyNestedSubsOnComponentDidUpdate,this.setState(I)):this.notifyNestedSubs()},s.prototype.notifyNestedSubsOnComponentDidUpdate=function(){this.componentDidUpdate=void 0,this.notifyNestedSubs()},s.prototype.isSubscribed=function(){return Boolean(this.subscription)&&this.subscription.isSubscribed()},s.prototype.addExtraProps=function(t){if(!(w||v||this.propsMode&&this.subscription))return t;var e=d({},t);return w&&(e.ref=this.setWrappedInstance),v&&(e[v]=this.renderCount++),this.propsMode&&this.subscription&&(e[j]=this.subscription),e},s.prototype.render=function(){var t=this.selector;if(t.shouldComponentUpdate=!1,t.error)throw t.error;return(0,e.h)(n,this.addExtraProps(t.props))},s}(e.Component);return s.WrappedComponent=n,s.displayName=o,s.childContextTypes=E,s.contextTypes=x,s.prototype.componentWillUpdate=function(){var t=this;if(this.version!==T){this.version=T,this.initSelector();var e=[];this.subscription&&(e=this.subscription.listeners.get(),this.subscription.tryUnsubscribe()),this.initSubscription(),P&&(this.subscription.trySubscribe(),e.forEach(function(e){return t.subscription.listeners.subscribe(e)}))}},N(s,n)}}var _=Object.prototype.hasOwnProperty;function B(t,e){return t===e?0!==t||0!==e||1/t==1/e:t!=t&&e!=e}function H(t,e){if(B(t,e))return!0;if("object"!==(void 0===t?"undefined":c(t))||null===t||"object"!==(void 0===e?"undefined":c(e))||null===e)return!1;var n=Object.keys(t),r=Object.keys(e);if(n.length!==r.length)return!1;for(var o=0;o=0;r--){var o=e[r](t);if(o)return o}return function(e,r){throw new Error("Invalid value of type "+(void 0===t?"undefined":c(t))+" for "+n+" argument when connecting component "+r.wrappedComponentName+".")}}function Wt(t,e){return t===e}function Ft(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},e=t.connectHOC,n=void 0===e?A:e,r=t.mapStateToPropsFactories,o=void 0===r?Ct:r,i=t.mapDispatchToPropsFactories,s=void 0===i?St:i,u=t.mergePropsFactories,p=void 0===u?qt:u,c=t.selectorFactory,a=void 0===c?Rt:c;return function(t,e,r){var i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},u=i.pure,c=void 0===u||u,f=i.areStatesEqual,h=void 0===f?Wt:f,b=i.areOwnPropsEqual,y=void 0===b?H:b,v=i.areStatePropsEqual,m=void 0===v?H:v,P=i.areMergedPropsEqual,O=void 0===P?H:P,S=l(i,["pure","areStatesEqual","areOwnPropsEqual","areStatePropsEqual","areMergedPropsEqual"]),g=It(t,o,"mapStateToProps"),w=It(e,s,"mapDispatchToProps"),C=It(r,p,"mergeProps");return n(a,d({methodName:"connect",getDisplayName:function(t){return"Connect("+t+")"},shouldHandleStateChanges:Boolean(t),initMapStateToProps:g,initMapDispatchToProps:w,initMergeProps:C,pure:c,areStatesEqual:h,areOwnPropsEqual:y,areStatePropsEqual:m,areMergedPropsEqual:O},S))}}var At=Ft(),_t={Provider:m,connect:At,connectAdvanced:A};exports.Provider=m,exports.connect=At,exports.connectAdvanced=A,exports.default=_t; +},{"preact":24,"redux":31}],36:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.StatelessComponent=void 0,exports.actionCreator=n,exports.setter=o,exports.createContainer=s,exports.bindActionCreator=c;var e=require("preact-redux"),t=require("preact");const r=new Set;function n(e){if(r.has(e))throw new Error(`Cannot re-use action type name: ${e}`);const t=(t={})=>({type:e,payload:t});return t.matches=(t=>t.type===e),t}function o(e,t){return(r=t,n)=>e.matches(n)?n.payload:r}function s(t,r){return(0,e.connect)(e=>e,e=>({dispatch:e}),(e,t,n)=>r(e,t.dispatch,n))(t)}class a extends t.Component{}function c(e,t){return r=>{e(t(r))}}exports.StatelessComponent=a; +},{"preact-redux":26,"preact":24}],102:[function(require,module,exports) { +"use strict";function t(t,i,s){return ts?s:t}Object.defineProperty(exports,"__esModule",{value:!0}),exports.clamp=t;class i{constructor(t,i){this.x=t,this.y=i}withX(t){return new i(t,this.y)}withY(t){return new i(this.x,t)}plus(t){return new i(this.x+t.x,this.y+t.y)}minus(t){return new i(this.x-t.x,this.y-t.y)}times(t){return new i(this.x*t,this.y*t)}timesPointwise(t){return new i(this.x*t.x,this.y*t.y)}dividedByPointwise(t){return new i(this.x/t.x,this.y/t.y)}dot(t){return this.x*t.x+this.y*t.y}equals(t){return this.x===t.x&&this.y===t.y}approxEquals(t,i=1e-9){return Math.abs(this.x-t.x){if(t.actions.flamechart.setHoveredNode.matches(a)&&r(a)){const{hover:t}=a.payload.args;return Object.assign({},e,{hover:t})}if(t.actions.flamechart.setSelectedNode.matches(a)&&r(a)){const{selectedNode:t}=a.payload.args;return Object.assign({},e,{selectedNode:t})}if(t.actions.flamechart.setConfigSpaceViewportRect.matches(a)&&r(a)){const{configSpaceViewportRect:t}=a.payload.args;return Object.assign({},e,{configSpaceViewportRect:t})}if(t.actions.flamechart.setLogicalSpaceViewportSize.matches(a)&&r(a)){const{logicalSpaceViewportSize:t}=a.payload.args;return Object.assign({},e,{logicalSpaceViewportSize:t})}return t.actions.setViewMode.matches(a)?Object.assign({},e,{hover:null}):e}}!function(e){e.LEFT_HEAVY="LEFT_HEAVY",e.CHRONO="CHRONO",e.SANDWICH_INVERTED_CALLERS="SANDWICH_INVERTED_CALLERS",e.SANDWICH_CALLEES="SANDWICH_CALLEES"}(a||(exports.FlamechartID=a={})); +},{"../lib/math":102,"./actions":40}],100:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.createSandwichView=l;var e=require("./flamechart-view-state"),a=require("./actions");function l(l){const r=(0,e.createFlamechartViewStateReducer)(e.FlamechartID.SANDWICH_CALLEES,l),t=(0,e.createFlamechartViewStateReducer)(e.FlamechartID.SANDWICH_INVERTED_CALLERS,l);return(e={callerCallee:null},c)=>{if(a.actions.sandwichView.setSelectedFrame.matches(c)&&function(e){const{payload:a}=e;return a.profileIndex===l}(c))return null==c.payload.args?Object.assign({},e,{callerCallee:null}):Object.assign({},e,{callerCallee:{selectedFrame:c.payload.args,calleeFlamegraph:r(void 0,c),invertedCallerFlamegraph:t(void 0,c)}});const{callerCallee:n}=e;if(n){const{calleeFlamegraph:a,invertedCallerFlamegraph:l}=n,i=r(a,c),s=t(l,c);return i===a&&s===l?e:Object.assign({},e,{callerCallee:Object.assign({},n,{calleeFlamegraph:i,invertedCallerFlamegraph:s})})}return e}} +},{"./flamechart-view-state":98,"./actions":40}],70:[function(require,module,exports) { +"use strict";function t(t){return t[t.length-1]||null}function e(t,e){t.sort(function(t,r){return e(t)99?e=">99%":t<.01?e="<0.01%":t<1?e=`${t.toFixed(2)}%`:t<10&&(e=`${t.toFixed(1)}%`),e}function f(t){return t-Math.floor(t)}function h(t){return 2*Math.abs(f(t)-.5)-1}function g(t,e,r,n,o=1){for(console.assert(!isNaN(o)&&!isNaN(n));;){if(e-t<=o)return[t,e];const s=(e+t)/2;r(s){let n;return null==e?(n=t(r),e={args:r,result:n},n):x(e.args,r)?e.result:(e.args=r,e.result=t(r),e.result)}}function y(t){let e=null;return r=>{let n;return null==e?(n=t(r),e={args:r,result:n},n):e.args===r?e.result:(e.args=r,e.result=t(r),e.result)}}function w(t){let e=null;return()=>(null==e&&(e={result:t()}),e.result)}exports.KeyedSet=s;const E=w(()=>{const t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",e=new Map;for(let r=0;r>4,"="!==u&&(o[s++]=(15&c)<<4|f>>2),"="!==a&&(o[s++]=(7&f)<<6|h)}if(s!==n)throw new Error(`Expected to decode ${n} bytes, but only decoded ${s})`);return o} +},{}],52:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.profileGroup=void 0,exports.actionCreatorWithIndex=n;var e=require("./flamechart-view-state"),t=require("./sandwich-view-state"),i=require("../lib/typed-redux"),r=require("./actions"),a=require("../lib/math"),o=require("../lib/utils");function n(e){return(0,i.actionCreator)(e)}function l(i,r){const a=(0,e.createFlamechartViewStateReducer)(e.FlamechartID.CHRONO,r),n=(0,e.createFlamechartViewStateReducer)(e.FlamechartID.LEFT_HEAVY,r),l=(0,t.createSandwichView)(r);return(e,t)=>{if(void 0===e)return{profile:i,chronoViewState:a(void 0,t),leftHeavyViewState:n(void 0,t),sandwichViewState:l(void 0,t)};const r={profile:i,chronoViewState:a(e.chronoViewState,t),leftHeavyViewState:n(e.leftHeavyViewState,t),sandwichViewState:l(e.sandwichViewState,t)};return(0,o.objectsHaveShallowEquality)(e,r)?e:r}}const c=exports.profileGroup=((e=null,t)=>{if(r.actions.setProfileGroup.matches(t)){const{indexToView:e,profiles:i,name:r}=t.payload;return{indexToView:e,name:r,profiles:i.map((e,i)=>l(e,i)(void 0,t))}}if(null!=e){const{indexToView:n,profiles:c}=e,s=(0,a.clamp)((0,i.setter)(r.actions.setProfileIndexToView,0)(n,t),0,c.length-1),u=c.map((e,i)=>l(e.profile,i)(e,t));return n===s&&(0,o.objectsHaveShallowEquality)(c,u)?e:Object.assign({},e,{indexToView:s,profiles:u})}return e}); +},{"./flamechart-view-state":98,"./sandwich-view-state":100,"../lib/typed-redux":36,"./actions":40,"../lib/math":102,"../lib/utils":70}],40:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.actions=void 0;var e=require("../lib/typed-redux"),t=require("./profiles-state"),a=exports.actions=void 0;!function(a){let o,r;a.setProfileGroup=(0,e.actionCreator)("setProfileGroup"),a.setProfileIndexToView=(0,e.actionCreator)("setProfileIndexToView"),a.setGLCanvas=(0,e.actionCreator)("setGLCanvas"),a.setViewMode=(0,e.actionCreator)("setViewMode"),a.setFlattenRecursion=(0,e.actionCreator)("setFlattenRecursion"),a.setDragActive=(0,e.actionCreator)("setDragActive"),a.setLoading=(0,e.actionCreator)("setLoading"),a.setError=(0,e.actionCreator)("setError"),a.setHashParams=(0,e.actionCreator)("setHashParams"),function(a){a.setTableSortMethod=(0,e.actionCreator)("sandwichView.setTableSortMethod"),a.setSelectedFrame=(0,t.actionCreatorWithIndex)("sandwichView.setSelectedFarmr")}(o=a.sandwichView||(a.sandwichView={})),function(e){e.setHoveredNode=(0,t.actionCreatorWithIndex)("flamechart.setHoveredNode"),e.setSelectedNode=(0,t.actionCreatorWithIndex)("flamechart.setSelectedNode"),e.setConfigSpaceViewportRect=(0,t.actionCreatorWithIndex)("flamechart.setConfigSpaceViewportRect"),e.setLogicalSpaceViewportSize=(0,t.actionCreatorWithIndex)("flamechart.setLogicalSpaceViewportSpace")}(r=a.flamechart||(a.flamechart={}))}(a||(exports.actions=a={})); +},{"../lib/typed-redux":36,"./profiles-state":52}],50:[function(require,module,exports) { +"use strict";function t(t=window.location.hash){try{if(!t.startsWith("#"))return{};const e=t.substr(1).split("&"),r={};for(const t of e){let[e,o]=t.split("=");o=decodeURIComponent(o),"profileURL"===e?r.profileURL=o:"title"===e?r.title=o:"localProfilePath"===e&&(r.localProfilePath=o)}return r}catch(t){return console.error("Error when loading hash fragment."),console.error(t),{}}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.getHashParams=t; +},{}],158:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=t;var e=/-webkit-|-moz-|-ms-/;function t(t){return"string"==typeof t&&e.test(t)}module.exports=exports.default; +},{}],123:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=i;var e=require("css-in-js-utils/lib/isPrefixedValue"),t=r(e);function r(e){return e&&e.__esModule?e:{default:e}}var u=["-webkit-","-moz-",""];function i(e,r){if("string"==typeof r&&!(0,t.default)(r)&&r.indexOf("calc(")>-1)return u.map(function(e){return r.replace(/calc\(/g,e+"calc(")})}module.exports=exports.default; +},{"css-in-js-utils/lib/isPrefixedValue":158}],124:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=u;var e=require("css-in-js-utils/lib/isPrefixedValue"),r=t(e);function t(e){return e&&e.__esModule?e:{default:e}}var s=["-webkit-",""];function u(e,t){if("string"==typeof t&&!(0,r.default)(t)&&t.indexOf("cross-fade(")>-1)return s.map(function(e){return t.replace(/cross-fade\(/g,e+"cross-fade(")})}module.exports=exports.default; +},{"css-in-js-utils/lib/isPrefixedValue":158}],125:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=o;var e=["-webkit-","-moz-",""],r={"zoom-in":!0,"zoom-out":!0,grab:!0,grabbing:!0};function o(o,t){if("cursor"===o&&r.hasOwnProperty(t))return e.map(function(e){return e+t})}module.exports=exports.default; +},{}],126:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=u;var e=require("css-in-js-utils/lib/isPrefixedValue"),t=r(e);function r(e){return e&&e.__esModule?e:{default:e}}var i=["-webkit-",""];function u(e,r){if("string"==typeof r&&!(0,t.default)(r)&&r.indexOf("filter(")>-1)return i.map(function(e){return r.replace(/filter\(/g,e+"filter(")})}module.exports=exports.default; +},{"css-in-js-utils/lib/isPrefixedValue":158}],127:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=i;var e={flex:["-webkit-box","-moz-box","-ms-flexbox","-webkit-flex","flex"],"inline-flex":["-webkit-inline-box","-moz-inline-box","-ms-inline-flexbox","-webkit-inline-flex","inline-flex"]};function i(i,l){if("display"===i&&e.hasOwnProperty(l))return e[l]}module.exports=exports.default; +},{}],128:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=s;var e={"space-around":"distribute","space-between":"justify","flex-start":"start","flex-end":"end"},t={alignContent:"msFlexLinePack",alignSelf:"msFlexItemAlign",alignItems:"msFlexAlign",justifyContent:"msFlexPack",order:"msFlexOrder",flexGrow:"msFlexPositive",flexShrink:"msFlexNegative",flexBasis:"msFlexPreferredSize"};function s(s,l,r){t.hasOwnProperty(s)&&(r[t[s]]=e[l]||l)}module.exports=exports.default; +},{}],129:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=i;var e={"space-around":"justify","space-between":"justify","flex-start":"start","flex-end":"end","wrap-reverse":"multiple",wrap:"multiple"},t={alignItems:"WebkitBoxAlign",justifyContent:"WebkitBoxPack",flexWrap:"WebkitBoxLines"};function i(i,r,o){"flexDirection"===i&&"string"==typeof r&&(r.indexOf("column")>-1?o.WebkitBoxOrient="vertical":o.WebkitBoxOrient="horizontal",r.indexOf("reverse")>-1?o.WebkitBoxDirection="reverse":o.WebkitBoxDirection="normal"),t.hasOwnProperty(i)&&(o[t[i]]=e[r]||r)}module.exports=exports.default; +},{}],130:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=n;var e=require("css-in-js-utils/lib/isPrefixedValue"),t=r(e);function r(e){return e&&e.__esModule?e:{default:e}}var i=["-webkit-","-moz-",""],a=/linear-gradient|radial-gradient|repeating-linear-gradient|repeating-radial-gradient/;function n(e,r){if("string"==typeof r&&!(0,t.default)(r)&&a.test(r))return i.map(function(e){return e+r})}module.exports=exports.default; +},{"css-in-js-utils/lib/isPrefixedValue":158}],131:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=u;var e=require("css-in-js-utils/lib/isPrefixedValue"),t=r(e);function r(e){return e&&e.__esModule?e:{default:e}}var i=["-webkit-",""];function u(e,r){if("string"==typeof r&&!(0,t.default)(r)&&r.indexOf("image-set(")>-1)return i.map(function(e){return r.replace(/image-set\(/g,e+"image-set(")})}module.exports=exports.default; +},{"css-in-js-utils/lib/isPrefixedValue":158}],132:[function(require,module,exports) { +"use strict";function e(e,t){if("position"===e&&"sticky"===t)return["-webkit-sticky","sticky"]}Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=e,module.exports=exports.default; +},{}],133:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=i;var t=["-webkit-","-moz-",""],e={maxHeight:!0,maxWidth:!0,width:!0,height:!0,columnWidth:!0,minWidth:!0,minHeight:!0},n={"min-content":!0,"max-content":!0,"fill-available":!0,"fit-content":!0,"contain-floats":!0};function i(i,o){if(e.hasOwnProperty(i)&&n.hasOwnProperty(o))return t.map(function(t){return t+o})}module.exports=exports.default; +},{}],181:[function(require,module,exports) { +"use strict";var e=/[A-Z]/g,r=/^ms-/,s={};function t(t){return t in s?s[t]:s[t]=t.replace(e,"-$&").toLowerCase().replace(r,"-ms-")}module.exports=t; +},{}],165:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=u;var e=require("hyphenate-style-name"),t=r(e);function r(e){return e&&e.__esModule?e:{default:e}}function u(e){return(0,t.default)(e)}module.exports=exports.default; +},{"hyphenate-style-name":181}],164:[function(require,module,exports) { +"use strict";function e(e){return e.charAt(0).toUpperCase()+e.slice(1)}Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=e,module.exports=exports.default; +},{}],134:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=l;var t=require("css-in-js-utils/lib/hyphenateProperty"),e=s(t),r=require("css-in-js-utils/lib/isPrefixedValue"),i=s(r),n=require("../../utils/capitalizeString"),o=s(n);function s(t){return t&&t.__esModule?t:{default:t}}var u={transition:!0,transitionProperty:!0,WebkitTransition:!0,WebkitTransitionProperty:!0,MozTransition:!0,MozTransitionProperty:!0},a={Webkit:"-webkit-",Moz:"-moz-",ms:"-ms-"};function f(t,r){if((0,i.default)(t))return t;for(var n=t.split(/,(?![^()]*(?:\([^()]*\))?\))/g),o=0,s=n.length;o-1&&"order"!==p)for(var d=r[l],c=0,b=d.length;c-1)return s;var a=n.split(/,(?![^()]*(?:\([^()]*\))?\))/g).filter(function(t){return!/-webkit-|-ms-/.test(t)}).join(",");return t.indexOf("Moz")>-1?a:(r["Webkit"+(0,o.default)(t)]=s,r["Moz"+(0,o.default)(t)]=a,n)}}module.exports=exports.default; +},{"css-in-js-utils/lib/hyphenateProperty":165,"css-in-js-utils/lib/isPrefixedValue":158,"../../utils/capitalizeString":164}],144:[function(require,module,exports) { +"use strict";function r(r){for(var t=5381,e=r.length;e;)t=33*t^r.charCodeAt(--e);return t>>>0}module.exports=r; +},{}],168:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=u;var e=require("./capitalizeString"),r=t(e);function t(e){return e&&e.__esModule?e:{default:e}}function u(e,t,u){if(e.hasOwnProperty(t)){for(var o={},a=e[t],n=(0,r.default)(t),f=Object.keys(u),l=0;l0&&(i[s]=d)}else{var x=(0,u.default)(l,s,n,i,t);x&&(i[s]=x),i=(0,r.default)(t,s,i)}}return i}}module.exports=exports.default; +},{"../utils/prefixProperty":168,"../utils/prefixValue":169,"../utils/addNewValuesOnly":170,"../utils/isObject":171}],166:[function(require,module,exports) { +var global = arguments[3]; +var e=arguments[3];function t(e){r.length||(n(),a=!0),r[r.length]=e}module.exports=t;var n,r=[],a=!1,o=0,u=1024;function l(){for(;ou){for(var t=0,n=r.length-o;t4&&void 0!==arguments[4]?arguments[4]:[];if(!pe[e]){var o=ne(t,r,i,ye,n);he(e,o)}},Se=function(){de=[],pe={},me=!1,fe=null},ve=function(){return de},xe=function(){if(me)throw new Error("Cannot buffer while already buffering");me=!0},be=function(){me=!1;var e=de;return de=[],e},ke=function(){return be().join("")},Oe=function(){var e=be();e.length>0&&ce(e)},we=function(){return Object.keys(pe)},Ae=function(e){e.forEach(function(e){pe[e]=!0})},Ce=function e(t,r,n,i){for(var o=0;o0&&void 0!==arguments[0]?arguments[0]:[];Ae(e)}}()},Me="undefined"!=typeof window?null:{renderStatic:function(){return function(e){return Se(),xe(),{html:e(),css:{content:ke(),renderedClassNames:we()}}}}()},qe=null;function Fe(e,t){return{StyleSheet:Object.assign({},Re,{extend:function(){return function(r){var n=r.map(function(e){return e.selectorHandler}).filter(function(e){return e});return Fe(e,t.concat(n))}}()}),StyleSheetServer:Me,StyleSheetTestUtils:qe,minify:function(){return function(e){Ie=e?V:Te}}(),css:function(){return function(){for(var r=arguments.length,n=Array(r),i=0;i{this.viewport=e||null}),this.pendingScroll=0,this.onWindowResize=(()=>{this.recomputeVisibleIndices(this.props)}),this.onViewportScroll=(e=>{this.recomputeVisibleIndices(this.props)}),this.state={firstVisibleIndex:null,lastVisibleIndex:null,invisiblePrefixSize:null,viewportSize:null,cachedTotalSize:e.items.reduce((e,i)=>e+i.size,0)}}recomputeVisibleIndices(e){if(!this.viewport)return;const{items:i}=e,t=this.viewport.getBoundingClientRect().height,s=this.viewport.scrollTop-t/4,o=this.viewport.scrollTop+t+t/4;let l=0,r=0,n=0;for(;n=s)break}const p=n;for(;n=o)break}const c=Math.min(n,i.length-1);this.setState({invisiblePrefixSize:r,firstVisibleIndex:p,lastVisibleIndex:c})}scrollIndexIntoView(e){this.pendingScroll=this.props.items.reduce((i,t,s)=>s>=e?i:i+t.size,0)}applyPendingScroll(){if(!this.viewport)return;const e="y"===this.props.axis?"top":"left";this.viewport.scrollTo({[e]:this.pendingScroll})}componentWillReceiveProps(e){this.props.items!==e.items&&this.recomputeVisibleIndices(e)}componentDidMount(){this.applyPendingScroll(),this.recomputeVisibleIndices(this.props),window.addEventListener("resize",this.onWindowResize)}componentWillUnmount(){window.removeEventListener("resize",this.onWindowResize)}render(){const{cachedTotalSize:i,firstVisibleIndex:t,lastVisibleIndex:s,invisiblePrefixSize:o}=this.state;return(0,e.h)("div",{className:this.props.className,ref:this.viewportRef,onScroll:this.onViewportScroll},(0,e.h)("div",{style:{height:i}},(0,e.h)("div",{style:{transform:`translateY(${o}px)`}},null!=t&&null!=s&&this.props.renderItems(t,s))))}}exports.ScrollableListView=i; +},{"preact":24}],117:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0});class t{constructor(t){this.data=t,this.prev=null,this.next=null}}class e{constructor(){this.head=null,this.tail=null,this.size=0}getHead(){return this.head}getTail(){return this.tail}getSize(){return this.size}append(t){this.tail?(this.tail.next=t,t.prev=this.tail,this.tail=t):this.head=this.tail=t,this.size++}prepend(t){return this.head?(this.head.prev=t,t.next=this.head,this.head=t):this.head=this.tail=t,this.size++,t}pop(){if(this.tail){const t=this.tail;return t.prev?(this.tail=t.prev,this.tail.next=null):this.head=this.tail=null,this.size--,t.prev=null,t}return null}dequeue(){if(this.head){const t=this.head;return t.next?(this.head=t.next,this.head.prev=null):this.head=this.tail=null,this.size--,t.next=null,t}return null}remove(t){null==t.prev?this.dequeue():null==t.next?this.pop():(t.next.prev=t.prev,t.prev.next=t.next,t.next=null,t.prev=null,this.size--)}}exports.List=e;class i{constructor(t){this.capacity=t,this.list=new e,this.map=new Map}has(t){return this.map.has(t)}get(t){const e=this.map.get(t);return e?(this.list.remove(e.listNode),this.list.prepend(e.listNode),e?e.value:null):null}getSize(){return this.list.getSize()}getCapacity(){return this.capacity}insert(e,i){const s=this.map.get(e);for(s&&this.list.remove(s.listNode);this.list.getSize()>=this.capacity;)this.map.delete(this.list.pop().data);const h=this.list.prepend(new t(e));this.map.set(e,{value:i,listNode:h})}getOrInsert(t,e){let i=this.get(t);return null==i&&(i=e(t),this.insert(t,i)),i}removeLRU(){const t=this.list.pop();if(!t)return null;const e=t.data,i=this.map.get(e).value;return this.map.delete(e),[e,i]}clear(){this.list=new e,this.map=new Map}}exports.LRUCache=i; +},{}],96:[function(require,module,exports) { + +var t,e,n=module.exports={};function r(){throw new Error("setTimeout has not been defined")}function o(){throw new Error("clearTimeout has not been defined")}function i(e){if(t===setTimeout)return setTimeout(e,0);if((t===r||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(n){try{return t.call(null,e,0)}catch(n){return t.call(this,e,0)}}}function u(t){if(e===clearTimeout)return clearTimeout(t);if((e===o||!e)&&clearTimeout)return e=clearTimeout,clearTimeout(t);try{return e(t)}catch(n){try{return e.call(null,t)}catch(n){return e.call(this,t)}}}!function(){try{t="function"==typeof setTimeout?setTimeout:r}catch(e){t=r}try{e="function"==typeof clearTimeout?clearTimeout:o}catch(t){e=o}}();var c,s=[],l=!1,a=-1;function f(){l&&c&&(l=!1,c.length?s=c.concat(s):a=-1,s.length&&h())}function h(){if(!l){var t=i(f);l=!0;for(var e=s.length;e;){for(c=s,s=[];++a1)for(var n=1;n=0&&e<=31),t.TEXTURE0+e}var h=exports.Graphics=void 0;!function(t){t.Rect=class{constructor(t=0,e=0,i=0,r=0){this.x=t,this.y=e,this.width=i,this.height=r}set(t,e,i,r){this.x=t,this.y=e,this.width=i,this.height=r}equals(t){return this.x===t.x&&this.y===t.y&&this.width===t.width&&this.height===t.height}};class e{constructor(t,e,i,r){this.redF=t,this.greenF=e,this.blueF=i,this.alphaF=r}}let i,r,s,n,h;e.TRANSPARENT=new e(0,0,0,0),t.Color=e,function(t){t[t.ZERO=0]="ZERO",t[t.ONE=1]="ONE",t[t.SOURCE_COLOR=2]="SOURCE_COLOR",t[t.TARGET_COLOR=3]="TARGET_COLOR",t[t.INVERSE_SOURCE_COLOR=4]="INVERSE_SOURCE_COLOR",t[t.INVERSE_TARGET_COLOR=5]="INVERSE_TARGET_COLOR",t[t.SOURCE_ALPHA=6]="SOURCE_ALPHA",t[t.TARGET_ALPHA=7]="TARGET_ALPHA",t[t.INVERSE_SOURCE_ALPHA=8]="INVERSE_SOURCE_ALPHA",t[t.INVERSE_TARGET_ALPHA=9]="INVERSE_TARGET_ALPHA",t[t.CONSTANT=10]="CONSTANT",t[t.INVERSE_CONSTANT=11]="INVERSE_CONSTANT"}(i=t.BlendOperation||(t.BlendOperation={})),function(t){t[t.TRIANGLES=0]="TRIANGLES",t[t.TRIANGLE_STRIP=1]="TRIANGLE_STRIP"}(r=t.Primitive||(t.Primitive={}));function a(t){return t==s.FLOAT?4:1}t.Context=class{setCopyBlendState(){this.setBlendState(i.ONE,i.ZERO)}setAddBlendState(){this.setBlendState(i.ONE,i.ONE)}setPremultipliedBlendState(){this.setBlendState(i.ONE,i.INVERSE_SOURCE_ALPHA)}setUnpremultipliedBlendState(){this.setBlendState(i.SOURCE_ALPHA,i.INVERSE_SOURCE_ALPHA)}},function(t){t[t.FLOAT=0]="FLOAT",t[t.BYTE=1]="BYTE"}(s=t.AttributeType||(t.AttributeType={})),t.attributeByteLength=a;class _{constructor(t,e,i,r){this.name=t,this.type=e,this.count=i,this.byteOffset=r}}t.Attribute=_;t.VertexFormat=class{constructor(){this._attributes=[],this._stride=0}get attributes(){return this._attributes}get stride(){return this._stride}add(t,e,i){return this.attributes.push(new _(t,e,i,this.stride)),this._stride+=i*a(e),this}};t.VertexBuffer=class{uploadFloat32Array(t){this.upload(new Uint8Array(t.buffer),0)}uploadFloats(t){this.uploadFloat32Array(new Float32Array(t))}},function(t){t[t.NEAREST=0]="NEAREST",t[t.LINEAR=1]="LINEAR"}(n=t.PixelFilter||(t.PixelFilter={})),function(t){t[t.REPEAT=0]="REPEAT",t[t.CLAMP=1]="CLAMP"}(h=t.PixelWrap||(t.PixelWrap={}));class o{constructor(t,e,i){this.minFilter=t,this.magFilter=e,this.wrap=i}}o.LINEAR_CLAMP=new o(n.LINEAR,n.LINEAR,h.CLAMP),o.LINEAR_MIN_NEAREST_MAG_CLAMP=new o(n.LINEAR,n.NEAREST,h.CLAMP),o.NEAREST_CLAMP=new o(n.NEAREST,n.NEAREST,h.CLAMP),t.TextureFormat=o}(h||(exports.Graphics=h={}));var a=exports.WebGL=void 0;!function(t){class a extends h.Context{constructor(t=document.createElement("canvas")){super(),this._attributeCount=0,this._blendOperations=0,this._contextResetHandlers=[],this._currentClearColor=h.Color.TRANSPARENT,this._currentRenderTarget=null,this._defaultViewport=new h.Rect,this._forceStateUpdate=!0,this._generation=1,this._height=0,this._oldBlendOperations=0,this._oldRenderTarget=null,this._oldViewport=new h.Rect,this._width=0,this.handleWebglContextRestored=(()=>{this._attributeCount=0,this._currentClearColor=h.Color.TRANSPARENT,this._forceStateUpdate=!0,this._generation++;for(let t of this._contextResetHandlers)t()}),this.ANGLE_instanced_arrays=null,this.ANGLE_instanced_arrays_generation=-1;let e=t.getContext("webgl",{alpha:!1,antialias:!1,depth:!1,preserveDrawingBuffer:!1,stencil:!1});if(null==e)throw new Error("Setup failure");this._gl=e;let i=t.style;t.width=0,t.height=0,i.width=i.height="0",t.addEventListener("webglcontextlost",t=>{t.preventDefault()}),t.addEventListener("webglcontextrestored",this.handleWebglContextRestored),this._blendOperationMap={[h.BlendOperation.ZERO]:this._gl.ZERO,[h.BlendOperation.ONE]:this._gl.ONE,[h.BlendOperation.SOURCE_COLOR]:this._gl.SRC_COLOR,[h.BlendOperation.TARGET_COLOR]:this._gl.DST_COLOR,[h.BlendOperation.INVERSE_SOURCE_COLOR]:this._gl.ONE_MINUS_SRC_COLOR,[h.BlendOperation.INVERSE_TARGET_COLOR]:this._gl.ONE_MINUS_DST_COLOR,[h.BlendOperation.SOURCE_ALPHA]:this._gl.SRC_ALPHA,[h.BlendOperation.TARGET_ALPHA]:this._gl.DST_ALPHA,[h.BlendOperation.INVERSE_SOURCE_ALPHA]:this._gl.ONE_MINUS_SRC_ALPHA,[h.BlendOperation.INVERSE_TARGET_ALPHA]:this._gl.ONE_MINUS_DST_ALPHA,[h.BlendOperation.CONSTANT]:this._gl.CONSTANT_COLOR,[h.BlendOperation.INVERSE_CONSTANT]:this._gl.ONE_MINUS_CONSTANT_COLOR}}get widthInPixels(){return this._width}get heightInPixels(){return this._height}testContextLoss(){this.handleWebglContextRestored()}get gl(){return this._gl}get generation(){return this._generation}addContextResetHandler(t){r(this._contextResetHandlers,t)}removeContextResetHandler(t){s(this._contextResetHandlers,t)}get currentRenderTarget(){return this._currentRenderTarget}beginFrame(){this.setRenderTarget(null)}endFrame(){}setBlendState(t,e){this._blendOperations=a._packBlendModes(t,e)}setViewport(t,e,i,r){(null!=this._currentRenderTarget?this._currentRenderTarget.viewport:this._defaultViewport).set(t,e,i,r)}get viewport(){return null!=this._currentRenderTarget?this._currentRenderTarget.viewport:this._defaultViewport}get renderTargetWidthInPixels(){return null!=this._currentRenderTarget?this._currentRenderTarget.viewport.width:this._width}get renderTargetHeightInPixels(){return null!=this._currentRenderTarget?this._currentRenderTarget.viewport.height:this._height}draw(t,e,i){this._updateRenderTargetAndViewport(),f.from(e).prepare(),R.from(i).prepare(),this._updateFormat(e.format),this._updateBlendState(),this._gl.drawArrays(t==h.Primitive.TRIANGLES?this._gl.TRIANGLES:this._gl.TRIANGLE_STRIP,0,Math.floor(i.byteCount/e.format.stride)),this._forceStateUpdate=!1}resize(t,e,i,r){let s=this._gl.canvas;const n=s.getBoundingClientRect();if(this._width===i&&this._height===e&&n.width===i&&n.height===r)return;let h=s.style;s.width=t,s.height=e,h.width=`${i}px`,h.height=`${r}px`,this.setViewport(0,0,t,e),this._width=t,this._height=e}clear(t){this._updateRenderTargetAndViewport(),this._updateBlendState(),t!=this._currentClearColor&&(this._gl.clearColor(t.redF,t.greenF,t.blueF,t.alphaF),this._currentClearColor=t),this._gl.clear(this._gl.COLOR_BUFFER_BIT)}setRenderTarget(t){this._currentRenderTarget=A.from(t)}createMaterial(t,e,i){let r=new f(this,t,e,i);return r.program,r}createVertexBuffer(t){return i(t>0&&t%4==0),new R(this,t)}createTexture(t,e,i,r){return new p(this,t,e,i,r)}createRenderTarget(t){return new A(this,p.from(t))}getANGLE_instanced_arrays(){if(this.ANGLE_instanced_arrays_generation!==this._generation&&(this.ANGLE_instanced_arrays=null),!this.ANGLE_instanced_arrays&&(this.ANGLE_instanced_arrays=this.gl.getExtension("ANGLE_instanced_arrays"),!this.ANGLE_instanced_arrays))throw new Error("Failed to get extension ANGLE_instanced_arrays");return this.ANGLE_instanced_arrays}_updateRenderTargetAndViewport(){let t=this._currentRenderTarget,e=null!=t?t.viewport:this._defaultViewport,i=this._gl;(this._forceStateUpdate||this._oldRenderTarget!=t)&&(i.bindFramebuffer(i.FRAMEBUFFER,t?t.framebuffer:null),this._oldRenderTarget=t),!this._forceStateUpdate&&this._oldViewport.equals(e)||(i.viewport(e.x,this.renderTargetHeightInPixels-e.y-e.height,e.width,e.height),this._oldViewport.set(e.x,e.y,e.width,e.height))}_updateBlendState(){if(this._forceStateUpdate||this._oldBlendOperations!=this._blendOperations){let t=this._gl,e=this._blendOperations,r=this._oldBlendOperations,s=15&e,n=e>>4;i(s in this._blendOperationMap),i(n in this._blendOperationMap),e==a.COPY_BLEND_OPERATIONS?t.disable(t.BLEND):((this._forceStateUpdate||r==a.COPY_BLEND_OPERATIONS)&&t.enable(t.BLEND),t.blendFunc(this._blendOperationMap[s],this._blendOperationMap[n])),this._oldBlendOperations=e}}_updateFormat(t){let e=this._gl,i=t.attributes,r=i.length;for(let s=0;sr;)this._attributeCount--,e.disableVertexAttribArray(this._attributeCount);this._attributeCount=r}getWebGLInfo(){const t=this.gl.getExtension("WEBGL_debug_renderer_info");return{renderer:t?this.gl.getParameter(t.UNMASKED_RENDERER_WEBGL):null,vendor:t?this.gl.getParameter(t.UNMASKED_VENDOR_WEBGL):null,version:this.gl.getParameter(this.gl.VERSION)}}static from(t){return i(null==t||t instanceof a),t}static _packBlendModes(t,e){return t|e<<4}}a.COPY_BLEND_OPERATIONS=a._packBlendModes(h.BlendOperation.ONE,h.BlendOperation.ZERO),t.Context=a;class _{constructor(t,e,i=0,r=null,s=!0){this._material=t,this._name=e,this._generation=i,this._location=r,this._isDirty=s}get location(){let t=a.from(this._material.context);if(this._generation!=t.generation&&(this._location=t.gl.getUniformLocation(this._material.program,this._name),this._generation=t.generation,!e)){let e=this._material.program,r=t.gl;for(let t=0,s=r.getProgramParameter(e,r.ACTIVE_UNIFORMS);t0&&this._texture.height>0?this._texture.texture:null)}}class f{constructor(t,e,i,r,s={},n=[],h=0,a=null){this._context=t,this._format=e,this._vertexSource=i,this._fragmentSource=r,this._uniformsMap=s,this._uniformsList=n,this._generation=h,this._program=a}get context(){return this._context}get format(){return this._format}get vertexSource(){return this._vertexSource}get fragmentSource(){return this._fragmentSource}setUniformFloat(t,e){let r=this._uniformsMap[t]||null;null==r&&(r=new o(this,t),this._uniformsMap[t]=r,this._uniformsList.push(r)),i(r instanceof o),r.set(e)}setUniformInt(t,e){let r=this._uniformsMap[t]||null;null==r&&(r=new l(this,t),this._uniformsMap[t]=r,this._uniformsList.push(r)),i(r instanceof l),r.set(e)}setUniformVec2(t,e,r){let s=this._uniformsMap[t]||null;null==s&&(s=new u(this,t),this._uniformsMap[t]=s,this._uniformsList.push(s)),i(s instanceof u),s.set(e,r)}setUniformVec3(t,e,r,s){let n=this._uniformsMap[t]||null;null==n&&(n=new c(this,t),this._uniformsMap[t]=n,this._uniformsList.push(n)),i(n instanceof c),n.set(e,r,s)}setUniformVec4(t,e,r,s,n){let h=this._uniformsMap[t]||null;null==h&&(h=new d(this,t),this._uniformsMap[t]=h,this._uniformsList.push(h)),i(h instanceof d),h.set(e,r,s,n)}setUniformMat3(t,e,r,s,n,h,a,_,o,l){let u=this._uniformsMap[t]||null;null==u&&(u=new g(this,t),this._uniformsMap[t]=u,this._uniformsList.push(u)),i(u instanceof g),u.set(e,r,s,n,h,a,_,o,l)}setUniformSampler(t,e,r){let s=this._uniformsMap[t]||null;null==s&&(s=new E(this,t),this._uniformsMap[t]=s,this._uniformsList.push(s)),i(s instanceof E),s.set(e,r)}get program(){let t=this._context.gl;if(this._generation!=this._context.generation){this._program=t.createProgram(),this._compileShader(t,t.VERTEX_SHADER,this.vertexSource),this._compileShader(t,t.FRAGMENT_SHADER,this.fragmentSource);let r=this.format.attributes;for(let e=0;e=0),i(0<=t&&t+r<=this._byteCount),i(0<=e&&e+r<=this._byteCount),this._bytes&&t!=e&&0!=r&&(this._bytes.set(this._bytes.subarray(t,this._byteCount),e),this._growDirtyRegion(Math.min(t,e),Math.max(t,e)+r))}upload(t,e=0){i(0<=e&&e+t.length<=this._byteCount),i(null!=this._bytes),this._bytes.set(t,e),this._growDirtyRegion(e,e+t.length)}free(){this._buffer&&this._context.gl.deleteBuffer(this._buffer),this._generation=0}prepare(){let t=this._context.gl;this._generation!==this._context.generation&&(this._buffer=t.createBuffer(),this._generation=this._context.generation,this._isDirty=!0),t.bindBuffer(t.ARRAY_BUFFER,this._buffer),this._isDirty&&(t.bufferData(t.ARRAY_BUFFER,this._byteCount,t.DYNAMIC_DRAW),this._dirtyMin=this._totalMin,this._dirtyMax=this._totalMax,this._isDirty=!1),this._dirtyMin{const t=e.AffineTransform.betweenRects(i.configSpaceSrcRect,i.physicalSpaceDstRect),r=new e.Vec2(this.gl.viewport.width,this.gl.viewport.height);return e.AffineTransform.withTranslation(new e.Vec2(-1,1)).times(e.AffineTransform.withScale(new e.Vec2(2,-2).dividedByPointwise(r))).times(t)})()),this.gl.setUnpremultipliedBlendState(),this.gl.draw(t.Graphics.Primitive.TRIANGLES,this.material,i.batch.getBuffer())}}exports.RectangleBatchRenderer=c; +},{"../lib/math":102,"./graphics":42,"./utils":119}],76:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Color=void 0;var t=require("./math");class r{constructor(t=0,r=0,e=0,o=1){this.r=t,this.g=r,this.b=e,this.a=o}static fromLumaChromaHue(e,o,s){const i=s/60,a=o*(1-Math.abs(i%2-1)),[h,c,u]=i<1?[o,a,0]:i<2?[a,o,0]:i<3?[0,o,a]:i<4?[0,a,o]:i<5?[a,0,o]:[o,0,a],l=e-(.3*h+.59*c+.11*u);return new r((0,t.clamp)(h+l,0,1),(0,t.clamp)(c+l,0,1),(0,t.clamp)(u+l,0,1),1)}toCSS(){return`rgba(${(255*this.r).toFixed()}, ${(255*this.g).toFixed()}, ${(255*this.b).toFixed()}, ${this.a.toFixed(2)})`}}exports.Color=r; +},{"./math":102}],72:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.RowAtlas=void 0;var e=require("../lib/lru-cache"),t=require("./rectangle-batch-renderer"),r=require("../lib/math"),i=require("../lib/color"),c=require("./graphics"),h=require("./utils");class a{constructor(h,a,s){this.gl=h,this.rectangleBatchRenderer=a,this.textureRenderer=s,this.texture=h.createTexture(c.Graphics.TextureFormat.NEAREST_CLAMP,4096,4096),this.renderTarget=h.createRenderTarget(this.texture),this.rowCache=new e.LRUCache(this.texture.height),this.clearLineBatch=new t.RectangleBatch(h),this.clearLineBatch.addRect(r.Rect.unit,new i.Color(0,0,0,0)),h.addContextResetHandler(()=>{this.rowCache.clear()})}has(e){return this.rowCache.has(e)}getResolution(){return this.texture.width}getCapacity(){return this.texture.height}allocateLine(e){if(this.rowCache.getSize(){for(let i of e){let e=this.rowCache.get(i);if(null!=e)continue;e=this.allocateLine(i);const c=new r.Rect(new r.Vec2(0,e),new r.Vec2(this.texture.width,1));this.rectangleBatchRenderer.render({batch:this.clearLineBatch,configSpaceSrcRect:r.Rect.unit,physicalSpaceDstRect:c}),t(c,i)}})}renderViaAtlas(e,t){let i=this.rowCache.get(e);if(null==i)return!1;const c=new r.Rect(new r.Vec2(0,i),new r.Vec2(this.texture.width,1));return this.textureRenderer.render({texture:this.texture,srcRect:c,dstRect:t}),!0}}exports.RowAtlas=a; +},{"../lib/lru-cache":117,"./rectangle-batch-renderer":118,"../lib/math":102,"../lib/color":76,"./graphics":42,"./utils":119}],120:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.TextureRenderer=void 0;var e=require("../lib/math"),t=require("./graphics"),r=require("./utils");const n="\n uniform mat3 uvTransform;\n uniform mat3 positionTransform;\n\n attribute vec2 position;\n attribute vec2 uv;\n varying vec2 vUv;\n\n void main() {\n vUv = (uvTransform * vec3(uv, 1)).xy;\n gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1);\n }\n",i="\n precision mediump float;\n\n varying vec2 vUv;\n uniform sampler2D texture;\n\n void main() {\n gl_FragColor = texture2D(texture, vUv);\n }\n";class s{constructor(e){this.gl=e;const r=new t.Graphics.VertexFormat;r.add("position",t.Graphics.AttributeType.FLOAT,2),r.add("uv",t.Graphics.AttributeType.FLOAT,2);const s=[{pos:[-1,1],uv:[0,1]},{pos:[1,1],uv:[1,1]},{pos:[-1,-1],uv:[0,0]},{pos:[1,-1],uv:[1,0]}],o=[];for(let e of s)o.push(e.pos[0]),o.push(e.pos[1]),o.push(e.uv[0]),o.push(e.uv[1]);this.buffer=e.createVertexBuffer(r.stride*s.length),this.buffer.upload(new Uint8Array(new Float32Array(o).buffer)),this.material=e.createMaterial(r,n,i)}render(n){this.material.setUniformSampler("texture",n.texture,0),(0,r.setUniformAffineTransform)(this.material,"uvTransform",(()=>{const{srcRect:t,texture:r}=n,i=e.AffineTransform.withTranslation(new e.Vec2(0,1)).times(e.AffineTransform.withScale(new e.Vec2(1,-1))).times(e.AffineTransform.betweenRects(new e.Rect(e.Vec2.zero,new e.Vec2(r.width,r.height)),e.Rect.unit)).transformRect(t);return e.AffineTransform.betweenRects(e.Rect.unit,i)})()),(0,r.setUniformAffineTransform)(this.material,"positionTransform",(()=>{const{dstRect:t}=n,{viewport:r}=this.gl,i=new e.Vec2(r.width,r.height),s=e.AffineTransform.withScale(new e.Vec2(1,-1)).times(e.AffineTransform.betweenRects(new e.Rect(e.Vec2.zero,i),e.Rect.NDC)).transformRect(t);return e.AffineTransform.betweenRects(e.Rect.NDC,s)})()),this.gl.setUnpremultipliedBlendState(),this.gl.draw(t.Graphics.Primitive.TRIANGLE_STRIP,this.material,this.buffer)}}exports.TextureRenderer=s; +},{"../lib/math":102,"./graphics":42,"./utils":119}],121:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ViewportRectangleRenderer=void 0;var e=require("./graphics"),i=require("./utils");const r=new e.Graphics.VertexFormat;r.add("position",e.Graphics.AttributeType.FLOAT,2);const o="\n attribute vec2 position;\n\n void main() {\n gl_Position = vec4(position, 0, 1);\n }\n",n="\n precision mediump float;\n\n uniform mat3 configSpaceToPhysicalViewSpace;\n uniform vec2 physicalSize;\n uniform vec2 physicalOrigin;\n uniform vec2 configSpaceViewportOrigin;\n uniform vec2 configSpaceViewportSize;\n uniform float framebufferHeight;\n\n void main() {\n vec2 origin = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportOrigin, 1.0)).xy;\n vec2 size = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportSize, 0.0)).xy;\n\n vec2 halfSize = physicalSize / 2.0;\n\n float borderWidth = 2.0;\n\n origin = floor(origin * halfSize) / halfSize + borderWidth * vec2(1.0, 1.0);\n size = floor(size * halfSize) / halfSize - 2.0 * borderWidth * vec2(1.0, 1.0);\n\n vec2 coord = gl_FragCoord.xy;\n coord.x = coord.x - physicalOrigin.x;\n coord.y = framebufferHeight - coord.y - physicalOrigin.y;\n vec2 clamped = clamp(coord, origin, origin + size);\n vec2 gap = clamped - coord;\n float maxdist = max(abs(gap.x), abs(gap.y));\n\n // TOOD(jlfwong): Could probably optimize this to use mix somehow.\n if (maxdist == 0.0) {\n // Inside viewport rectangle\n gl_FragColor = vec4(0, 0, 0, 0);\n } else if (maxdist < borderWidth) {\n // Inside viewport rectangle at border\n gl_FragColor = vec4(0.7, 0.7, 0.7, 0.8);\n } else {\n // Outside viewport rectangle\n gl_FragColor = vec4(0.7, 0.7, 0.7, 0.5);\n }\n }\n";class t{constructor(e){this.gl=e;const i=[[-1,1],[1,1],[-1,-1],[1,-1]],t=[];for(let e of i)t.push(e[0]),t.push(e[1]);this.buffer=e.createVertexBuffer(r.stride*i.length),this.buffer.upload(new Uint8Array(new Float32Array(t).buffer)),this.material=e.createMaterial(r,o,n)}render(r){(0,i.setUniformAffineTransform)(this.material,"configSpaceToPhysicalViewSpace",r.configSpaceToPhysicalViewSpace),(0,i.setUniformVec2)(this.material,"configSpaceViewportOrigin",r.configSpaceViewportRect.origin),(0,i.setUniformVec2)(this.material,"configSpaceViewportSize",r.configSpaceViewportRect.size);const o=this.gl.viewport;this.material.setUniformVec2("physicalOrigin",o.x,o.y),this.material.setUniformVec2("physicalSize",o.width,o.height),this.material.setUniformFloat("framebufferHeight",this.gl.renderTargetHeightInPixels),this.gl.setBlendState(e.Graphics.BlendOperation.SOURCE_ALPHA,e.Graphics.BlendOperation.INVERSE_SOURCE_ALPHA),this.gl.draw(e.Graphics.Primitive.TRIANGLE_STRIP,this.material,this.buffer)}}exports.ViewportRectangleRenderer=t; +},{"./graphics":42,"./utils":119}],122:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.FlamechartColorPassRenderer=void 0;var e=require("../lib/math"),t=require("./graphics"),n=require("./utils");const r=new t.Graphics.VertexFormat;r.add("position",t.Graphics.AttributeType.FLOAT,2),r.add("uv",t.Graphics.AttributeType.FLOAT,2);const i="\n uniform mat3 uvTransform;\n uniform mat3 positionTransform;\n\n attribute vec2 position;\n attribute vec2 uv;\n varying vec2 vUv;\n\n void main() {\n vUv = (uvTransform * vec3(uv, 1)).xy;\n gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1);\n }\n",o="\n precision mediump float;\n\n uniform vec2 uvSpacePixelSize;\n uniform float renderOutlines;\n\n varying vec2 vUv;\n uniform sampler2D colorTexture;\n\n // https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue\n vec3 hcl2rgb(float H, float C, float L) {\n float hPrime = H / 60.0;\n float X = C * (1.0 - abs(mod(hPrime, 2.0) - 1.0));\n vec3 RGB =\n hPrime < 1.0 ? vec3(C, X, 0) :\n hPrime < 2.0 ? vec3(X, C, 0) :\n hPrime < 3.0 ? vec3(0, C, X) :\n hPrime < 4.0 ? vec3(0, X, C) :\n hPrime < 5.0 ? vec3(X, 0, C) :\n vec3(C, 0, X);\n\n float m = L - dot(RGB, vec3(0.30, 0.59, 0.11));\n return RGB + vec3(m, m, m);\n }\n\n float triangle(float x) {\n return 2.0 * abs(fract(x) - 0.5) - 1.0;\n }\n\n vec3 colorForBucket(float t) {\n float x = triangle(30.0 * t);\n float H = 360.0 * (0.9 * t);\n float C = 0.25 + 0.2 * x;\n float L = 0.80 - 0.15 * x;\n return hcl2rgb(H, C, L);\n }\n\n void main() {\n vec4 here = texture2D(colorTexture, vUv);\n\n if (here.z == 0.0) {\n // Background color\n gl_FragColor = vec4(0, 0, 0, 0);\n return;\n }\n\n // Sample the 4 surrounding pixels in the depth texture to determine\n // if we should draw a boundary here or not.\n vec4 N = texture2D(colorTexture, vUv + vec2(0, uvSpacePixelSize.y));\n vec4 E = texture2D(colorTexture, vUv + vec2(uvSpacePixelSize.x, 0));\n vec4 S = texture2D(colorTexture, vUv + vec2(0, -uvSpacePixelSize.y));\n vec4 W = texture2D(colorTexture, vUv + vec2(-uvSpacePixelSize.x, 0));\n\n // NOTE: For outline checks, we intentionally check both the right\n // and the left to determine if we're an edge. If a rectangle is a single\n // pixel wide, we don't want to render it as an outline, so this method\n // of checking ensures that we don't outline single physical-space\n // pixel width rectangles.\n if (\n renderOutlines > 0.0 &&\n (\n here.y == N.y && here.y != S.y || // Top edge\n here.y == S.y && here.y != N.y || // Bottom edge\n here.x == E.x && here.x != W.x || // Left edge\n here.x == W.x && here.x != E.x\n )\n ) {\n // We're on an edge! Draw transparent.\n gl_FragColor = vec4(0, 0, 0, 0);\n } else {\n // Not on an edge. Draw the appropriate color.\n gl_FragColor = vec4(colorForBucket(here.z), here.a);\n }\n }\n";class a{constructor(e){this.gl=e;const t=[{pos:[-1,1],uv:[0,1]},{pos:[1,1],uv:[1,1]},{pos:[-1,-1],uv:[0,0]},{pos:[1,-1],uv:[1,0]}],n=[];for(let e of t)n.push(e.pos[0]),n.push(e.pos[1]),n.push(e.uv[0]),n.push(e.uv[1]);this.buffer=e.createVertexBuffer(r.stride*t.length),this.buffer.uploadFloats(n),this.material=e.createMaterial(r,i,o)}render(r){const{srcRect:i,rectInfoTexture:o}=r,a=e.AffineTransform.withTranslation(new e.Vec2(0,1)).times(e.AffineTransform.withScale(new e.Vec2(1,-1))).times(e.AffineTransform.betweenRects(new e.Rect(e.Vec2.zero,new e.Vec2(o.width,o.height)),e.Rect.unit)).transformRect(i),s=e.AffineTransform.betweenRects(e.Rect.unit,a),{dstRect:c}=r,l=new e.Vec2(this.gl.viewport.width,this.gl.viewport.height),u=e.AffineTransform.withScale(new e.Vec2(1,-1)).times(e.AffineTransform.betweenRects(new e.Rect(e.Vec2.zero,l),e.Rect.NDC)).transformRect(c),f=e.AffineTransform.betweenRects(e.Rect.NDC,u),h=e.Vec2.unit.dividedByPointwise(new e.Vec2(r.rectInfoTexture.width,r.rectInfoTexture.height));this.material.setUniformSampler("colorTexture",r.rectInfoTexture,0),(0,n.setUniformAffineTransform)(this.material,"uvTransform",s),this.material.setUniformFloat("renderOutlines",r.renderOutlines?1:0),this.material.setUniformVec2("uvSpacePixelSize",h.x,h.y),(0,n.setUniformAffineTransform)(this.material,"positionTransform",f),this.gl.setUnpremultipliedBlendState(),this.gl.draw(t.Graphics.Primitive.TRIANGLE_STRIP,this.material,this.buffer)}}exports.FlamechartColorPassRenderer=a; +},{"../lib/math":102,"./graphics":42,"./utils":119}],74:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.CanvasContext=void 0;var e=require("./graphics"),r=require("./rectangle-batch-renderer"),t=require("./texture-renderer"),i=require("../lib/math"),n=require("./overlay-rectangle-renderer"),s=require("./flamechart-color-pass-renderer");class o{constructor(i){this.animationFrameRequest=null,this.beforeFrameHandlers=new Set,this.onBeforeFrame=(()=>{this.animationFrameRequest=null,this.gl.setViewport(0,0,this.gl.renderTargetWidthInPixels,this.gl.renderTargetHeightInPixels),this.gl.clear(new e.Graphics.Color(1,1,1,1));for(const e of this.beforeFrameHandlers)e()}),this.gl=new e.WebGL.Context(i),this.rectangleBatchRenderer=new r.RectangleBatchRenderer(this.gl),this.textureRenderer=new t.TextureRenderer(this.gl),this.viewportRectangleRenderer=new n.ViewportRectangleRenderer(this.gl),this.flamechartColorPassRenderer=new s.FlamechartColorPassRenderer(this.gl);const o=this.gl.getWebGLInfo();o&&console.log(`WebGL initialized. renderer: ${o.renderer}, vendor: ${o.vendor}, version: ${o.version}`),window.testContextLoss=(()=>{this.gl.testContextLoss()})}addBeforeFrameHandler(e){this.beforeFrameHandlers.add(e)}removeBeforeFrameHandler(e){this.beforeFrameHandlers.delete(e)}requestFrame(){this.animationFrameRequest||(this.animationFrameRequest=requestAnimationFrame(this.onBeforeFrame))}setViewport(e,r){const{origin:t,size:i}=e;let n=this.gl.viewport;this.gl.setViewport(t.x,t.y,i.x,i.y),r();let{x:s,y:o,width:a,height:l}=n;this.gl.setViewport(s,o,a,l)}renderBehind(e,r){const t=e.getBoundingClientRect(),n=new i.Rect(new i.Vec2(t.left*window.devicePixelRatio,t.top*window.devicePixelRatio),new i.Vec2(t.width*window.devicePixelRatio,t.height*window.devicePixelRatio));this.setViewport(n,r)}}exports.CanvasContext=o; +},{"./graphics":42,"./rectangle-batch-renderer":118,"./texture-renderer":120,"../lib/math":102,"./overlay-rectangle-renderer":121,"./flamechart-color-pass-renderer":122}],38:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getFrameToColorBucket=exports.getProfileToView=exports.getProfileWithRecursionFlattened=exports.getRowAtlas=exports.getCanvasContext=exports.createGetCSSColorForFrame=exports.createGetColorBucketForFrame=void 0;var e=require("../lib/utils"),t=require("../gl/row-atlas"),r=require("../gl/canvas-context"),o=require("../lib/color");const n=exports.createGetColorBucketForFrame=(0,e.memoizeByReference)(e=>t=>e.get(t.key)||0),a=exports.createGetCSSColorForFrame=(0,e.memoizeByReference)(t=>{const r=n(t);return t=>{const n=r(t)/255,a=(0,e.triangle)(30*n),l=.9*n*360,i=.25+.2*a,s=.8-.15*a;return o.Color.fromLumaChromaHue(s,i,l).toCSS()}}),l=exports.getCanvasContext=(0,e.memoizeByReference)(e=>new r.CanvasContext(e)),i=exports.getRowAtlas=(0,e.memoizeByReference)(e=>new t.RowAtlas(e.gl,e.rectangleBatchRenderer,e.textureRenderer)),s=exports.getProfileWithRecursionFlattened=(0,e.memoizeByReference)(e=>e.getProfileWithRecursionFlattened()),c=exports.getProfileToView=(0,e.memoizeByShallowEquality)(({profile:e,flattenRecursion:t})=>t?e.getProfileWithRecursionFlattened():e),u=exports.getFrameToColorBucket=(0,e.memoizeByReference)(e=>{const t=[];function r(e){return(e.file||"")+e.name}e.forEachFrame(e=>t.push(e)),t.sort(function(e,t){return r(e)>r(t)?1:-1});const o=new Map;for(let e=0;e{t.preventDefault();const{sortMethod:o}=this.props;if(o.field==e)this.props.setSortMethod({field:e,direction:o.direction===h.ASCENDING?h.DESCENDING:h.ASCENDING});else switch(e){case n.SYMBOL_NAME:this.props.setSortMethod({field:e,direction:h.ASCENDING});break;case n.SELF:case n.TOTAL:this.props.setSortMethod({field:e,direction:h.DESCENDING})}}),this.getFrameList=(()=>{const{profile:e,sortMethod:t}=this.props,r=[];switch(e.forEachFrame(e=>r.push(e)),t.field){case n.SYMBOL_NAME:(0,o.sortBy)(r,e=>e.name.toLowerCase());break;case n.SELF:(0,o.sortBy)(r,e=>e.getSelfWeight());break;case n.TOTAL:(0,o.sortBy)(r,e=>e.getTotalWeight())}return t.direction===h.DESCENDING&&r.reverse(),r}),this.listView=null,this.listViewRef=(e=>{if(e===this.listView)return;this.listView=e;const{selectedFrame:t}=this.props;if(!t||!e)return;const o=this.getFrameList().indexOf(t);-1!==o&&e.scrollIndexIntoView(o)})}renderRow(r,s){const{profile:l,selectedFrame:a}=this.props,c=r.getTotalWeight(),n=r.getSelfWeight(),h=100*c/l.getTotalNonIdleWeight(),p=100*n/l.getTotalNonIdleWeight(),S=r===a;return(0,e.h)("tr",{key:`${s}`,onClick:this.props.setSelectedFrame.bind(null,r),className:(0,t.css)(E.tableRow,s%2==0&&E.tableRowEven,S&&E.tableRowSelected)},(0,e.h)("td",{className:(0,t.css)(E.numericCell)},l.formatValue(c)," (",(0,o.formatPercent)(h),")",(0,e.h)(d,{perc:h})),(0,e.h)("td",{className:(0,t.css)(E.numericCell)},l.formatValue(n)," (",(0,o.formatPercent)(p),")",(0,e.h)(d,{perc:p})),(0,e.h)("td",{title:r.file,className:(0,t.css)(E.textCell)},(0,e.h)(i.ColorChit,{color:this.props.getCSSColorForFrame(r)}),r.name))}render(){const{sortMethod:o}=this.props,i=this.getFrameList(),l=i.map(e=>({size:r.Sizes.FRAME_HEIGHT}));return(0,e.h)("div",{className:(0,t.css)(r.commonStyle.vbox,E.profileTableView)},(0,e.h)("table",{className:(0,t.css)(E.tableView)},(0,e.h)("thead",{className:(0,t.css)(E.tableHeader)},(0,e.h)("tr",null,(0,e.h)("th",{className:(0,t.css)(E.numericCell),onClick:e=>this.onSortClick(n.TOTAL,e)},(0,e.h)(p,{activeDirection:o.field===n.TOTAL?o.direction:null}),"Total"),(0,e.h)("th",{className:(0,t.css)(E.numericCell),onClick:e=>this.onSortClick(n.SELF,e)},(0,e.h)(p,{activeDirection:o.field===n.SELF?o.direction:null}),"Self"),(0,e.h)("th",{className:(0,t.css)(E.textCell),onClick:e=>this.onSortClick(n.SYMBOL_NAME,e)},(0,e.h)(p,{activeDirection:o.field===n.SYMBOL_NAME?o.direction:null}),"Symbol Name")))),(0,e.h)(s.ScrollableListView,{ref:this.listViewRef,axis:"y",items:l,className:(0,t.css)(E.scrollView),renderItems:(o,r)=>{const s=[];for(let e=o;e<=r;e++)s.push(this.renderRow(i[e],e));return(0,e.h)("table",{className:(0,t.css)(E.tableView)},s)}}))}}exports.ProfileTableView=S;const E=t.StyleSheet.create({profileTableView:{background:r.Colors.WHITE,height:"100%"},scrollView:{overflowY:"auto",overflowX:"hidden"},tableView:{width:"100%",fontSize:r.FontSize.LABEL,background:r.Colors.WHITE},tableHeader:{borderBottom:`2px solid ${r.Colors.LIGHT_GRAY}`,textAlign:"left",color:r.Colors.GRAY,userSelect:"none"},sortIcon:{position:"relative",top:1,marginRight:r.Sizes.FRAME_HEIGHT/4},tableRow:{height:r.Sizes.FRAME_HEIGHT},tableRowEven:{background:r.Colors.OFF_WHITE},tableRowSelected:{background:r.Colors.DARK_BLUE,color:r.Colors.WHITE},numericCell:{textOverflow:"ellipsis",overflow:"hidden",whiteSpace:"nowrap",position:"relative",textAlign:"right",paddingRight:r.Sizes.FRAME_HEIGHT,width:6*r.Sizes.FRAME_HEIGHT,minWidth:6*r.Sizes.FRAME_HEIGHT},textCell:{textOverflow:"ellipsis",overflow:"hidden",whiteSpace:"nowrap",width:"100%",maxWidth:0},hBarDisplay:{position:"absolute",background:r.Colors.TRANSPARENT_GREEN,bottom:2,height:2,width:`calc(100% - ${2*r.Sizes.FRAME_HEIGHT}px)`,right:r.Sizes.FRAME_HEIGHT},hBarDisplayFilled:{height:"100%",position:"absolute",background:r.Colors.GREEN,right:0}}),m=exports.ProfileTableViewContainer=(0,a.createContainer)(S,(e,t,o)=>{const{activeProfileState:r}=o,{profile:i,sandwichViewState:s,index:a}=r;if(!i)throw new Error("profile missing");const{tableSortMethod:n}=e,{callerCallee:h}=s,d=h?h.selectedFrame:null,p=(0,c.getFrameToColorBucket)(i),S=(0,c.createGetCSSColorForFrame)(p);return{profile:i,profileIndex:r.index,selectedFrame:d,getCSSColorForFrame:S,sortMethod:n,setSelectedFrame:e=>{t(l.actions.sandwichView.setSelectedFrame({profileIndex:a,args:e}))},setSortMethod:e=>{t(l.actions.sandwichView.setTableSortMethod(e))}}}); +},{"preact":24,"aphrodite":68,"../lib/utils":70,"./style":79,"./color-chit":106,"./scrollable-list-view":108,"../store/actions":40,"../lib/typed-redux":36,"../store/getters":38}],29:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.canUseXHR=exports.ViewMode=void 0,exports.createApplicationStore=d;var e=require("./actions"),t=require("redux"),r=n(t),o=require("../lib/typed-redux"),s=require("../lib/hash-params"),i=require("./profiles-state"),a=require("../views/profile-table-view");function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t.default=e,t}var c=exports.ViewMode=void 0;!function(e){e[e.CHRONO_FLAME_CHART=0]="CHRONO_FLAME_CHART",e[e.LEFT_HEAVY_FLAME_GRAPH=1]="LEFT_HEAVY_FLAME_GRAPH",e[e.SANDWICH_VIEW=2]="SANDWICH_VIEW"}(c||(exports.ViewMode=c={}));const l=window.location.protocol,u=exports.canUseXHR="http:"===l||"https:"===l;function d(t){const n=(0,s.getHashParams)(),l=u&&null!=n.profileURL,d=r.combineReducers({profileGroup:i.profileGroup,hashParams:(0,o.setter)(e.actions.setHashParams,n),flattenRecursion:(0,o.setter)(e.actions.setFlattenRecursion,!1),viewMode:(0,o.setter)(e.actions.setViewMode,c.CHRONO_FLAME_CHART),glCanvas:(0,o.setter)(e.actions.setGLCanvas,null),dragActive:(0,o.setter)(e.actions.setDragActive,!1),loading:(0,o.setter)(e.actions.setLoading,l),error:(0,o.setter)(e.actions.setError,!1),tableSortMethod:(0,o.setter)(e.actions.sandwichView.setTableSortMethod,{field:a.SortField.SELF,direction:a.SortDirection.DESCENDING})});return r.createStore(d,t)} +},{"./actions":40,"redux":31,"../lib/typed-redux":36,"../lib/hash-params":50,"./profiles-state":52,"../views/profile-table-view":55}],81:[function(require,module,exports) { +"use strict";function e(e){return e.replace(/\\([a-fA-F0-9]{2})/g,(e,n)=>{const t=parseInt(n,16);return String.fromCharCode(t)})}function n(n){const t=n.split("\n");if(!t.length)return null;if(""===t[t.length-1]&&t.pop(),!t.length)return null;const r=new Map,o=/^(\d+):(.+)$/,s=/^([\$\w]+):([\$\w-]+)$/;for(const n of t){const t=o.exec(n);if(t){r.set(`wasm-function[${t[1]}]`,e(t[2]));continue}const c=s.exec(n);if(!c)return null;r.set(c[1],e(c[2]))}return r}Object.defineProperty(exports,"__esModule",{value:!0}),exports.importEmscriptenSymbolMap=n; +},{}],142:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Flamechart=void 0;var t=require("./utils"),e=require("./math");class r{constructor(e){this.source=e,this.layers=[],this.totalWeight=0,this.minFrameWidth=1;const r=[];this.minFrameWidth=1/0;this.totalWeight=e.getTotalWeight(),e.forEachCall((e,i)=>{const s=(0,t.lastOf)(r),h={node:e,parent:s,children:[],start:i,end:i};s&&s.children.push(h),r.push(h)},(t,e)=>{console.assert(r.length>0);const i=r.pop();if(i.end=e,i.end-i.start==0)return;const s=r.length;for(;this.layers.length<=s;)this.layers.push([]);this.layers[s].push(i),this.minFrameWidth=Math.min(this.minFrameWidth,i.end-i.start)}),isFinite(this.minFrameWidth)||(this.minFrameWidth=1)}getTotalWeight(){return this.totalWeight}getLayers(){return this.layers}getColorBucketForFrame(t){return this.source.getColorBucketForFrame(t)}getMinFrameWidth(){return this.minFrameWidth}formatValue(t){return this.source.formatValue(t)}getClampedViewportWidth(t){const r=this.getTotalWeight(),i=Math.pow(2,40),s=(0,e.clamp)(3*this.getMinFrameWidth(),r/i,r);return(0,e.clamp)(t,s,r)}}exports.Flamechart=r; +},{"./utils":70,"./math":102}],143:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.FlamechartRenderer=exports.FlamechartRowAtlasKey=void 0;var e=require("./rectangle-batch-renderer"),t=require("../lib/math"),r=require("../lib/color"),n=require("../lib/utils"),s=require("./graphics"),o=require("./utils");const c=1e4;class i{constructor(e,t,r){this.batch=e,this.bounds=t,this.numPrecedingRectanglesInRow=r,this.children=[]}getBatch(){return this.batch}getBounds(){return this.bounds}getRectCount(){return this.batch.getRectCount()}getChildren(){return this.children}getParity(){return this.numPrecedingRectanglesInRow%2}forEachLeafNodeWithinBounds(e,t){this.bounds.hasIntersectionWith(e)&&t(this)}}class h{constructor(e){if(this.children=e,this.rectCount=0,0===e.length)throw new Error("Empty interior node");let r=1/0,n=-1/0,s=1/0,o=-1/0;for(let t of e){this.rectCount+=t.getRectCount();const e=t.getBounds();r=Math.min(r,e.left()),n=Math.max(n,e.right()),s=Math.min(s,e.top()),o=Math.max(o,e.bottom())}this.bounds=new t.Rect(new t.Vec2(r,s),new t.Vec2(n-r,o-s))}getBounds(){return this.bounds}getRectCount(){return this.rectCount}getChildren(){return this.children}forEachLeafNodeWithinBounds(e,t){if(this.bounds.hasIntersectionWith(e))for(let r of this.children)r.forEachLeafNodeWithinBounds(e,t)}}class a{get key(){return`${this.stackDepth}_${this.index}_${this.zoomLevel}`}constructor(e){this.stackDepth=e.stackDepth,this.zoomLevel=e.zoomLevel,this.index=e.index}static getOrInsert(e,t){return e.getOrInsert(new a(t))}}exports.FlamechartRowAtlasKey=a;class l{constructor(s,o,a,l,g,d={inverted:!1}){this.gl=s,this.rowAtlas=o,this.flamechart=a,this.rectangleBatchRenderer=l,this.colorPassRenderer=g,this.options=d,this.layers=[],this.rectInfoTexture=null,this.rectInfoRenderTarget=null,this.atlasKeys=new n.KeyedSet;const f=a.getLayers().length;for(let n=0;n=c&&(s.push(new i(u,new t.Rect(new t.Vec2(l,o),new t.Vec2(g-l,1)),R)),l=1/0,g=-1/0,u=new e.RectangleBatch(this.gl));const d=new t.Rect(new t.Vec2(a.start,o),new t.Vec2(a.end-a.start,1));l=Math.min(l,d.left()),g=Math.max(g,d.right());const f=new r.Color((1+h%255)/256,(1+n%255)/256,(1+this.flamechart.getColorBucketForFrame(a.node.frame))/256);u.addRect(d,f),R++}u.getRectCount()>0&&s.push(new i(u,new t.Rect(new t.Vec2(l,o),new t.Vec2(g-l,1)),R)),this.layers.push(new h(s))}}getRectInfoTexture(e,t){if(this.rectInfoTexture){const r=this.rectInfoTexture;r.width==e&&r.height==t||r.resize(e,t)}else this.rectInfoTexture=this.gl.createTexture(s.Graphics.TextureFormat.NEAREST_CLAMP,e,t);return this.rectInfoTexture}getRectInfoRenderTarget(e,t){const r=this.getRectInfoTexture(e,t);return this.rectInfoRenderTarget&&this.rectInfoRenderTarget.texture!=r&&(this.rectInfoRenderTarget.texture.free(),this.rectInfoRenderTarget.setColor(r)),this.rectInfoRenderTarget||(this.rectInfoRenderTarget=this.gl.createRenderTarget(r)),this.rectInfoRenderTarget}free(){this.rectInfoRenderTarget&&this.rectInfoRenderTarget.free(),this.rectInfoTexture&&this.rectInfoTexture.free()}configSpaceBoundsForKey(e){const{stackDepth:r,zoomLevel:n,index:s}=e,o=this.flamechart.getTotalWeight()/Math.pow(2,n),c=this.flamechart.getLayers().length,i=this.options.inverted?c-1-r:r;return new t.Rect(new t.Vec2(o*s,i),new t.Vec2(o,1))}render(e){const{configSpaceSrcRect:r,physicalSpaceDstRect:n}=e,c=[],i=t.AffineTransform.betweenRects(r,n);if(r.isEmpty())return;let h=0;for(;;){const e=a.getOrInsert(this.atlasKeys,{stackDepth:0,zoomLevel:h,index:0}),t=this.configSpaceBoundsForKey(e);if(i.transformRect(t).width(){const r=this.configSpaceBoundsForKey(t);this.layers[t.stackDepth].forEachLeafNodeWithinBounds(r,t=>{this.rectangleBatchRenderer.render({batch:t.getBatch(),configSpaceSrcRect:r,physicalSpaceDstRect:e})})});const T=this.getRectInfoRenderTarget(n.width(),n.height());(0,o.renderInto)(this.gl,T,()=>{this.gl.clear(new s.Graphics.Color(0,0,0,0));const e=new t.Rect(t.Vec2.zero,new t.Vec2(this.gl.viewport.width,this.gl.viewport.height)),n=t.AffineTransform.betweenRects(r,e);for(let e of m){const t=this.configSpaceBoundsForKey(e);this.rowAtlas.renderViaAtlas(e,n.transformRect(t))}for(let e of I){const t=this.configSpaceBoundsForKey(e),r=n.transformRect(t);this.layers[e.stackDepth].forEachLeafNodeWithinBounds(t,e=>{this.rectangleBatchRenderer.render({batch:e.getBatch(),configSpaceSrcRect:t,physicalSpaceDstRect:r})})}});const x=this.getRectInfoTexture(n.width(),n.height());this.colorPassRenderer.render({rectInfoTexture:x,srcRect:new t.Rect(t.Vec2.zero,new t.Vec2(x.width,x.height)),dstRect:n,renderOutlines:e.renderOutlines})}}exports.FlamechartRenderer=l; +},{"./rectangle-batch-renderer":118,"../lib/math":102,"../lib/color":76,"../lib/utils":70,"./graphics":42,"./utils":119}],163:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.style=void 0;var e=require("aphrodite"),o=require("./style");const t=exports.style=e.StyleSheet.create({hoverCount:{color:o.Colors.GREEN},fill:{width:"100%",height:"100%",position:"absolute",left:0,top:0},minimap:{height:o.Sizes.MINIMAP_HEIGHT,borderBottom:`${o.Sizes.SEPARATOR_HEIGHT}px solid ${o.Colors.LIGHT_GRAY}`},panZoomView:{flex:1},detailView:{display:"grid",height:o.Sizes.DETAIL_VIEW_HEIGHT,overflow:"hidden",gridTemplateColumns:"120px 120px 1fr",gridTemplateRows:"repeat(4, 1fr)",borderTop:`${o.Sizes.SEPARATOR_HEIGHT}px solid ${o.Colors.LIGHT_GRAY}`,fontSize:o.FontSize.LABEL,position:"absolute",background:o.Colors.WHITE,width:"100vw",bottom:0},stackTraceViewPadding:{padding:5},stackTraceView:{height:o.Sizes.DETAIL_VIEW_HEIGHT,lineHeight:`${o.FontSize.LABEL+2}px`,overflow:"auto"},stackLine:{whiteSpace:"nowrap"},stackFileLine:{color:o.Colors.LIGHT_GRAY},statsTable:{display:"grid",gridTemplateColumns:"1fr 1fr",gridTemplateRows:`repeat(3, ${o.FontSize.LABEL+10}px)`,gridGap:"1px 1px",textAlign:"center",paddingRight:1},statsTableHeader:{gridColumn:"1 / 3"},statsTableCell:{position:"relative",display:"flex",justifyContent:"center",alignItems:"center"},thisInstanceCell:{background:o.Colors.DARK_BLUE,color:o.Colors.WHITE},allInstancesCell:{background:o.Colors.PALE_DARK_BLUE,color:o.Colors.WHITE},barDisplay:{position:"absolute",top:0,left:0,background:"rgba(0, 0, 0, 0.2)",width:"100%"}}); +},{"aphrodite":68,"./style":79}],173:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ELLIPSIS=void 0,exports.cachedMeasureTextWidth=n,exports.trimTextMid=s;var e=require("./utils");const t=exports.ELLIPSIS="…",r=new Map;let i=-1;function n(e,t){return window.devicePixelRatio!==i&&(r.clear(),i=window.devicePixelRatio),r.has(t)||r.set(t,e.measureText(t).width),r.get(t)}function o(e,r){const i=Math.floor(r/2),n=e.substr(0,i),o=e.substr(e.length-i,i);return n+t+o}function s(t,r,i){if(n(t,r)<=i)return r;const[s]=(0,e.binarySearch)(0,r.length,e=>n(t,o(r,e)),i);return o(r,s)} +},{"./utils":70}],159:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.FlamechartMinimapView=void 0;var e,i=require("preact"),t=require("aphrodite"),o=require("../lib/math"),s=require("./flamechart-style"),n=require("./style"),a=require("../lib/text-utils");!function(e){e[e.DRAW_NEW_VIEWPORT=0]="DRAW_NEW_VIEWPORT",e[e.TRANSLATE_VIEWPORT=1]="TRANSLATE_VIEWPORT"}(e||(e={}));class r extends i.Component{constructor(){super(...arguments),this.container=null,this.containerRef=(e=>{this.container=e||null}),this.overlayCanvas=null,this.overlayCtx=null,this.onWindowResize=(()=>{this.onBeforeFrame()}),this.onBeforeFrame=(()=>{this.maybeClearInteractionLock(),this.resizeOverlayCanvasIfNeeded(),this.renderRects(),this.renderOverlays()}),this.renderCanvas=(()=>{this.props.canvasContext.requestFrame()}),this.frameHadWheelEvent=!1,this.framesWithoutWheelEvents=0,this.interactionLock=null,this.maybeClearInteractionLock=(()=>{this.interactionLock&&(this.frameHadWheelEvent||(this.framesWithoutWheelEvents++,this.framesWithoutWheelEvents>=2&&(this.interactionLock=null,this.framesWithoutWheelEvents=0)),this.props.canvasContext.requestFrame()),this.frameHadWheelEvent=!1}),this.onWheel=(e=>{if(e.preventDefault(),this.frameHadWheelEvent=!0,(e.metaKey||e.ctrlKey)&&"pan"!==this.interactionLock){let i=1+e.deltaY/100;e.ctrlKey&&(i=1+e.deltaY/40),i=(0,o.clamp)(i,.1,10),this.zoom(i)}else"zoom"!==this.interactionLock&&this.pan(new o.Vec2(e.deltaX,e.deltaY));this.renderCanvas()}),this.dragStartConfigSpaceMouse=null,this.dragConfigSpaceViewportOffset=null,this.draggingMode=null,this.onMouseDown=(i=>{const t=this.configSpaceMouse(i);t&&(this.props.configSpaceViewportRect.contains(t)?(this.draggingMode=e.TRANSLATE_VIEWPORT,this.dragConfigSpaceViewportOffset=t.minus(this.props.configSpaceViewportRect.origin)):this.draggingMode=e.DRAW_NEW_VIEWPORT,this.dragStartConfigSpaceMouse=t,window.addEventListener("mousemove",this.onWindowMouseMove),window.addEventListener("mouseup",this.onWindowMouseUp),this.updateCursor(t))}),this.onWindowMouseMove=(i=>{if(!this.dragStartConfigSpaceMouse)return;let t=this.configSpaceMouse(i);if(t)if(this.updateCursor(t),t=new o.Rect(new o.Vec2(0,0),this.configSpaceSize()).closestPointTo(t),this.draggingMode===e.DRAW_NEW_VIEWPORT){const e=this.dragStartConfigSpaceMouse;let i=t;if(!e||!i)return;const s=Math.min(e.x,i.x),n=Math.max(e.x,i.x)-s,a=this.props.configSpaceViewportRect.height();this.props.setConfigSpaceViewportRect(new o.Rect(new o.Vec2(s,i.y-a/2),new o.Vec2(n,a)))}else if(this.draggingMode===e.TRANSLATE_VIEWPORT){if(!this.dragConfigSpaceViewportOffset)return;const e=t.minus(this.dragConfigSpaceViewportOffset);this.props.setConfigSpaceViewportRect(this.props.configSpaceViewportRect.withOrigin(e))}}),this.updateCursor=(i=>{this.draggingMode===e.TRANSLATE_VIEWPORT?(document.body.style.cursor="grabbing",document.body.style.cursor="-webkit-grabbing"):this.draggingMode===e.DRAW_NEW_VIEWPORT?document.body.style.cursor="col-resize":this.props.configSpaceViewportRect.contains(i)?(document.body.style.cursor="grab",document.body.style.cursor="-webkit-grab"):document.body.style.cursor="col-resize"}),this.onMouseLeave=(()=>{null==this.draggingMode&&(document.body.style.cursor="default")}),this.onMouseMove=(e=>{const i=this.configSpaceMouse(e);i&&this.updateCursor(i)}),this.onWindowMouseUp=(e=>{this.draggingMode=null,window.removeEventListener("mousemove",this.onWindowMouseMove),window.removeEventListener("mouseup",this.onWindowMouseUp);const i=this.configSpaceMouse(e);i&&this.updateCursor(i)}),this.overlayCanvasRef=(e=>{e?(this.overlayCanvas=e,this.overlayCtx=this.overlayCanvas.getContext("2d"),this.renderCanvas()):(this.overlayCanvas=null,this.overlayCtx=null)})}physicalViewSize(){return new o.Vec2(this.overlayCanvas?this.overlayCanvas.width:0,this.overlayCanvas?this.overlayCanvas.height:0)}minimapOrigin(){return new o.Vec2(0,n.Sizes.FRAME_HEIGHT*window.devicePixelRatio)}configSpaceSize(){return new o.Vec2(this.props.flamechart.getTotalWeight(),this.props.flamechart.getLayers().length)}configSpaceToPhysicalViewSpace(){const e=this.minimapOrigin();return o.AffineTransform.betweenRects(new o.Rect(new o.Vec2(0,0),this.configSpaceSize()),new o.Rect(e,this.physicalViewSize().minus(e)))}logicalToPhysicalViewSpace(){return o.AffineTransform.withScale(new o.Vec2(window.devicePixelRatio,window.devicePixelRatio))}windowToLogicalViewSpace(){if(!this.container)return new o.AffineTransform;const e=this.container.getBoundingClientRect();return o.AffineTransform.withTranslation(new o.Vec2(-e.left,-e.top))}renderRects(){this.container&&(this.physicalViewSize().x<2||this.props.canvasContext.renderBehind(this.container,()=>{this.props.flamechartRenderer.render({configSpaceSrcRect:new o.Rect(new o.Vec2(0,0),this.configSpaceSize()),physicalSpaceDstRect:new o.Rect(this.minimapOrigin(),this.physicalViewSize().minus(this.minimapOrigin())),renderOutlines:!1}),this.props.canvasContext.viewportRectangleRenderer.render({configSpaceViewportRect:this.props.configSpaceViewportRect,configSpaceToPhysicalViewSpace:this.configSpaceToPhysicalViewSpace()})}))}renderOverlays(){const e=this.overlayCtx;if(!e)return;const i=this.physicalViewSize();e.clearRect(0,0,i.x,i.y);const t=this.configSpaceToPhysicalViewSpace(),s=this.configSpaceSize().x,r=(this.configSpaceToPhysicalViewSpace().inverted()||new o.AffineTransform).times(this.logicalToPhysicalViewSpace()).transformVector(new o.Vec2(200,1)).x,c=n.Sizes.FRAME_HEIGHT*window.devicePixelRatio,h=n.FontSize.LABEL*window.devicePixelRatio,l=(c-h)/2;e.font=`${h}px/${c}px ${n.FontFamily.MONOSPACE}`,e.textBaseline="top";let p=Math.pow(10,Math.floor(Math.log10(r)));r/p>5?p*=5:r/p>2&&(p*=2),e.fillStyle="rgba(255, 255, 255, 0.8)",e.fillRect(0,0,i.x,c),e.textBaseline="top",e.fillStyle=n.Colors.DARK_GRAY;for(let n=Math.ceil(0/p)*p;n ")),i.push(c.name),c.file){let l=c.file;c.line&&(l+=`:${c.line}`,c.col&&(l+=`:${c.col}`)),i.push((0,s.h)("span",{className:(0,e.css)(t.style.stackFileLine)}," (",l,")"))}l.push((0,s.h)("div",{className:(0,e.css)(t.style.stackLine)},i))}return(0,s.h)("div",{className:(0,e.css)(t.style.stackTraceView)},(0,s.h)("div",{className:(0,e.css)(t.style.stackTraceViewPadding)},l))}}class c extends s.Component{render(){const{flamechart:l,selectedNode:a}=this.props,{frame:c}=a;return(0,s.h)("div",{className:(0,e.css)(t.style.detailView)},(0,s.h)(r,{title:"This Instance",cellStyle:t.style.thisInstanceCell,grandTotal:l.getTotalWeight(),selectedTotal:a.getTotalWeight(),selectedSelf:a.getSelfWeight(),formatter:l.formatValue.bind(l)}),(0,s.h)(r,{title:"All Instances",cellStyle:t.style.allInstancesCell,grandTotal:l.getTotalWeight(),selectedTotal:c.getTotalWeight(),selectedSelf:c.getSelfWeight(),formatter:l.formatValue.bind(l)}),(0,s.h)(i,{node:a,getFrameColor:this.props.getCSSColorForFrame}))}}exports.FlamechartDetailView=c; +},{"aphrodite":68,"preact":24,"./flamechart-style":163,"../lib/utils":70,"./color-chit":106}],161:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.FlamechartPanZoomView=void 0;var e=require("../lib/math"),t=require("./style"),i=require("../lib/text-utils"),o=require("./flamechart-style"),s=require("preact"),n=require("aphrodite");class r extends s.Component{constructor(){super(...arguments),this.container=null,this.containerRef=(e=>{this.container=e||null}),this.overlayCanvas=null,this.overlayCtx=null,this.hoveredLabel=null,this.overlayCanvasRef=(e=>{e?(this.overlayCanvas=e,this.overlayCtx=this.overlayCanvas.getContext("2d"),this.renderCanvas()):(this.overlayCanvas=null,this.overlayCtx=null)}),this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT=t.Sizes.FRAME_HEIGHT,this.onWindowResize=(()=>{this.updateConfigSpaceViewport(),this.onBeforeFrame()}),this.frameHadWheelEvent=!1,this.framesWithoutWheelEvents=0,this.interactionLock=null,this.maybeClearInteractionLock=(()=>{this.interactionLock&&(this.frameHadWheelEvent||(this.framesWithoutWheelEvents++,this.framesWithoutWheelEvents>=2&&(this.interactionLock=null,this.framesWithoutWheelEvents=0)),this.props.canvasContext.requestFrame()),this.frameHadWheelEvent=!1}),this.onBeforeFrame=(()=>{this.resizeOverlayCanvasIfNeeded(),this.renderRects(),this.renderOverlays(),this.maybeClearInteractionLock()}),this.renderCanvas=(()=>{this.props.canvasContext.requestFrame()}),this.lastDragPos=null,this.mouseDownPos=null,this.onMouseDown=(t=>{this.mouseDownPos=this.lastDragPos=new e.Vec2(t.offsetX,t.offsetY),this.updateCursor(),window.addEventListener("mouseup",this.onWindowMouseUp)}),this.onMouseDrag=(t=>{if(!this.lastDragPos)return;const i=new e.Vec2(t.offsetX,t.offsetY);this.pan(this.lastDragPos.minus(i)),this.lastDragPos=i,this.hoveredLabel&&this.props.onNodeHover(null)}),this.onDblClick=(t=>{if(this.hoveredLabel){const t=this.hoveredLabel.configSpaceBounds,i=new e.Rect(t.origin.minus(new e.Vec2(0,1)),t.size.withY(this.props.configSpaceViewportRect.height()));this.props.setConfigSpaceViewportRect(i)}}),this.onClick=(t=>{const i=new e.Vec2(t.offsetX,t.offsetY),o=this.mouseDownPos;this.mouseDownPos=null,o&&i.minus(o).length()>5||(this.hoveredLabel?(this.props.onNodeSelect(this.hoveredLabel.node),this.renderCanvas()):this.props.onNodeSelect(null))}),this.onWindowMouseUp=(e=>{this.lastDragPos=null,this.updateCursor(),window.removeEventListener("mouseup",this.onWindowMouseUp)}),this.onMouseMove=(t=>{if(this.updateCursor(),this.lastDragPos)return t.preventDefault(),void this.onMouseDrag(t);this.hoveredLabel=null;const i=new e.Vec2(t.offsetX,t.offsetY),o=this.logicalToPhysicalViewSpace().transformPosition(i),s=this.configSpaceToPhysicalViewSpace().inverseTransformPosition(o);if(!s)return;const n=(t,i=0)=>{const o=t.end-t.start,r=this.props.renderInverted?this.configSpaceSize().y-1-i:i,a=new e.Rect(new e.Vec2(t.start,r),new e.Vec2(o,1));if(s.xa.right())return null;a.contains(s)&&(this.hoveredLabel={configSpaceBounds:a,node:t.node});for(let e of t.children)n(e,i+1)};for(let e of this.props.flamechart.getLayers()[0]||[])n(e);this.hoveredLabel?this.props.onNodeHover({node:this.hoveredLabel.node,event:t}):this.props.onNodeHover(null),this.renderCanvas()}),this.onMouseLeave=(e=>{this.hoveredLabel=null,this.props.onNodeHover(null),this.renderCanvas()}),this.onWheel=(t=>{t.preventDefault(),this.frameHadWheelEvent=!0;const i=t.metaKey||t.ctrlKey;let o=t.deltaY,s=t.deltaX;if(t.deltaMode===t.DOM_DELTA_LINE&&(o*=this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT,s*=this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT),i&&"pan"!==this.interactionLock){let i=1+o/100;t.ctrlKey&&(i=1+o/40),i=(0,e.clamp)(i,.1,10),this.zoom(new e.Vec2(t.offsetX,t.offsetY),i)}else"zoom"!==this.interactionLock&&this.pan(new e.Vec2(s,o));this.renderCanvas()}),this.onWindowKeyPress=(t=>{if(!this.container)return;const{width:i,height:o}=this.container.getBoundingClientRect();"="===t.key||"+"===t.key?(this.zoom(new e.Vec2(i/2,o/2),.5),t.preventDefault()):"-"!==t.key&&"_"!==t.key||(this.zoom(new e.Vec2(i/2,o/2),2),t.preventDefault()),t.ctrlKey||t.shiftKey||t.metaKey||("0"===t.key?this.zoom(new e.Vec2(i/2,o/2),1e9):"ArrowRight"===t.key||"KeyD"===t.code?this.pan(new e.Vec2(100,0)):"ArrowLeft"===t.key||"KeyA"===t.code?this.pan(new e.Vec2(-100,0)):"ArrowUp"===t.key||"KeyW"===t.code?this.pan(new e.Vec2(0,-100)):"ArrowDown"===t.key||"KeyS"===t.code?this.pan(new e.Vec2(0,100)):"Escape"===t.key&&(this.props.onNodeSelect(null),this.renderCanvas()))})}setConfigSpaceViewportRect(e){this.props.setConfigSpaceViewportRect(e)}configSpaceSize(){return new e.Vec2(this.props.flamechart.getTotalWeight(),this.props.flamechart.getLayers().length)}physicalViewSize(){return new e.Vec2(this.overlayCanvas?this.overlayCanvas.width:0,this.overlayCanvas?this.overlayCanvas.height:0)}physicalBounds(){if(this.props.renderInverted){const t=this.physicalViewSize().y,i=(this.configSpaceSize().y+1)*this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT*window.devicePixelRatio;if(i{const f=t.end-t.start,w=this.props.renderInverted?this.configSpaceSize().y-1-d:d,u=new e.Rect(new e.Vec2(t.start,w),new e.Vec2(f,1));if(!(fthis.props.configSpaceViewportRect.right()||u.right()this.props.configSpaceViewportRect.bottom())return;if(u.hasIntersectionWith(this.props.configSpaceViewportRect)){let e=s.transformRect(u);if(e.left()<0&&(e=e.withOrigin(e.origin.withX(0)).withSize(e.size.withX(e.size.x+e.left()))),e.right()>a.x&&(e=e.withSize(e.size.withX(a.x-e.left()))),e.width()>h){const s=(0,i.trimTextMid)(o,t.node.frame.name,e.width()-2*l);o.fillText(s,e.left()+l,Math.round(e.bottom()-(r-n)/2))}}for(let e of t.children)p(e,d+1)}};for(let e of this.props.flamechart.getLayers()[0]||[])p(e);const d=2*window.devicePixelRatio;o.strokeStyle=t.Colors.PALE_DARK_BLUE,o.lineWidth=d;const f=(s.inverseTransformVector(new e.Vec2(1,0))||new e.Vec2(0,0)).x,w=(i,n=0)=>{if(!this.props.selectedNode)return;const r=i.end-i.start,a=this.props.renderInverted?this.configSpaceSize().y-1-n:n,h=new e.Rect(new e.Vec2(i.start,a),new e.Vec2(r,1));if(!(rthis.props.configSpaceViewportRect.right()||h.right()this.props.configSpaceViewportRect.bottom())){if(h.hasIntersectionWith(this.props.configSpaceViewportRect)){const e=s.transformRect(h);i.node.frame===this.props.selectedNode.frame&&(i.node===this.props.selectedNode?o.strokeStyle!==t.Colors.DARK_BLUE&&(o.stroke(),o.beginPath(),o.strokeStyle=t.Colors.DARK_BLUE):o.strokeStyle!==t.Colors.PALE_DARK_BLUE&&(o.stroke(),o.beginPath(),o.strokeStyle=t.Colors.PALE_DARK_BLUE),o.rect(Math.round(e.left()+1+d/2),Math.round(e.top()+1+d/2),Math.round(Math.max(0,e.width()-2-d)),Math.round(Math.max(0,e.height()-2-d))))}for(let e of i.children)w(e,n+1)}};o.beginPath();for(let e of this.props.flamechart.getLayers()[0]||[])w(e);o.stroke(),this.renderTimeIndicators()}renderTimeIndicators(){const o=this.overlayCtx;if(!o)return;const s=this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT*window.devicePixelRatio,n=this.physicalViewSize(),r=this.configSpaceToPhysicalViewSpace(),a=(s-t.FontSize.LABEL*window.devicePixelRatio)/2,h=this.props.configSpaceViewportRect.left(),c=this.props.configSpaceViewportRect.right(),l=(this.configSpaceToPhysicalViewSpace().inverted()||new e.AffineTransform).times(this.logicalToPhysicalViewSpace()).transformVector(new e.Vec2(200,1)).x;let p=Math.pow(10,Math.floor(Math.log10(l)));l/p>5?p*=5:l/p>2&&(p*=2);{const l=this.props.renderInverted?n.y-s:0;o.fillStyle="rgba(255, 255, 255, 0.8)",o.fillRect(0,l,n.x,s),o.fillStyle=t.Colors.DARK_GRAY,o.textBaseline="top";for(let t=Math.ceil(h/p)*p;t{this.props.flamechartRenderer.render({physicalSpaceDstRect:this.physicalBounds(),configSpaceSrcRect:this.props.configSpaceViewportRect,renderOutlines:!0})}))}pan(t){this.interactionLock="pan";const i=this.logicalToPhysicalViewSpace().transformVector(t),o=this.configSpaceToPhysicalViewSpace().inverseTransformVector(i);this.hoveredLabel&&this.props.onNodeHover(null),o&&this.props.transformViewport(e.AffineTransform.withTranslation(o))}zoom(t,i){this.interactionLock="zoom";const o=this.logicalToPhysicalViewSpace().transformPosition(t),s=this.configSpaceToPhysicalViewSpace().inverseTransformPosition(o);if(!s)return;const n=e.AffineTransform.withTranslation(s.times(-1)).scaledBy(new e.Vec2(i,1)).translatedBy(s);this.props.transformViewport(n)}updateCursor(){this.lastDragPos?(document.body.style.cursor="grabbing",document.body.style.cursor="-webkit-grabbing"):document.body.style.cursor="default"}shouldComponentUpdate(){return!1}componentWillReceiveProps(e){this.props.flamechart!==e.flamechart?(this.hoveredLabel=null,this.renderCanvas()):this.props.selectedNode!==e.selectedNode?this.renderCanvas():this.props.configSpaceViewportRect!==e.configSpaceViewportRect&&this.renderCanvas()}componentDidMount(){this.props.canvasContext.addBeforeFrameHandler(this.onBeforeFrame),window.addEventListener("resize",this.onWindowResize),window.addEventListener("keydown",this.onWindowKeyPress)}componentWillUnmount(){this.props.canvasContext.removeBeforeFrameHandler(this.onBeforeFrame),window.removeEventListener("resize",this.onWindowResize),window.removeEventListener("keydown",this.onWindowKeyPress)}render(){return(0,s.h)("div",{className:(0,n.css)(o.style.panZoomView,t.commonStyle.vbox),onMouseDown:this.onMouseDown,onMouseMove:this.onMouseMove,onMouseLeave:this.onMouseLeave,onClick:this.onClick,onDblClick:this.onDblClick,onWheel:this.onWheel,ref:this.containerRef},(0,s.h)("canvas",{width:1,height:1,ref:this.overlayCanvasRef,className:(0,n.css)(o.style.fill)}))}}exports.FlamechartPanZoomView=r; +},{"../lib/math":102,"./style":79,"../lib/text-utils":173,"./flamechart-style":163,"preact":24,"aphrodite":68}],162:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Hovertip=void 0;var e=require("./style"),o=require("aphrodite"),t=require("preact");class i extends t.Component{render(){const{containerSize:i,offset:r}=this.props,n=i.x,p=i.y,d={};return r.x+7+e.Sizes.TOOLTIP_WIDTH_MAX{const t=a.Sizes.DETAIL_VIEW_HEIGHT/a.Sizes.FRAME_HEIGHT,i=this.configSpaceSize(),o=this.props.flamechart.getClampedViewportWidth(e.size.x),s=e.size.withX(o),c=r.Vec2.clamp(e.origin,new r.Vec2(0,-1),r.Vec2.max(r.Vec2.zero,i.minus(s).plus(new r.Vec2(0,t+1))));this.props.setConfigSpaceViewportRect(new r.Rect(c,e.size.withX(o)))}),this.setLogicalSpaceViewportSize=(e=>{this.props.setLogicalSpaceViewportSize(e)}),this.transformViewport=(e=>{const t=e.transformRect(this.props.configSpaceViewportRect);this.setConfigSpaceViewportRect(t)}),this.onNodeHover=(e=>{this.props.setNodeHover(e)}),this.onNodeClick=(e=>{this.props.setSelectedNode(e)}),this.container=null,this.containerRef=(e=>{this.container=e||null})}configSpaceSize(){return new r.Vec2(this.props.flamechart.getTotalWeight(),this.props.flamechart.getLayers().length)}formatValue(e){const t=100*e/this.props.flamechart.getTotalWeight(),r=(0,i.formatPercent)(t);return`${this.props.flamechart.formatValue(e)} (${r})`}renderTooltip(){if(!this.container)return null;const{hover:i}=this.props;if(!i)return null;const{width:o,height:a,left:c,top:p}=this.container.getBoundingClientRect(),h=new r.Vec2(i.event.clientX-c,i.event.clientY-p);return(0,e.h)(n.Hovertip,{containerSize:new r.Vec2(o,a),offset:h},(0,e.h)("span",{className:(0,t.css)(s.style.hoverCount)},this.formatValue(i.node.getTotalWeight()))," ",i.node.frame.name)}render(){return(0,e.h)("div",{className:(0,t.css)(s.style.fill,a.commonStyle.vbox),ref:this.containerRef},(0,e.h)(o.FlamechartMinimapView,{configSpaceViewportRect:this.props.configSpaceViewportRect,transformViewport:this.transformViewport,flamechart:this.props.flamechart,flamechartRenderer:this.props.flamechartRenderer,canvasContext:this.props.canvasContext,setConfigSpaceViewportRect:this.setConfigSpaceViewportRect}),(0,e.h)(p.FlamechartPanZoomView,{canvasContext:this.props.canvasContext,flamechart:this.props.flamechart,flamechartRenderer:this.props.flamechartRenderer,renderInverted:!1,onNodeHover:this.onNodeHover,onNodeSelect:this.onNodeClick,selectedNode:this.props.selectedNode,transformViewport:this.transformViewport,configSpaceViewportRect:this.props.configSpaceViewportRect,setConfigSpaceViewportRect:this.setConfigSpaceViewportRect,logicalSpaceViewportSize:this.props.logicalSpaceViewportSize,setLogicalSpaceViewportBounds:this.setLogicalSpaceViewportSize}),this.renderTooltip(),this.props.selectedNode&&(0,e.h)(c.FlamechartDetailView,{flamechart:this.props.flamechart,getCSSColorForFrame:this.props.getCSSColorForFrame,selectedNode:this.props.selectedNode}))}}exports.FlamechartView=l; +},{"preact":24,"aphrodite":68,"../lib/math":102,"../lib/utils":70,"./flamechart-minimap-view":159,"./flamechart-style":163,"./style":79,"./flamechart-detail-view":160,"./flamechart-pan-zoom-view":161,"./hovertip":162,"../lib/typed-redux":36}],62:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.LeftHeavyFlamechartView=exports.getLeftHeavyFlamechart=exports.ChronoFlamechartView=exports.createMemoizedFlamechartRenderer=exports.getChronoViewFlamechart=void 0,exports.createFlamechartSetters=n;var e=require("../store/flamechart-view-state"),t=require("../lib/flamechart"),r=require("../gl/flamechart-renderer"),a=require("../lib/typed-redux"),o=require("../lib/utils"),l=require("./flamechart-view"),c=require("../store/getters"),i=require("../store/actions");function n(e,t,r){function a(a,o){return l=>{const c=Object.assign({},o(l),{id:t});e(a({profileIndex:r,args:c}))}}const{setHoveredNode:o,setLogicalSpaceViewportSize:l,setConfigSpaceViewportRect:c,setSelectedNode:n}=i.actions.flamechart;return{setNodeHover:a(o,e=>({hover:e})),setLogicalSpaceViewportSize:a(l,e=>({logicalSpaceViewportSize:e})),setConfigSpaceViewportRect:a(c,e=>({configSpaceViewportRect:e})),setSelectedNode:a(n,e=>({selectedNode:e}))}}const m=exports.getChronoViewFlamechart=(0,o.memoizeByShallowEquality)(({profile:e,getColorBucketForFrame:r})=>new t.Flamechart({getTotalWeight:e.getTotalWeight.bind(e),forEachCall:e.forEachCall.bind(e),formatValue:e.formatValue.bind(e),getColorBucketForFrame:r})),s=exports.createMemoizedFlamechartRenderer=(e=>(0,o.memoizeByShallowEquality)(({canvasContext:t,flamechart:a})=>new r.FlamechartRenderer(t.gl,(0,c.getRowAtlas)(t),a,t.rectangleBatchRenderer,t.flamechartColorPassRenderer,e))),h=s(),F=exports.ChronoFlamechartView=(0,a.createContainer)(l.FlamechartView,(t,r,a)=>{const{activeProfileState:o,glCanvas:l}=a,{index:i,profile:s,chronoViewState:F}=o,C=(0,c.getCanvasContext)(l),f=(0,c.getFrameToColorBucket)(s),g=(0,c.createGetColorBucketForFrame)(f),d=(0,c.createGetCSSColorForFrame)(f),u=m({profile:s,getColorBucketForFrame:g}),p=h({canvasContext:C,flamechart:u});return Object.assign({renderInverted:!1,flamechart:u,flamechartRenderer:p,canvasContext:C,getCSSColorForFrame:d},n(r,e.FlamechartID.CHRONO,i),F)}),C=exports.getLeftHeavyFlamechart=(0,o.memoizeByShallowEquality)(({profile:e,getColorBucketForFrame:r})=>new t.Flamechart({getTotalWeight:e.getTotalNonIdleWeight.bind(e),forEachCall:e.forEachCallGrouped.bind(e),formatValue:e.formatValue.bind(e),getColorBucketForFrame:r})),f=s(),g=exports.LeftHeavyFlamechartView=(0,a.createContainer)(l.FlamechartView,(t,r,a)=>{const{activeProfileState:o,glCanvas:l}=a,{index:i,profile:m,leftHeavyViewState:s}=o,h=(0,c.getCanvasContext)(l),F=(0,c.getFrameToColorBucket)(m),g=(0,c.createGetColorBucketForFrame)(F),d=(0,c.createGetCSSColorForFrame)(F),u=C({profile:m,getColorBucketForFrame:g}),p=f({canvasContext:h,flamechart:u});return Object.assign({renderInverted:!1,flamechart:u,flamechartRenderer:p,canvasContext:h,getCSSColorForFrame:d},n(r,e.FlamechartID.LEFT_HEAVY,i),s)}); +},{"../store/flamechart-view-state":98,"../lib/flamechart":142,"../gl/flamechart-renderer":143,"../lib/typed-redux":36,"../lib/utils":70,"./flamechart-view":115,"../store/getters":38,"../store/actions":40}],167:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.style=exports.FlamechartWrapper=void 0;var e=require("aphrodite"),t=require("preact"),r=require("./style"),o=require("../lib/math"),i=require("./flamechart-pan-zoom-view"),s=require("../lib/utils"),a=require("./hovertip"),n=require("../lib/typed-redux");class p extends n.StatelessComponent{constructor(){super(...arguments),this.setConfigSpaceViewportRect=(e=>{this.props.setConfigSpaceViewportRect(this.clampViewportToFlamegraph(e))}),this.setLogicalSpaceViewportSize=(e=>{this.props.setLogicalSpaceViewportSize(e)}),this.transformViewport=(e=>{this.setConfigSpaceViewportRect(e.transformRect(this.props.configSpaceViewportRect))}),this.container=null,this.containerRef=(e=>{this.container=e||null}),this.setNodeHover=(e=>{this.props.setNodeHover(e)})}clampViewportToFlamegraph(e){const{flamechart:t,renderInverted:r}=this.props,i=new o.Vec2(t.getTotalWeight(),t.getLayers().length),s=this.props.flamechart.getClampedViewportWidth(e.size.x),a=e.size.withX(s),n=o.Vec2.clamp(e.origin,new o.Vec2(0,r?0:-1),o.Vec2.max(o.Vec2.zero,i.minus(a).plus(new o.Vec2(0,1))));return new o.Rect(n,e.size.withX(s))}formatValue(e){const t=100*e/this.props.flamechart.getTotalWeight(),r=(0,s.formatPercent)(t);return`${this.props.flamechart.formatValue(e)} (${r})`}renderTooltip(){if(!this.container)return null;const{hover:r}=this.props;if(!r)return null;const{width:i,height:s,left:n,top:p}=this.container.getBoundingClientRect(),l=new o.Vec2(r.event.clientX-n,r.event.clientY-p);return(0,t.h)(a.Hovertip,{containerSize:new o.Vec2(i,s),offset:l},(0,t.h)("span",{className:(0,e.css)(c.hoverCount)},this.formatValue(r.node.getTotalWeight()))," ",r.node.frame.name)}render(){return(0,t.h)("div",{className:(0,e.css)(r.commonStyle.fillY,r.commonStyle.fillX,r.commonStyle.vbox),ref:this.containerRef},(0,t.h)(i.FlamechartPanZoomView,{selectedNode:null,onNodeHover:this.setNodeHover,onNodeSelect:s.noop,configSpaceViewportRect:this.props.configSpaceViewportRect,setConfigSpaceViewportRect:this.setConfigSpaceViewportRect,transformViewport:this.transformViewport,flamechart:this.props.flamechart,flamechartRenderer:this.props.flamechartRenderer,canvasContext:this.props.canvasContext,renderInverted:this.props.renderInverted,logicalSpaceViewportSize:this.props.logicalSpaceViewportSize,setLogicalSpaceViewportBounds:this.setLogicalSpaceViewportSize}),this.renderTooltip())}}exports.FlamechartWrapper=p;const c=exports.style=e.StyleSheet.create({hoverCount:{color:r.Colors.GREEN}}); +},{"aphrodite":68,"preact":24,"./style":79,"../lib/math":102,"./flamechart-pan-zoom-view":161,"../lib/utils":70,"./hovertip":162,"../lib/typed-redux":36}],137:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.InvertedCallerFlamegraphView=void 0;var e=require("../lib/utils"),r=require("../lib/flamechart"),t=require("./flamechart-view-container"),a=require("../lib/typed-redux"),l=require("../store/getters"),o=require("../store/flamechart-view-state"),i=require("./flamechart-wrapper");const n=(0,e.memoizeByShallowEquality)(({profile:e,frame:r,flattenRecursion:t})=>{let a=e.getInvertedProfileForCallersOf(r);return t?a.getProfileWithRecursionFlattened():a}),c=(0,e.memoizeByShallowEquality)(({invertedCallerProfile:e,getColorBucketForFrame:t})=>new r.Flamechart({getTotalWeight:e.getTotalNonIdleWeight.bind(e),forEachCall:e.forEachCallGrouped.bind(e),formatValue:e.formatValue.bind(e),getColorBucketForFrame:t})),s=(0,t.createMemoizedFlamechartRenderer)({inverted:!0}),m=exports.InvertedCallerFlamegraphView=(0,a.createContainer)(i.FlamechartWrapper,(e,r,a)=>{const{activeProfileState:i}=a;let{profile:m,sandwichViewState:f,index:d}=i,{flattenRecursion:u,glCanvas:C}=e;if(!m)throw new Error("profile missing");if(!C)throw new Error("glCanvas missing");const{callerCallee:h}=f;if(!h)throw new Error("callerCallee missing");const{selectedFrame:F}=h;m=u?(0,l.getProfileWithRecursionFlattened)(m):m;const g=(0,l.getFrameToColorBucket)(m),v=(0,l.createGetColorBucketForFrame)(g),p=(0,l.createGetCSSColorForFrame)(g),w=(0,l.getCanvasContext)(C),S=c({invertedCallerProfile:n({profile:m,frame:F,flattenRecursion:u}),getColorBucketForFrame:v}),E=s({canvasContext:w,flamechart:S});return Object.assign({renderInverted:!0,flamechart:S,flamechartRenderer:E,canvasContext:w,getCSSColorForFrame:p},(0,t.createFlamechartSetters)(r,o.FlamechartID.SANDWICH_INVERTED_CALLERS,d),{setSelectedNode:()=>{}},h.invertedCallerFlamegraph)}); +},{"../lib/utils":70,"../lib/flamechart":142,"./flamechart-view-container":62,"../lib/typed-redux":36,"../store/getters":38,"../store/flamechart-view-state":98,"./flamechart-wrapper":167}],138:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.CalleeFlamegraphView=void 0;var e=require("../lib/utils"),r=require("../lib/flamechart"),t=require("./flamechart-view-container"),a=require("../lib/typed-redux"),l=require("../store/getters"),o=require("../store/flamechart-view-state"),i=require("./flamechart-wrapper");const c=(0,e.memoizeByShallowEquality)(({profile:e,frame:r,flattenRecursion:t})=>{let a=e.getProfileForCalleesOf(r);return t?a.getProfileWithRecursionFlattened():a}),n=(0,e.memoizeByShallowEquality)(({calleeProfile:e,getColorBucketForFrame:t})=>new r.Flamechart({getTotalWeight:e.getTotalNonIdleWeight.bind(e),forEachCall:e.forEachCallGrouped.bind(e),formatValue:e.formatValue.bind(e),getColorBucketForFrame:t})),s=(0,t.createMemoizedFlamechartRenderer)(),m=exports.CalleeFlamegraphView=(0,a.createContainer)(i.FlamechartWrapper,(e,r,a)=>{const{activeProfileState:i}=a,{index:m,profile:f,sandwichViewState:u}=i,{flattenRecursion:h,glCanvas:C}=e;if(!f)throw new Error("profile missing");if(!C)throw new Error("glCanvas missing");const{callerCallee:F}=u;if(!F)throw new Error("callerCallee missing");const{selectedFrame:g}=F,d=(0,l.getFrameToColorBucket)(f),p=(0,l.createGetColorBucketForFrame)(d),w=(0,l.createGetCSSColorForFrame)(d),v=(0,l.getCanvasContext)(C),S=n({calleeProfile:c({profile:f,frame:g,flattenRecursion:h}),getColorBucketForFrame:p}),q=s({canvasContext:v,flamechart:S});return Object.assign({renderInverted:!1,flamechart:S,flamechartRenderer:q,canvasContext:v,getCSSColorForFrame:w},(0,t.createFlamechartSetters)(r,o.FlamechartID.SANDWICH_CALLEES,m),{setSelectedNode:()=>{}},F.calleeFlamegraph)}); +},{"../lib/utils":70,"../lib/flamechart":142,"./flamechart-view-container":62,"../lib/typed-redux":36,"../store/getters":38,"../store/flamechart-view-state":98,"./flamechart-wrapper":167}],60:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.SandwichViewContainer=void 0;var e=require("aphrodite"),t=require("./profile-table-view"),a=require("preact"),l=require("./style"),r=require("../store/actions"),s=require("../lib/typed-redux"),i=require("./inverted-caller-flamegraph-view"),o=require("./callee-flamegraph-view");class n extends s.StatelessComponent{constructor(){super(...arguments),this.setSelectedFrame=(e=>{this.props.setSelectedFrame(e)}),this.onWindowKeyPress=(e=>{"Escape"===e.key&&this.setSelectedFrame(null)})}componentDidMount(){window.addEventListener("keydown",this.onWindowKeyPress)}componentWillUnmount(){window.removeEventListener("keydown",this.onWindowKeyPress)}render(){const{selectedFrame:r}=this.props;let s=null;return r&&(s=(0,a.h)("div",{className:(0,e.css)(l.commonStyle.fillY,c.callersAndCallees,l.commonStyle.vbox)},(0,a.h)("div",{className:(0,e.css)(l.commonStyle.hbox,c.panZoomViewWraper)},(0,a.h)("div",{className:(0,e.css)(c.flamechartLabelParent)},(0,a.h)("div",{className:(0,e.css)(c.flamechartLabel)},"Callers")),(0,a.h)(i.InvertedCallerFlamegraphView,{glCanvas:this.props.glCanvas,activeProfileState:this.props.activeProfileState})),(0,a.h)("div",{className:(0,e.css)(c.divider)}),(0,a.h)("div",{className:(0,e.css)(l.commonStyle.hbox,c.panZoomViewWraper)},(0,a.h)("div",{className:(0,e.css)(c.flamechartLabelParent,c.flamechartLabelParentBottom)},(0,a.h)("div",{className:(0,e.css)(c.flamechartLabel,c.flamechartLabelBottom)},"Callees")),(0,a.h)(o.CalleeFlamegraphView,{glCanvas:this.props.glCanvas,activeProfileState:this.props.activeProfileState})))),(0,a.h)("div",{className:(0,e.css)(l.commonStyle.hbox,l.commonStyle.fillY)},(0,a.h)("div",{className:(0,e.css)(c.tableView)},(0,a.h)(t.ProfileTableViewContainer,{activeProfileState:this.props.activeProfileState})),s)}}const c=e.StyleSheet.create({tableView:{flex:1},panZoomViewWraper:{flex:1},flamechartLabelParent:{display:"flex",flexDirection:"column",justifyContent:"flex-end",alignItems:"flex-start",fontSize:l.FontSize.TITLE,width:1.2*l.FontSize.TITLE,borderRight:`1px solid ${l.Colors.LIGHT_GRAY}`},flamechartLabelParentBottom:{justifyContent:"flex-start"},flamechartLabel:{transform:"rotate(-90deg)",transformOrigin:"50% 50% 0",width:1.2*l.FontSize.TITLE,flexShrink:1},flamechartLabelBottom:{transform:"rotate(-90deg)",display:"flex",justifyContent:"flex-end"},callersAndCallees:{flex:1,borderLeft:`${l.Sizes.SEPARATOR_HEIGHT}px solid ${l.Colors.LIGHT_GRAY}`},divider:{height:2,background:l.Colors.LIGHT_GRAY}}),d=exports.SandwichViewContainer=(0,s.createContainer)(n,(e,t,a)=>{const{activeProfileState:l,glCanvas:s}=a,{sandwichViewState:i,index:o}=l,{callerCallee:n}=i;return{activeProfileState:l,glCanvas:s,setSelectedFrame:e=>{t(r.actions.sandwichView.setSelectedFrame({profileIndex:o,args:e}))},selectedFrame:n?n.selectedFrame:null,profileIndex:o}}); +},{"aphrodite":68,"./profile-table-view":55,"preact":24,"./style":79,"../store/actions":40,"../lib/typed-redux":36,"./inverted-caller-flamegraph-view":137,"./callee-flamegraph-view":138}],140:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ByteFormatter=exports.TimeFormatter=exports.RawValueFormatter=void 0;var t=require("./utils");class e{constructor(){this.unit="none"}format(t){return t.toLocaleString()}}exports.RawValueFormatter=e;class r{constructor(t){this.unit=t,this.multiplier="nanoseconds"===t?1e-9:"microseconds"===t?1e-6:"milliseconds"===t?.001:1}formatUnsigned(e){const r=e*this.multiplier;if(r/60>=1){const e=Math.floor(r/60),o=Math.floor(r-60*e).toString();return`${e}:${(0,t.zeroPad)(o,2)}`}return r/1>=1?`${r.toFixed(2)}s`:r/.001>=1?`${(r/.001).toFixed(2)}ms`:r/1e-6>=1?`${(r/1e-6).toFixed(2)}µs`:`${(r/1e-9).toFixed(2)}ns`}format(t){return`${t<0?"-":""}${this.formatUnsigned(Math.abs(t))}`}}exports.TimeFormatter=r;class o{constructor(){this.unit="bytes"}format(t){return t<1024?`${t.toFixed(0)} B`:(t/=1024)<1024?`${t.toFixed(2)} KB`:(t/=1024)<1024?`${t.toFixed(2)} MB`:`${(t/=1024).toFixed(2)} GB`}}exports.ByteFormatter=o; +},{"./utils":70}],145:[function(require,module,exports) { +var t=null;function r(){return t||(t=e()),t}function e(){try{throw new Error}catch(r){var t=(""+r.stack).match(/(https?|file|ftp):\/\/[^)\n]+/g);if(t)return n(t[0])}return"/"}function n(t){return(""+t).replace(/^((?:https?|file|ftp):\/\/.+)\/[^\/]+$/,"$1")+"/"}exports.getBundleURL=r,exports.getBaseURL=n; +},{}],66:[function(require,module,exports) { +var r=require("./bundle-url").getBundleURL;function e(r){Array.isArray(r)||(r=[r]);var e=r[r.length-1];try{return Promise.resolve(require(e))}catch(n){if("MODULE_NOT_FOUND"===n.code)return new u(function(n,i){t(r.slice(0,-1)).then(function(){return require(e)}).then(n,i)});throw n}}function t(r){return Promise.all(r.map(s))}var n={};function i(r,e){n[r]=e}module.exports=exports=e,exports.load=t,exports.register=i;var o={};function s(e){var t;if(Array.isArray(e)&&(t=e[1],e=e[0]),o[e])return o[e];var i=(e.substring(e.lastIndexOf(".")+1,e.length)||e).toLowerCase(),s=n[i];return s?o[e]=s(r()+e).then(function(r){return r&&module.bundle.register(t,r),r}):void 0}function u(r){this.executor=r,this.promise=null}u.prototype.then=function(r,e){return null===this.promise&&(this.promise=new Promise(this.executor)),this.promise.then(r,e)},u.prototype.catch=function(r){return null===this.promise&&(this.promise=new Promise(this.executor)),this.promise.catch(r)}; +},{"./bundle-url":145}],139:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.CallTreeProfileBuilder=exports.StackListProfileBuilder=exports.Profile=exports.CallTreeNode=exports.Frame=exports.HasWeights=void 0;var e=require("./utils"),t=require("./value-formatters"),s=function(e,t,s,r){return new(s||(s=Promise))(function(a,i){function l(e){try{h(r.next(e))}catch(e){i(e)}}function o(e){try{h(r.throw(e))}catch(e){i(e)}}function h(e){e.done?a(e.value):new s(function(t){t(e.value)}).then(l,o)}h((r=r.apply(e,t||[])).next())})};const r=require("_bundle_loader")(require.resolve("./demangle-cpp"));r.then(()=>{});class a{constructor(){this.selfWeight=0,this.totalWeight=0}getSelfWeight(){return this.selfWeight}getTotalWeight(){return this.totalWeight}addToTotalWeight(e){this.totalWeight+=e}addToSelfWeight(e){this.selfWeight+=e}overwriteWeightWith(e){this.selfWeight=e.selfWeight,this.totalWeight=e.totalWeight}}exports.HasWeights=a;class i extends a{constructor(e){super(),this.key=e.key,this.name=e.name,this.file=e.file,this.line=e.line,this.col=e.col}static getOrInsert(e,t){return e.getOrInsert(new i(t))}}exports.Frame=i,i.root=new i({key:"(speedscope root)",name:"(speedscope root)"});class l extends a{constructor(e,t){super(),this.frame=e,this.parent=t,this.children=[],this.frozen=!1}isRoot(){return this.frame===i.root}isFrozen(){return this.frozen}freeze(){this.frozen=!0}}exports.CallTreeNode=l;class o{constructor(s=0){this.name="",this.frames=new e.KeyedSet,this.appendOrderCalltreeRoot=new l(i.root,null),this.groupedCalltreeRoot=new l(i.root,null),this.samples=[],this.weights=[],this.valueFormatter=new t.RawValueFormatter,this.totalNonIdleWeight=null,this.totalWeight=s}getAppendOrderCalltreeRoot(){return this.appendOrderCalltreeRoot}getGroupedCalltreeRoot(){return this.groupedCalltreeRoot}formatValue(e){return this.valueFormatter.format(e)}setValueFormatter(e){this.valueFormatter=e}getWeightUnit(){return this.valueFormatter.unit}getName(){return this.name}setName(e){this.name=e}getTotalWeight(){return this.totalWeight}getTotalNonIdleWeight(){return null===this.totalNonIdleWeight&&(this.totalNonIdleWeight=this.groupedCalltreeRoot.children.reduce((e,t)=>e+t.getTotalWeight(),0)),this.totalNonIdleWeight}forEachCallGrouped(e,t){!function s(r,a){r.frame!==i.root&&e(r,a);let l=0;const o=[...r.children];o.sort((e,t)=>e.getTotalWeight()>t.getTotalWeight()?-1:1),o.forEach(function(e){s(e,a+l),l+=e.getTotalWeight()}),r.frame!==i.root&&t(r,a+r.getTotalWeight())}(this.groupedCalltreeRoot,0)}forEachCall(t,s){let r=[],a=0,l=0;for(let o of this.samples){let h=null;for(h=o;h&&h.frame!=i.root&&-1===r.indexOf(h);h=h.parent);for(;r.length>0&&(0,e.lastOf)(r)!=h;){s(r.pop(),a)}const n=[];for(let e=o;e&&e.frame!=i.root&&e!=h;e=e.parent)n.push(e);n.reverse();for(let e of n)t(e,a);r=r.concat(n),a+=this.weights[l++]}for(let e=r.length-1;e>=0;e--)s(r[e],a)}forEachFrame(e){this.frames.forEach(e)}forEachSample(e){for(let t=0;t{r.frames.getOrInsert(e).overwriteWeightWith(e)}),r}getInvertedProfileForCallersOf(e){const t=i.getOrInsert(this.frames,e),s=new h,r=[];!function e(s){if(s.frame===t)r.push(s);else for(let t of s.children)e(t)}(this.appendOrderCalltreeRoot);for(let e of r){const t=[];for(let s=e;null!=s&&s.frame!==i.root;s=s.parent)t.push(s.frame);s.appendSampleWithWeight(t,e.getTotalWeight())}const a=s.build();return a.name=this.name,a.valueFormatter=this.valueFormatter,a}getProfileForCalleesOf(e){const t=i.getOrInsert(this.frames,e),s=new h;!function e(r){if(r.frame===t)!function(e){const t=[];!function e(r){t.push(r.frame),s.appendSampleWithWeight(t,r.getSelfWeight());for(let t of r.children)e(t);t.pop()}(e)}(r);else for(let t of r.children)e(t)}(this.appendOrderCalltreeRoot);const r=s.build();return r.name=this.name,r.valueFormatter=this.valueFormatter,r}demangle(){return s(this,void 0,void 0,function*(){let e=null;for(let t of this.frames)t.name.startsWith("__Z")&&(e||(e=(yield r).demangleCpp),t.name=e(t.name))})}remapNames(e){for(let t of this.frames)t.name=e(t.name)}}exports.Profile=o;class h extends o{constructor(){super(...arguments),this.pendingSample=null}_appendSample(t,s,r){if(isNaN(s))throw new Error("invalid weight");let a=r?this.appendOrderCalltreeRoot:this.groupedCalltreeRoot,o=new Set;for(let h of t){const t=i.getOrInsert(this.frames,h),n=r?(0,e.lastOf)(a.children):a.children.find(e=>e.frame===t);if(n&&!n.isFrozen()&&n.frame==t)a=n;else{const e=a;a=new l(t,a),e.children.push(a)}a.addToTotalWeight(s),o.add(a.frame)}if(a.addToSelfWeight(s),r)for(let e of a.children)e.freeze();if(r){a.frame.addToSelfWeight(s);for(let e of o)e.addToTotalWeight(s);a===(0,e.lastOf)(this.samples)?this.weights[this.weights.length-1]+=s:(this.samples.push(a),this.weights.push(s))}}appendSampleWithWeight(e,t){if(0!==t){if(t<0)throw new Error("Samples must have positive weights");this._appendSample(e,t,!0),this._appendSample(e,t,!1)}}appendSampleWithTimestamp(e,t){if(this.pendingSample){if(t0?this.appendSampleWithWeight(this.pendingSample.stack,this.pendingSample.centralTimestamp-this.pendingSample.startTimestamp):(this.appendSampleWithWeight(this.pendingSample.stack,1),this.setValueFormatter(new t.RawValueFormatter))),this.totalWeight=Math.max(this.totalWeight,this.weights.reduce((e,t)=>e+t,0)),this}}exports.StackListProfileBuilder=h;class n extends o{constructor(){super(...arguments),this.appendOrderStack=[this.appendOrderCalltreeRoot],this.groupedOrderStack=[this.groupedCalltreeRoot],this.framesInStack=new Map,this.stack=[],this.lastValue=0}addWeightsToFrames(t){const s=t-this.lastValue;for(let e of this.framesInStack.keys())e.addToTotalWeight(s);const r=(0,e.lastOf)(this.stack);r&&r.addToSelfWeight(s)}addWeightsToNodes(t,s){const r=t-this.lastValue;for(let e of s)e.addToTotalWeight(r);const a=(0,e.lastOf)(s);a&&a.addToSelfWeight(r)}_enterFrame(t,s,r){let a=r?this.appendOrderStack:this.groupedOrderStack;this.addWeightsToNodes(s,a);let i=(0,e.lastOf)(a);if(i){if(r){const e=s-this.lastValue;if(e>0)this.samples.push(i),this.weights.push(s-this.lastValue);else if(e<0)throw new Error(`Samples must be provided in increasing order of cumulative value. Last sample was ${this.lastValue}, this sample was ${s}`)}const o=r?(0,e.lastOf)(i.children):i.children.find(e=>e.frame===t);let h;o&&!o.isFrozen()&&o.frame==t?h=o:(h=new l(t,i),i.children.push(h)),a.push(h)}}enterFrame(e,t){const s=i.getOrInsert(this.frames,e);this.addWeightsToFrames(t),this._enterFrame(s,t,!0),this._enterFrame(s,t,!1),this.stack.push(s);const r=this.framesInStack.get(s)||0;this.framesInStack.set(s,r+1),this.lastValue=t}_leaveFrame(e,t,s){let r=s?this.appendOrderStack:this.groupedOrderStack;if(this.addWeightsToNodes(t,r),s){const s=this.appendOrderStack.pop();if(null==s)throw new Error(`Trying to leave ${e.key} when stack is empty`);if(null==this.lastValue)throw new Error(`Trying to leave a ${e.key} before any have been entered`);s.freeze();const r=t-this.lastValue;if(r>0)this.samples.push(s),this.weights.push(t-this.lastValue);else if(r<0)throw new Error(`Samples must be provided in increasing order of cumulative value. Last sample was ${this.lastValue}, this sample was ${t}`)}else this.groupedOrderStack.pop()}leaveFrame(e,t){const s=i.getOrInsert(this.frames,e);this.addWeightsToFrames(t),this._leaveFrame(s,t,!0),this._leaveFrame(s,t,!1),this.stack.pop();const r=this.framesInStack.get(s);null!=r&&(1===r?this.framesInStack.delete(s):this.framesInStack.set(s,r-1),this.lastValue=t,this.totalWeight=Math.max(this.totalWeight,this.lastValue))}build(){if(this.appendOrderStack.length>1||this.groupedOrderStack.length>1)throw new Error("Tried to complete profile construction with a non-empty stack");return this}}exports.CallTreeProfileBuilder=n; +},{"./utils":70,"./value-formatters":140,"_bundle_loader":66,"./demangle-cpp":[["demangle-cpp.8a387750.js",180],"demangle-cpp.8a387750.map",180]}],141:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=exports.FileFormat=void 0;!function(e){let t,o;!function(e){e.EVENTED="evented",e.SAMPLED="sampled"}(t=e.ProfileType||(e.ProfileType={})),function(e){e.OPEN_FRAME="O",e.CLOSE_FRAME="C"}(o=e.EventType||(e.EventType={}))}(e||(exports.FileFormat=e={})); +},{}],19:[function(require,module,exports) { +module.exports={name:"speedscope",version:"1.5.3",description:"",repository:"jlfwong/speedscope",main:"index.js",bin:{speedscope:"./bin/cli.js"},scripts:{deploy:"./scripts/deploy.sh",prepack:"./scripts/build-release.sh",prettier:"prettier --write 'src/**/*.ts' 'src/**/*.tsx'",lint:"eslint 'src/**/*.ts' 'src/**/*.tsx'",jest:"./scripts/test-setup.sh && jest --runInBand",coverage:"npm run jest -- --coverage && coveralls < coverage/lcov.info",test:"tsc --noEmit && npm run lint && npm run coverage",serve:"parcel assets/index.html --open --no-autoinstall"},files:["bin/cli.js","dist/release/**","!*.map"],browserslist:["last 2 Chrome versions","last 2 Firefox versions"],author:"",license:"MIT",devDependencies:{"@types/jest":"22.2.3","@types/jszip":"3.1.4","@types/node":"10.1.4","@types/pako":"1.0.0",aphrodite:"2.1.0",coveralls:"3.0.1",eslint:"4.19.1","eslint-plugin-prettier":"2.6.0",jest:"24.3.0",jsverify:"0.8.3",jszip:"3.1.5",pako:"1.0.6","parcel-bundler":"1.9.2",preact:"8.2.7","preact-redux":"jlfwong/preact-redux#a56dcc4",prettier:"1.12.0",protobufjs:"6.8.8",quicktype:"15.0.209",redux:"^4.0.0","ts-jest":"24.3.0",typescript:"3.2.4","typescript-eslint-parser":"17.0.1","uglify-es":"3.2.2"},jest:{transform:{"^.+\\.tsx?$":"ts-jest"},testRegex:"\\.test\\.tsx?$",collectCoverageFrom:["**/*.{ts,tsx}","!**/*.d.{ts,tsx}"],moduleFileExtensions:["ts","tsx","js","jsx","json"]},dependencies:{opn:"5.3.0"}}; +},{}],83:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.exportProfileGroup=r,exports.importSpeedscopeProfiles=s,exports.saveToFile=l;var e=require("./profile"),t=require("./value-formatters"),n=require("./file-format-spec");function r(e){const t=[],n=new Map;function r(e){let r=n.get(e);if(null==r){const o={name:e.name};null!=e.file&&(o.file=e.file),null!=e.line&&(o.line=e.line),null!=e.col&&(o.col=e.col),r=t.length,n.set(e,r),t.push(o)}return r}const a={exporter:`speedscope@${require("../../package.json").version}`,name:e.name,activeProfileIndex:e.indexToView,$schema:"https://www.speedscope.app/file-format-schema.json",shared:{frames:t},profiles:[]};for(let t of e.profiles)a.profiles.push(o(t,r));return a}function o(e,t){const r={type:n.FileFormat.ProfileType.EVENTED,name:e.getName(),unit:e.getWeightUnit(),startValue:0,endValue:e.getTotalWeight(),events:[]};return e.forEachCall((e,o)=>{r.events.push({type:n.FileFormat.EventType.OPEN_FRAME,frame:t(e.frame),at:o})},(e,o)=>{r.events.push({type:n.FileFormat.EventType.CLOSE_FRAME,frame:t(e.frame),at:o})}),r}function a(r,o){function a(e){const{name:n,unit:o}=r;switch(o){case"nanoseconds":case"microseconds":case"milliseconds":case"seconds":e.setValueFormatter(new t.TimeFormatter(o));break;case"bytes":e.setValueFormatter(new t.ByteFormatter);break;case"none":e.setValueFormatter(new t.RawValueFormatter)}e.setName(n)}switch(r.type){case n.FileFormat.ProfileType.EVENTED:return function(t){const{startValue:r,endValue:s,events:l}=t,i=new e.CallTreeProfileBuilder(s-r);a(i);const c=o.map((e,t)=>Object.assign({key:t},e));for(let e of l)switch(e.type){case n.FileFormat.EventType.OPEN_FRAME:i.enterFrame(c[e.frame],e.at-r);break;case n.FileFormat.EventType.CLOSE_FRAME:i.leaveFrame(c[e.frame],e.at-r)}return i.build()}(r);case n.FileFormat.ProfileType.SAMPLED:return function(t){const{startValue:n,endValue:r,samples:s,weights:l}=t,i=new e.StackListProfileBuilder(r-n);a(i);const c=o.map((e,t)=>Object.assign({key:t},e));if(s.length!==l.length)throw new Error(`Expected samples.length (${s.length}) to equal weights.length (${l.length})`);for(let e=0;ec[e]),n)}return i.build()}(r)}}function s(e){return{name:e.name||e.profiles[0].name||"profile",indexToView:e.activeProfileIndex||0,profiles:e.profiles.map(t=>a(t,e.shared.frames))}}function l(e){const t=r(e),n=new Blob([JSON.stringify(t)],{type:"text/json"}),o=`${(t.name?t.name.split(".")[0]:"profile").replace(/\W+/g,"_")}.speedscope.json`;console.log("Saving",o);const a=document.createElement("a");a.download=o,a.href=window.URL.createObjectURL(n),a.dataset.downloadurl=["text/json",a.download,a.href].join(":"),document.body.appendChild(a),a.click(),document.body.removeChild(a)} +},{"./profile":139,"./value-formatters":140,"./file-format-spec":141,"../../package.json":19}],64:[function(require,module,exports) { +module.exports="perf-vertx-stacks-01-collapsed-all.3e0a632c.txt"; +},{}],34:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Application=exports.GLCanvas=exports.Toolbar=void 0;var e=require("preact"),t=require("aphrodite"),o=require("./style"),i=require("../lib/emscripten"),s=require("./sandwich-view"),r=require("../lib/file-format"),n=require("../store"),a=require("../lib/typed-redux"),l=require("./flamechart-view-container"),c=require("../gl/graphics"),d=function(e,t,o,i){return new(o||(o=Promise))(function(s,r){function n(e){try{l(i.next(e))}catch(e){r(e)}}function a(e){try{l(i.throw(e))}catch(e){r(e)}}function l(e){e.done?s(e.value):new o(function(t){t(e.value)}).then(n,a)}l((i=i.apply(e,t||[])).next())})};const p=require("_bundle_loader")(require.resolve("../import"));function h(e,t){return d(this,void 0,void 0,function*(){return(yield p).importProfileGroupFromText(e,t)})}function f(e,t){return d(this,void 0,void 0,function*(){return(yield p).importProfileGroupFromBase64(e,t)})}function m(e,t){return d(this,void 0,void 0,function*(){return(yield p).importProfilesFromArrayBuffer(e,t)})}function u(e){return d(this,void 0,void 0,function*(){return(yield p).importProfilesFromFile(e)})}function v(e){return d(this,void 0,void 0,function*(){return(yield p).importFromFileSystemDirectoryEntry(e)})}p.then(()=>{});const g=require("../../sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt");class w extends a.StatelessComponent{constructor(){super(...arguments),this.setTimeOrder=(()=>{this.props.setViewMode(n.ViewMode.CHRONO_FLAME_CHART)}),this.setLeftHeavyOrder=(()=>{this.props.setViewMode(n.ViewMode.LEFT_HEAVY_FLAME_GRAPH)}),this.setSandwichView=(()=>{this.props.setViewMode(n.ViewMode.SANDWICH_VIEW)})}renderLeftContent(){return this.props.activeProfileState?(0,e.h)("div",{className:(0,t.css)(y.toolbarLeft)},(0,e.h)("div",{className:(0,t.css)(y.toolbarTab,this.props.viewMode===n.ViewMode.CHRONO_FLAME_CHART&&y.toolbarTabActive),onClick:this.setTimeOrder},(0,e.h)("span",{className:(0,t.css)(y.emoji)},"🕰"),"Time Order"),(0,e.h)("div",{className:(0,t.css)(y.toolbarTab,this.props.viewMode===n.ViewMode.LEFT_HEAVY_FLAME_GRAPH&&y.toolbarTabActive),onClick:this.setLeftHeavyOrder},(0,e.h)("span",{className:(0,t.css)(y.emoji)},"⬅️"),"Left Heavy"),(0,e.h)("div",{className:(0,t.css)(y.toolbarTab,this.props.viewMode===n.ViewMode.SANDWICH_VIEW&&y.toolbarTabActive),onClick:this.setSandwichView},(0,e.h)("span",{className:(0,t.css)(y.emoji)},"🥪"),"Sandwich")):null}renderCenterContent(){const{activeProfileState:o,profileGroup:i}=this.props;if(o&&i){const{index:r}=o;if(1===i.profiles.length)return o.profile.getName();{function s(o,i,s){return(0,e.h)("button",{disabled:i,onClick:s,className:(0,t.css)(y.emoji,y.toolbarProfileNavButton,i&&y.toolbarProfileNavButtonDisabled)},o)}const n=s("⬅️",0===r,()=>this.props.setProfileIndexToView(r-1)),a=s("➡️",r>=i.profiles.length-1,()=>this.props.setProfileIndexToView(r+1));return(0,e.h)("div",{className:(0,t.css)(y.toolbarCenter)},n,o.profile.getName()," ",(0,e.h)("span",{className:(0,t.css)(y.toolbarProfileIndex)},"(",o.index+1,"/",i.profiles.length,")"),a)}}return"🔬speedscope"}renderRightContent(){const o=(0,e.h)("div",{className:(0,t.css)(y.toolbarTab),onClick:this.props.browseForFile},(0,e.h)("span",{className:(0,t.css)(y.emoji)},"⤵️"),"Import"),i=(0,e.h)("div",{className:(0,t.css)(y.toolbarTab)},(0,e.h)("a",{href:"https://github.com/jlfwong/speedscope#usage",className:(0,t.css)(y.noLinkStyle),target:"_blank"},(0,e.h)("span",{className:(0,t.css)(y.emoji)},"❓"),"Help"));return(0,e.h)("div",{className:(0,t.css)(y.toolbarRight)},this.props.activeProfileState&&(0,e.h)("div",{className:(0,t.css)(y.toolbarTab),onClick:this.props.saveFile},(0,e.h)("span",{className:(0,t.css)(y.emoji)},"⤴️"),"Export"),o,i)}render(){return(0,e.h)("div",{className:(0,t.css)(y.toolbar)},this.renderLeftContent(),this.renderCenterContent(),this.renderRightContent())}}exports.Toolbar=w;class b extends e.Component{constructor(){super(...arguments),this.canvas=null,this.ref=(e=>{e instanceof HTMLCanvasElement?this.canvas=e:this.canvas=null,this.props.setGLCanvas(this.canvas)}),this.container=null,this.containerRef=(e=>{e instanceof HTMLElement?this.container=e:this.container=null}),this.maybeResize=(()=>{if(!this.container)return;if(!this.props.canvasContext)return;let{width:e,height:t}=this.container.getBoundingClientRect();const o=e,i=t,s=e*window.devicePixelRatio,r=t*window.devicePixelRatio;this.props.canvasContext.gl.resize(s,r,o,i),this.props.canvasContext.gl.clear(new c.Graphics.Color(1,1,1,1))}),this.onWindowResize=(()=>{this.props.canvasContext&&this.props.canvasContext.requestFrame()})}componentWillReceiveProps(e){this.props.canvasContext!==e.canvasContext&&(this.props.canvasContext&&this.props.canvasContext.removeBeforeFrameHandler(this.maybeResize),e.canvasContext&&(e.canvasContext.addBeforeFrameHandler(this.maybeResize),e.canvasContext.requestFrame()))}componentDidMount(){window.addEventListener("resize",this.onWindowResize)}componentWillUnmount(){this.props.canvasContext&&this.props.canvasContext.removeBeforeFrameHandler(this.maybeResize),window.removeEventListener("resize",this.onWindowResize)}render(){return(0,e.h)("div",{ref:this.containerRef,className:(0,t.css)(y.glCanvasView)},(0,e.h)("canvas",{ref:this.ref,width:1,height:1}))}}exports.GLCanvas=b;class C extends a.StatelessComponent{constructor(){super(...arguments),this.loadExample=(()=>{this.loadProfile(()=>d(this,void 0,void 0,function*(){return yield h("perf-vertx-stacks-01-collapsed-all.txt",yield fetch(g).then(e=>e.text()))}))}),this.onDrop=(e=>{if(this.props.setDragActive(!1),e.preventDefault(),!e.dataTransfer)return;const t=e.dataTransfer.items[0];if("webkitGetAsEntry"in t){const e=t.webkitGetAsEntry();if(e.isDirectory&&e.name.endsWith(".trace"))return console.log("Importing as Instruments.app .trace file"),void this.loadProfile(()=>d(this,void 0,void 0,function*(){return yield v(e)}))}let o=e.dataTransfer.files.item(0);o&&this.loadFromFile(o)}),this.onDragOver=(e=>{this.props.setDragActive(!0),e.preventDefault()}),this.onDragLeave=(e=>{this.props.setDragActive(!1),e.preventDefault()}),this.onWindowKeyPress=(e=>d(this,void 0,void 0,function*(){if("1"===e.key)this.props.setViewMode(n.ViewMode.CHRONO_FLAME_CHART);else if("2"===e.key)this.props.setViewMode(n.ViewMode.LEFT_HEAVY_FLAME_GRAPH);else if("3"===e.key)this.props.setViewMode(n.ViewMode.SANDWICH_VIEW);else if("r"===e.key){const{flattenRecursion:e}=this.props;this.props.setFlattenRecursion(!e)}else if("n"===e.key){const{activeProfileState:e}=this.props;e&&this.props.setProfileIndexToView(e.index+1)}else if("p"===e.key){const{activeProfileState:e}=this.props;e&&this.props.setProfileIndexToView(e.index-1)}})),this.saveFile=(()=>{if(this.props.profileGroup){const{name:e,indexToView:t,profiles:o}=this.props.profileGroup,i={name:e,indexToView:t,profiles:o.map(e=>e.profile)};(0,r.saveToFile)(i)}}),this.browseForFile=(()=>{const e=document.createElement("input");e.type="file",e.addEventListener("change",this.onFileSelect),e.click()}),this.onWindowKeyDown=(e=>d(this,void 0,void 0,function*(){"s"===e.key&&(e.ctrlKey||e.metaKey)?(e.preventDefault(),this.saveFile()):"o"===e.key&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),this.browseForFile())})),this.onDocumentPaste=(e=>{e.preventDefault(),e.stopPropagation();const t=e.clipboardData;if(!t)return;const o=t.getData("text");this.loadProfile(()=>d(this,void 0,void 0,function*(){return yield h("From Clipboard",o)}))}),this.onFileSelect=(e=>{const t=e.target.files.item(0);t&&this.loadFromFile(t)})}loadProfile(e){return d(this,void 0,void 0,function*(){if(this.props.setLoading(!0),yield new Promise(e=>setTimeout(e,0)),!this.props.glCanvas)return;console.time("import");let t=null;try{t=yield e()}catch(e){return console.log("Failed to load format",e),void this.props.setError(!0)}if(null==t)return alert("Unrecognized format! See documentation about supported formats."),void this.props.setLoading(!1);if(0===t.profiles.length)return alert("Successfully imported profile, but it's empty!"),void this.props.setLoading(!1);this.props.hashParams.title&&(t=Object.assign({name:this.props.hashParams.title},t)),document.title=`${t.name} - speedscope`;for(let e of t.profiles)yield e.demangle();for(let e of t.profiles){const t=this.props.hashParams.title||e.getName();e.setName(t)}console.timeEnd("import"),this.props.setProfileGroup(t),this.props.setLoading(!1)})}loadFromFile(e){this.loadProfile(()=>d(this,void 0,void 0,function*(){const t=yield u(e);if(t){for(let o of t.profiles)o.getName()||o.setName(e.name);return t}if(this.props.profileGroup&&this.props.activeProfileState){const t=new FileReader,o=new Promise(e=>{t.addEventListener("loadend",()=>{if("string"!=typeof t.result)throw new Error("Expected reader.result to be a string");e(t.result)})});t.readAsText(e);const s=yield o,r=(0,i.importEmscriptenSymbolMap)(s);if(r){const{profile:e,index:t}=this.props.activeProfileState;return console.log("Importing as emscripten symbol map"),e.remapNames(e=>r.get(e)||e),{name:this.props.profileGroup.name||"profile",indexToView:t,profiles:[e]}}}return null}))}componentDidMount(){window.addEventListener("keydown",this.onWindowKeyDown),window.addEventListener("keypress",this.onWindowKeyPress),document.addEventListener("paste",this.onDocumentPaste),this.maybeLoadHashParamProfile()}componentWillUnmount(){window.removeEventListener("keydown",this.onWindowKeyDown),window.removeEventListener("keypress",this.onWindowKeyPress),document.removeEventListener("paste",this.onDocumentPaste)}maybeLoadHashParamProfile(){return d(this,void 0,void 0,function*(){if(this.props.hashParams.profileURL){if(!n.canUseXHR)return void alert(`Cannot load a profile URL when loading from "${window.location.protocol}" URL protocol`);this.loadProfile(()=>d(this,void 0,void 0,function*(){const e=yield fetch(this.props.hashParams.profileURL);let t=new URL(this.props.hashParams.profileURL).pathname;return t.includes("/")&&(t=t.slice(t.lastIndexOf("/")+1)),yield m(t,yield e.arrayBuffer())}))}else if(this.props.hashParams.localProfilePath){window.speedscope={loadFileFromBase64:(e,t)=>{this.loadProfile(()=>f(e,t))}};const e=document.createElement("script");e.src=`file:///${this.props.hashParams.localProfilePath}`,document.head.appendChild(e)}})}renderLanding(){return(0,e.h)("div",{className:(0,t.css)(y.landingContainer)},(0,e.h)("div",{className:(0,t.css)(y.landingMessage)},(0,e.h)("p",{className:(0,t.css)(y.landingP)},"👋 Hi there! Welcome to 🔬speedscope, an interactive"," ",(0,e.h)("a",{className:(0,t.css)(y.link),href:"http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html"},"flamegraph")," ","visualizer. Use it to help you make your software faster."),n.canUseXHR?(0,e.h)("p",{className:(0,t.css)(y.landingP)},"Drag and drop a profile file onto this window to get started, click the big blue button below to browse for a profile to explore, or"," ",(0,e.h)("a",{tabIndex:0,className:(0,t.css)(y.link),onClick:this.loadExample},"click here")," ","to load an example profile."):(0,e.h)("p",{className:(0,t.css)(y.landingP)},"Drag and drop a profile file onto this window to get started, or click the big blue button below to browse for a profile to explore."),(0,e.h)("div",{className:(0,t.css)(y.browseButtonContainer)},(0,e.h)("input",{type:"file",name:"file",id:"file",onChange:this.onFileSelect,className:(0,t.css)(y.hide)}),(0,e.h)("label",{for:"file",className:(0,t.css)(y.browseButton),tabIndex:0},"Browse")),(0,e.h)("p",{className:(0,t.css)(y.landingP)},"See the"," ",(0,e.h)("a",{className:(0,t.css)(y.link),href:"https://github.com/jlfwong/speedscope#usage",target:"_blank"},"documentation")," ","for information about supported file formats, keyboard shortcuts, and how to navigate around the profile."),(0,e.h)("p",{className:(0,t.css)(y.landingP)},"speedscope is open source. Please"," ",(0,e.h)("a",{className:(0,t.css)(y.link),target:"_blank",href:"https://github.com/jlfwong/speedscope/issues"},"report any issues on GitHub"),".")))}renderError(){return(0,e.h)("div",{className:(0,t.css)(y.error)},(0,e.h)("div",null,"😿 Something went wrong."),(0,e.h)("div",null,"Check the JS console for more details."))}renderLoadingBar(){return(0,e.h)("div",{className:(0,t.css)(y.loading)})}renderContent(){const{viewMode:t,activeProfileState:o,error:i,loading:r,glCanvas:a}=this.props;if(i)return this.renderError();if(r)return this.renderLoadingBar();if(!o||!a)return this.renderLanding();switch(t){case n.ViewMode.CHRONO_FLAME_CHART:return(0,e.h)(l.ChronoFlamechartView,{activeProfileState:o,glCanvas:a});case n.ViewMode.LEFT_HEAVY_FLAME_GRAPH:return(0,e.h)(l.LeftHeavyFlamechartView,{activeProfileState:o,glCanvas:a});case n.ViewMode.SANDWICH_VIEW:return(0,e.h)(s.SandwichViewContainer,{activeProfileState:o,glCanvas:a})}}render(){return(0,e.h)("div",{onDrop:this.onDrop,onDragOver:this.onDragOver,onDragLeave:this.onDragLeave,className:(0,t.css)(y.root,this.props.dragActive&&y.dragTargetRoot)},(0,e.h)(b,{setGLCanvas:this.props.setGLCanvas,canvasContext:this.props.canvasContext}),(0,e.h)(w,Object.assign({saveFile:this.saveFile,browseForFile:this.browseForFile},this.props)),(0,e.h)("div",{className:(0,t.css)(y.contentContainer)},this.renderContent()),this.props.dragActive&&(0,e.h)("div",{className:(0,t.css)(y.dragTarget)}))}}exports.Application=C;const y=t.StyleSheet.create({glCanvasView:{position:"absolute",width:"100vw",height:"100vh",zIndex:-1,pointerEvents:"none"},error:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",height:"100%"},loading:{height:3,marginBottom:-3,background:o.Colors.DARK_BLUE,transformOrigin:"0% 50%",animationName:[{from:{transform:"scaleX(0)"},to:{transform:"scaleX(1)"}}],animationTimingFunction:"cubic-bezier(0, 1, 0, 1)",animationDuration:"30s"},root:{width:"100vw",height:"100vh",overflow:"hidden",display:"flex",flexDirection:"column",position:"relative",fontFamily:o.FontFamily.MONOSPACE,lineHeight:"20px"},dragTargetRoot:{cursor:"copy"},dragTarget:{boxSizing:"border-box",position:"absolute",top:0,left:0,width:"100%",height:"100%",border:`5px dashed ${o.Colors.DARK_BLUE}`,pointerEvents:"none"},contentContainer:{position:"relative",display:"flex",overflow:"hidden",flexDirection:"column",flex:1},landingContainer:{display:"flex",alignItems:"center",justifyContent:"center",flex:1},landingMessage:{maxWidth:600},landingP:{marginBottom:16},hide:{display:"none"},browseButtonContainer:{display:"flex",alignItems:"center",justifyContent:"center"},browseButton:{marginBottom:16,height:72,flex:1,maxWidth:256,textAlign:"center",fontSize:o.FontSize.BIG_BUTTON,lineHeight:"72px",background:o.Colors.DARK_BLUE,color:o.Colors.WHITE,transition:`all ${o.Duration.HOVER_CHANGE} ease-in`,":hover":{background:o.Colors.BRIGHT_BLUE}},link:{color:o.Colors.BRIGHT_BLUE,cursor:"pointer",textDecoration:"none"},toolbar:{height:o.Sizes.TOOLBAR_HEIGHT,flexShrink:0,background:o.Colors.BLACK,color:o.Colors.WHITE,textAlign:"center",fontFamily:o.FontFamily.MONOSPACE,fontSize:o.FontSize.TITLE,lineHeight:`${o.Sizes.TOOLBAR_TAB_HEIGHT}px`,userSelect:"none"},toolbarLeft:{position:"absolute",height:o.Sizes.TOOLBAR_HEIGHT,overflow:"hidden",top:0,left:0,marginRight:2,textAlign:"left"},toolbarCenter:{paddingTop:1,height:o.Sizes.TOOLBAR_HEIGHT},toolbarRight:{height:o.Sizes.TOOLBAR_HEIGHT,overflow:"hidden",position:"absolute",top:0,right:0,marginRight:2,textAlign:"right"},toolbarProfileIndex:{color:o.Colors.LIGHT_GRAY},toolbarProfileNavButton:{opacity:.8,fontSize:o.FontSize.TITLE,lineHeight:`${o.Sizes.TOOLBAR_TAB_HEIGHT}px`,":hover":{opacity:1},background:"none",border:"none",padding:0,marginLeft:"0.3em",marginRight:"0.3em",transition:`all ${o.Duration.HOVER_CHANGE} ease-in`},toolbarProfileNavButtonDisabled:{opacity:.5,":hover":{opacity:.5}},toolbarTab:{background:o.Colors.DARK_GRAY,marginTop:o.Sizes.SEPARATOR_HEIGHT,height:o.Sizes.TOOLBAR_TAB_HEIGHT,lineHeight:`${o.Sizes.TOOLBAR_TAB_HEIGHT}px`,paddingLeft:2,paddingRight:8,display:"inline-block",marginLeft:2,transition:`all ${o.Duration.HOVER_CHANGE} ease-in`,":hover":{background:o.Colors.GRAY}},toolbarTabActive:{background:o.Colors.BRIGHT_BLUE,":hover":{background:o.Colors.BRIGHT_BLUE}},noLinkStyle:{textDecoration:"none",color:"inherit"},emoji:{display:"inline-block",verticalAlign:"middle",paddingTop:"0px",marginRight:"0.3em"}}); +},{"preact":24,"aphrodite":68,"./style":79,"../lib/emscripten":81,"./sandwich-view":60,"../lib/file-format":83,"../store":29,"../lib/typed-redux":36,"./flamechart-view-container":62,"../gl/graphics":42,"_bundle_loader":66,"../import":[["import.a03c2bef.js",104],"import.a03c2bef.map",104],"../../sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt":64}],21:[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ApplicationContainer=void 0;var e=require("../lib/typed-redux"),t=require("./application"),i=require("../store/getters"),o=require("../store/actions"),n=require("../gl/graphics");const r=exports.ApplicationContainer=(0,e.createContainer)(t.Application,(t,r)=>{const{flattenRecursion:s,profileGroup:a}=t;let l=null;if(a&&a.profiles.length>a.indexToView){const e=a.indexToView,t=a.profiles[e];l=Object.assign({},a.profiles[a.indexToView],{profile:(0,i.getProfileToView)({profile:t.profile,flattenRecursion:s}),index:a.indexToView})}function c(t){return(0,e.bindActionCreator)(r,t)}const p={setGLCanvas:c(o.actions.setGLCanvas),setLoading:c(o.actions.setLoading),setError:c(o.actions.setError),setProfileGroup:c(o.actions.setProfileGroup),setDragActive:c(o.actions.setDragActive),setViewMode:c(o.actions.setViewMode),setFlattenRecursion:c(o.actions.setFlattenRecursion),setProfileIndexToView:c(o.actions.setProfileIndexToView)};return Object.assign({activeProfileState:l,dispatch:r,canvasContext:t.glCanvas?(0,i.getCanvasContext)(t.glCanvas):null,resizeCanvas:(e,o,r,s)=>{if(t.glCanvas){const a=(0,i.getCanvasContext)(t.glCanvas).gl;a.resize(e,o,r,s),a.clear(new n.Graphics.Color(1,1,1,1))}}},p,t)}); +},{"../lib/typed-redux":36,"./application":34,"../store/getters":38,"../store/actions":40,"../gl/graphics":42}],13:[function(require,module,exports) { +"use strict";var e=require("preact"),o=require("./store"),t=require("preact-redux"),r=require("./views/application-container");console.log(`speedscope v${require("../package.json").version}`),module.hot&&(module.hot.dispose(()=>{(0,e.render)((0,e.h)("div",null),document.body,document.body.lastElementChild||void 0)}),module.hot.accept());const i=window.store,d=(0,o.createApplicationStore)(i?i.getState():{});window.store=d,(0,e.render)((0,e.h)(t.Provider,{store:d},(0,e.h)(r.ApplicationContainer,null)),document.body,document.body.lastElementChild||void 0); +},{"preact":24,"./store":29,"preact-redux":26,"./views/application-container":21,"../package.json":19}],215:[function(require,module,exports) { +module.exports=function(n){return new Promise(function(e,o){var r=document.createElement("script");r.async=!0,r.type="text/javascript",r.charset="utf-8",r.src=n,r.onerror=function(n){r.onerror=r.onload=null,o(n)},r.onload=function(){r.onerror=r.onload=null,e()},document.getElementsByTagName("head")[0].appendChild(r)})}; +},{}],0:[function(require,module,exports) { +var b=require(66);b.register("js",require(215)); +},{}]},{},[0,13], null) +//# sourceMappingURL=speedscope.9ee942dd.map \ No newline at end of file diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/logo.3704c1bbca650bb72a64b5d4c3fa5ced.svg b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/logo.3704c1bbca650bb72a64b5d4c3fa5ced.svg new file mode 100644 index 0000000000000000000000000000000000000000..70be9ee548c6a425e5e2b1621f0ee8f980a2ac8e --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/logo.3704c1bbca650bb72a64b5d4c3fa5ced.svg @@ -0,0 +1,34 @@ + + + + +Ray Logo + + + + + + + + + + diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-100.a45108d3b34af91f9113.woff b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-100.a45108d3b34af91f9113.woff new file mode 100644 index 0000000000000000000000000000000000000000..7306a7b7141ad287fe93b046009c053591184626 Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-100.a45108d3b34af91f9113.woff differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-300italic.c64e7e354c88e613c77c.woff2 b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-300italic.c64e7e354c88e613c77c.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b6653fb978bd439bbecbd81c8eb814f9c53c92de Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-300italic.c64e7e354c88e613c77c.woff2 differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-400.176f8f5bd5f02b3abfcf.woff2 b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-400.176f8f5bd5f02b3abfcf.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1a537015109f44983c26738a436c8f5edc44b9ae Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-400.176f8f5bd5f02b3abfcf.woff2 differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-400.49ae34d4cc6b98c00c69.woff b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-400.49ae34d4cc6b98c00c69.woff new file mode 100644 index 0000000000000000000000000000000000000000..69c88254051499539452130e33c36a20e9469e8e Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-400.49ae34d4cc6b98c00c69.woff differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-400italic.d022bc70dc1bf7b3425d.woff2 b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-400italic.d022bc70dc1bf7b3425d.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..2741d4f0829e357ce245b819c46eaf7279cf3ff2 Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-400italic.d022bc70dc1bf7b3425d.woff2 differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-500.cea99d3e3e13a3a599a0.woff b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-500.cea99d3e3e13a3a599a0.woff new file mode 100644 index 0000000000000000000000000000000000000000..869925869a7b9d5e5080cc3e4f5291611b08dcfa Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-500.cea99d3e3e13a3a599a0.woff differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-500.f5b74d7ffcdf85b9dd60.woff2 b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-500.f5b74d7ffcdf85b9dd60.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..6362d7f64a4d45d901e9f1399e020a476ffa8065 Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-500.f5b74d7ffcdf85b9dd60.woff2 differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-500italic.0d8bb5b3ee5f5dac9e44.woff2 b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-500italic.0d8bb5b3ee5f5dac9e44.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0ff2f813d3cc88c106f1dee4eb41d800f175dd5b Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-500italic.0d8bb5b3ee5f5dac9e44.woff2 differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-700.2267169ee7270a22a963.woff b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-700.2267169ee7270a22a963.woff new file mode 100644 index 0000000000000000000000000000000000000000..0f14effba09e91884ebca24e1c6e565ef8d0977e Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-700.2267169ee7270a22a963.woff differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-700.c18ee39fb002ad58b6dc.woff2 b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-700.c18ee39fb002ad58b6dc.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..32b25eee7c5c3309ea53facaaf016256887ec0b2 Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-700.c18ee39fb002ad58b6dc.woff2 differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-700italic.7d8125ff7f707231fd89.woff2 b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-700italic.7d8125ff7f707231fd89.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..fe58be2f7c28aed835f1615e42b5b1a160668f8c Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-700italic.7d8125ff7f707231fd89.woff2 differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-900.870c8c1486f76054301a.woff2 b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-900.870c8c1486f76054301a.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..802499d3fd9da031f14759635a3c7a489e84d072 Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-900.870c8c1486f76054301a.woff2 differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-900.bac8362e7a6ea60b6983.woff b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-900.bac8362e7a6ea60b6983.woff new file mode 100644 index 0000000000000000000000000000000000000000..4d50531e3e8970970a867c4b459c044bad3a7925 Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-900.bac8362e7a6ea60b6983.woff differ diff --git a/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-900italic.cb5ad999740e9d8a8bd1.woff2 b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-900italic.cb5ad999740e9d8a8bd1.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7d846b1a4c5d005c8171512bb667de92c44afa7b Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/client/build/static/media/roboto-latin-900italic.cb5ad999740e9d8a8bd1.woff2 differ diff --git a/lib/python3.12/site-packages/ray/dashboard/consts.py b/lib/python3.12/site-packages/ray/dashboard/consts.py new file mode 100644 index 0000000000000000000000000000000000000000..30505878cb80edef9a4f6751696fc3b3cbe3dfe5 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/consts.py @@ -0,0 +1,104 @@ +import os + +from ray._private.ray_constants import env_bool, env_float, env_integer + +DASHBOARD_LOG_FILENAME = "dashboard.log" +DASHBOARD_AGENT_ADDR_NODE_ID_PREFIX = "DASHBOARD_AGENT_ADDR_NODE_ID_PREFIX:" +DASHBOARD_AGENT_ADDR_IP_PREFIX = "DASHBOARD_AGENT_ADDR_IP_PREFIX:" +DASHBOARD_AGENT_LOG_FILENAME = "dashboard_agent.log" +DASHBOARD_AGENT_CHECK_PARENT_INTERVAL_S_ENV_NAME = ( + "RAY_DASHBOARD_AGENT_CHECK_PARENT_INTERVAL_S" # noqa +) +DASHBOARD_AGENT_CHECK_PARENT_INTERVAL_S = env_integer( + DASHBOARD_AGENT_CHECK_PARENT_INTERVAL_S_ENV_NAME, 0.4 +) +# The maximum time that parent can be considered +# as dead before agent kills itself. +_PARENT_DEATH_THREASHOLD = 5 +RAY_STATE_SERVER_MAX_HTTP_REQUEST_ENV_NAME = "RAY_STATE_SERVER_MAX_HTTP_REQUEST" +# Default number of in-progress requests to the state api server. +RAY_STATE_SERVER_MAX_HTTP_REQUEST = env_integer( + RAY_STATE_SERVER_MAX_HTTP_REQUEST_ENV_NAME, 100 +) +# Max allowed number of in-progress requests could be configured. +RAY_STATE_SERVER_MAX_HTTP_REQUEST_ALLOWED = 1000 + +RAY_DASHBOARD_STATS_PURGING_INTERVAL = env_integer( + "RAY_DASHBOARD_STATS_PURGING_INTERVAL", 60 * 10 +) +RAY_DASHBOARD_STATS_UPDATING_INTERVAL = env_integer( + "RAY_DASHBOARD_STATS_UPDATING_INTERVAL", 15 +) +GCS_SERVER_ADDRESS = "GcsServerAddress" +# GCS check alive +GCS_CHECK_ALIVE_INTERVAL_SECONDS = env_integer("GCS_CHECK_ALIVE_INTERVAL_SECONDS", 5) +GCS_RPC_TIMEOUT_SECONDS = env_integer("RAY_DASHBOARD_GCS_RPC_TIMEOUT_SECONDS", 60) +# aiohttp_cache +AIOHTTP_CACHE_TTL_SECONDS = 2 +AIOHTTP_CACHE_MAX_SIZE = 128 +AIOHTTP_CACHE_DISABLE_ENVIRONMENT_KEY = "RAY_DASHBOARD_NO_CACHE" +# Default value for datacenter (the default value in protobuf) +DEFAULT_LANGUAGE = "PYTHON" +DEFAULT_JOB_ID = "ffff" +# Hook that is invoked on the dashboard `/api/component_activities` endpoint. +# Environment variable stored here should be a callable that does not +# take any arguments and should return a dictionary mapping +# activity component type (str) to +# ray.dashboard.modules.api.api_head.RayActivityResponse. +# Example: "your.module.ray_cluster_activity_hook". +RAY_CLUSTER_ACTIVITY_HOOK = "RAY_CLUSTER_ACTIVITY_HOOK" + +# The number of candidate agents +CANDIDATE_AGENT_NUMBER = max(env_integer("CANDIDATE_AGENT_NUMBER", 1), 1) +# when head receive JobSubmitRequest, maybe not any agent is available, +# we need to wait for agents in other node start +WAIT_AVAILABLE_AGENT_TIMEOUT = 10 +TRY_TO_GET_AGENT_INFO_INTERVAL_SECONDS = 0.5 +RAY_JOB_ALLOW_DRIVER_ON_WORKER_NODES_ENV_VAR = "RAY_JOB_ALLOW_DRIVER_ON_WORKER_NODES" +RAY_STREAM_RUNTIME_ENV_LOG_TO_JOB_DRIVER_LOG_ENV_VAR = ( + "RAY_STREAM_RUNTIME_ENV_LOG_TO_JOB_DRIVER_LOG" +) + +# The max time to wait for the JobSupervisor to start before failing the job. +DEFAULT_JOB_START_TIMEOUT_SECONDS = 60 * 15 +RAY_JOB_START_TIMEOUT_SECONDS_ENV_VAR = "RAY_JOB_START_TIMEOUT_SECONDS" +# Port that dashboard prometheus metrics will be exported to +DASHBOARD_METRIC_PORT = env_integer("DASHBOARD_METRIC_PORT", 44227) + +# We use RayNodeType to mark head/worker nodes. IsHeadNode is retained +# for backward compatibility for user-customized dashboards that might rely on it +NODE_TAG_KEYS = ["ip", "Version", "SessionName", "IsHeadNode", "RayNodeType"] +GPU_TAG_KEYS = NODE_TAG_KEYS + ["GpuDeviceName", "GpuIndex"] + +# TpuDeviceName and TpuIndex are expected to be equal to the number of TPU +# chips in the cluster. TpuType and TpuTopology are proportional to the number +# of node pools. +TPU_TAG_KEYS = NODE_TAG_KEYS + ["TpuDeviceName", "TpuIndex", "TpuType", "TpuTopology"] +CLUSTER_TAG_KEYS = ["node_type", "Version", "SessionName"] +COMPONENT_METRICS_TAG_KEYS = ["ip", "pid", "Version", "Component", "SessionName"] +COMPONENT_GPU_TAG_KEYS = GPU_TAG_KEYS + COMPONENT_METRICS_TAG_KEYS + +# Dashboard metrics are tracked separately at the dashboard. TODO(sang): Support GCS. +# Note that for dashboard subprocess module, the component name is "dashboard_[module_name]". +AVAILABLE_COMPONENT_NAMES_FOR_METRICS = { + "workers", + "raylet", + "agent", + "dashboard", + "gcs", +} +METRICS_INPUT_ROOT = os.path.join( + os.path.dirname(__file__), "modules", "metrics", "export" +) +METRICS_RECORD_INTERVAL_S = env_integer("METRICS_RECORD_INTERVAL_S", 5) +PROMETHEUS_CONFIG_INPUT_PATH = os.path.join( + METRICS_INPUT_ROOT, "prometheus", "prometheus.yml" +) +PARENT_HEALTH_CHECK_BY_PIPE = env_bool( + "RAY_enable_pipe_based_agent_to_parent_health_check", False +) + +# Maximum time to wait for the subprocess module to be ready. +SUBPROCESS_MODULE_WAIT_READY_TIMEOUT = env_float( + "RAY_DASHBOARD_SUBPROCESS_MODULE_WAIT_READY_TIMEOUT", 30.0 +) diff --git a/lib/python3.12/site-packages/ray/dashboard/dashboard.py b/lib/python3.12/site-packages/ray/dashboard/dashboard.py new file mode 100644 index 0000000000000000000000000000000000000000..f774e22300d5855b746750eec99ed58d5f91d480 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/dashboard.py @@ -0,0 +1,316 @@ +import argparse +import logging +import os +import platform +import signal +import sys +import traceback +from typing import Optional, Set + +import ray +import ray._private.ray_constants as ray_constants +import ray.dashboard.consts as dashboard_consts +import ray.dashboard.head as dashboard_head +import ray.dashboard.utils as dashboard_utils +from ray._common.ray_constants import ( + LOGGING_ROTATE_BACKUP_COUNT, + LOGGING_ROTATE_BYTES, +) +from ray._common.utils import get_or_create_event_loop +from ray._private import logging_utils +from ray._private.ray_logging import setup_component_logger +from ray._private.utils import ( + format_error_message, + publish_error_to_driver, +) + +# Logger for this module. It should be configured at the entry point +# into the program using Ray. Ray provides a default configuration at +# entry/init points. +logger = logging.getLogger(__name__) + + +class Dashboard: + """A dashboard process for monitoring Ray nodes. + + This dashboard is made up of a REST API which collates data published by + Reporter processes on nodes into a json structure, and a webserver + which polls said API for display purposes. + + Args: + host: Host address of dashboard aiohttp server. + port: Port number of dashboard aiohttp server. + port_retries: The retry times to select a valid port. + gcs_address: GCS address of the cluster. + cluster_id_hex: Cluster ID hex string. + node_ip_address: The IP address of the dashboard. + serve_frontend: If configured, frontend HTML + is not served from the dashboard. + log_dir: Log directory of dashboard. + logging_level: The logging level (e.g. logging.INFO, logging.DEBUG) + logging_format: The format string for log messages + logging_filename: The name of the log file + logging_rotate_bytes: Max size in bytes before rotating log file + logging_rotate_backup_count: Number of backup files to keep when rotating + """ + + def __init__( + self, + host: str, + port: int, + port_retries: int, + gcs_address: str, + cluster_id_hex: str, + node_ip_address: str, + log_dir: str, + logging_level: int, + logging_format: str, + logging_filename: str, + logging_rotate_bytes: int, + logging_rotate_backup_count: int, + temp_dir: str = None, + session_dir: str = None, + minimal: bool = False, + serve_frontend: bool = True, + modules_to_load: Optional[Set[str]] = None, + ): + self.dashboard_head = dashboard_head.DashboardHead( + http_host=host, + http_port=port, + http_port_retries=port_retries, + gcs_address=gcs_address, + cluster_id_hex=cluster_id_hex, + node_ip_address=node_ip_address, + log_dir=log_dir, + logging_level=logging_level, + logging_format=logging_format, + logging_filename=logging_filename, + logging_rotate_bytes=logging_rotate_bytes, + logging_rotate_backup_count=logging_rotate_backup_count, + temp_dir=temp_dir, + session_dir=session_dir, + minimal=minimal, + serve_frontend=serve_frontend, + modules_to_load=modules_to_load, + ) + + async def run(self): + await self.dashboard_head.run() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Ray dashboard.") + parser.add_argument( + "--host", required=True, type=str, help="The host to use for the HTTP server." + ) + parser.add_argument( + "--port", required=True, type=int, help="The port to use for the HTTP server." + ) + parser.add_argument( + "--port-retries", + required=False, + type=int, + default=0, + help="The retry times to select a valid port.", + ) + parser.add_argument( + "--gcs-address", required=True, type=str, help="The address (ip:port) of GCS." + ) + parser.add_argument( + "--cluster-id-hex", required=True, type=str, help="The cluster ID in hex." + ) + parser.add_argument( + "--node-ip-address", + required=True, + type=str, + help="The IP address of the node where this is running.", + ) + parser.add_argument( + "--logging-level", + required=False, + type=lambda s: logging.getLevelName(s.upper()), + default=ray_constants.LOGGER_LEVEL, + choices=ray_constants.LOGGER_LEVEL_CHOICES, + help=ray_constants.LOGGER_LEVEL_HELP, + ) + parser.add_argument( + "--logging-format", + required=False, + type=str, + default=ray_constants.LOGGER_FORMAT, + help=ray_constants.LOGGER_FORMAT_HELP, + ) + parser.add_argument( + "--logging-filename", + required=False, + type=str, + default=dashboard_consts.DASHBOARD_LOG_FILENAME, + help="Specify the name of log file, " + 'log to stdout if set empty, default is "{}"'.format( + dashboard_consts.DASHBOARD_LOG_FILENAME + ), + ) + parser.add_argument( + "--logging-rotate-bytes", + required=False, + type=int, + default=LOGGING_ROTATE_BYTES, + help="Specify the max bytes for rotating " + "log file, default is {} bytes.".format(LOGGING_ROTATE_BYTES), + ) + parser.add_argument( + "--logging-rotate-backup-count", + required=False, + type=int, + default=LOGGING_ROTATE_BACKUP_COUNT, + help="Specify the backup count of rotated log file, default is {}.".format( + LOGGING_ROTATE_BACKUP_COUNT + ), + ) + parser.add_argument( + "--log-dir", + required=True, + type=str, + default=None, + help="Specify the path of log directory.", + ) + parser.add_argument( + "--temp-dir", + required=True, + type=str, + default=None, + help="Specify the path of the temporary directory use by Ray process.", + ) + parser.add_argument( + "--session-dir", + required=True, + type=str, + default=None, + help="Specify the path of the session directory of the cluster.", + ) + parser.add_argument( + "--minimal", + action="store_true", + help=( + "Minimal dashboard only contains a subset of features that don't " + "require additional dependencies installed when ray is installed " + "by `pip install ray[default]`." + ), + ) + parser.add_argument( + "--modules-to-load", + required=False, + default=None, + help=( + "Specify the list of module names in [module_1],[module_2] format." + "E.g., JobHead,StateHead... " + "If nothing is specified, all modules are loaded." + ), + ) + parser.add_argument( + "--disable-frontend", + action="store_true", + help=("If configured, frontend html is not served from the server."), + ) + parser.add_argument( + "--stdout-filepath", + required=False, + type=str, + default="", + help="The filepath to dump dashboard stdout.", + ) + parser.add_argument( + "--stderr-filepath", + required=False, + type=str, + default="", + help="The filepath to dump dashboard stderr.", + ) + + args = parser.parse_args() + + try: + # Disable log rotation for windows platform. + logging_rotation_bytes = ( + args.logging_rotate_bytes if sys.platform != "win32" else 0 + ) + logging_rotation_backup_count = ( + args.logging_rotate_backup_count if sys.platform != "win32" else 1 + ) + setup_component_logger( + logging_level=args.logging_level, + logging_format=args.logging_format, + log_dir=args.log_dir, + filename=args.logging_filename, + max_bytes=logging_rotation_bytes, + backup_count=logging_rotation_backup_count, + ) + + # Setup stdout/stderr redirect files if redirection enabled. + logging_utils.redirect_stdout_stderr_if_needed( + args.stdout_filepath, + args.stderr_filepath, + logging_rotation_bytes, + logging_rotation_backup_count, + ) + + if args.modules_to_load: + modules_to_load = set(args.modules_to_load.strip(" ,").split(",")) + else: + # None == default. + modules_to_load = None + + loop = get_or_create_event_loop() + dashboard = Dashboard( + host=args.host, + port=args.port, + port_retries=args.port_retries, + gcs_address=args.gcs_address, + cluster_id_hex=args.cluster_id_hex, + node_ip_address=args.node_ip_address, + log_dir=args.log_dir, + logging_level=args.logging_level, + logging_format=args.logging_format, + logging_filename=args.logging_filename, + logging_rotate_bytes=logging_rotation_bytes, + logging_rotate_backup_count=logging_rotation_backup_count, + temp_dir=args.temp_dir, + session_dir=args.session_dir, + minimal=args.minimal, + serve_frontend=(not args.disable_frontend), + modules_to_load=modules_to_load, + ) + + def sigterm_handler(): + logger.warning("Exiting with SIGTERM immediately...") + os._exit(signal.SIGTERM) + + if sys.platform != "win32": + # TODO(rickyyx): we currently do not have any logic for actual + # graceful termination in the dashboard. Most of the underlying + # async tasks run by the dashboard head doesn't handle CancelledError. + # So a truly graceful shutdown is not trivial w/o much refactoring. + # Re-open the issue: https://github.com/ray-project/ray/issues/25518 + # if a truly graceful shutdown is required. + loop.add_signal_handler(signal.SIGTERM, sigterm_handler) + + loop.run_until_complete(dashboard.run()) + except Exception as e: + traceback_str = format_error_message(traceback.format_exc()) + message = ( + f"The dashboard on node {platform.uname()[1]} " + f"failed with the following " + f"error:\n{traceback_str}" + ) + if isinstance(e, dashboard_utils.FrontendNotFoundError): + logger.warning(message) + else: + logger.error(message) + raise e + + # Something went wrong, so push an error to all drivers. + publish_error_to_driver( + ray_constants.DASHBOARD_DIED_ERROR, + message, + gcs_client=ray._raylet.GcsClient(address=args.gcs_address), + ) diff --git a/lib/python3.12/site-packages/ray/dashboard/dashboard_metrics.py b/lib/python3.12/site-packages/ray/dashboard/dashboard_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..7f1b6f2b22a9ed826409f80542e333aa51a5b037 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/dashboard_metrics.py @@ -0,0 +1,123 @@ +from typing import Optional + +from ray.dashboard.consts import COMPONENT_METRICS_TAG_KEYS + + +class NullMetric: + """Mock metric class to be used in case of prometheus_client import error.""" + + def set(self, *args, **kwargs): + pass + + def observe(self, *args, **kwargs): + pass + + def inc(self, *args, **kwargs): + pass + + +try: + + from prometheus_client import CollectorRegistry, Counter, Gauge, Histogram + + # The metrics in this class should be kept in sync with + # python/ray/tests/test_metrics_agent.py + class DashboardPrometheusMetrics: + def __init__(self, registry: Optional[CollectorRegistry] = None): + self.registry: CollectorRegistry = registry or CollectorRegistry( + auto_describe=True + ) + # Buckets: 5ms, 10ms, 25ms, 50ms, 75ms + # 100ms, 250ms, 500ms, 750ms + # 1s, 2.5s, 5s, 7.5s, 10s + # 20s, 40s, 60s + # used for API duration + histogram_buckets_s = [ + 0.005, + 0.01, + 0.025, + 0.05, + 0.075, + 0.1, + 0.25, + 0.5, + 0.75, + 1, + 2.5, + 5, + 7.5, + 10, + 20, + 40, + 60, + ] + self.metrics_request_duration = Histogram( + "dashboard_api_requests_duration_seconds", + "Total duration in seconds per endpoint", + ("endpoint", "http_status", "Version", "SessionName", "Component"), + unit="seconds", + namespace="ray", + registry=self.registry, + buckets=histogram_buckets_s, + ) + self.metrics_request_count = Counter( + "dashboard_api_requests_count", + "Total requests count per endpoint", + ( + "method", + "endpoint", + "http_status", + "Version", + "SessionName", + "Component", + ), + unit="requests", + namespace="ray", + registry=self.registry, + ) + self.metrics_event_loop_tasks = Gauge( + "dashboard_event_loop_tasks", + "Number of tasks currently pending in the event loop's queue.", + tuple(COMPONENT_METRICS_TAG_KEYS), + unit="tasks", + namespace="ray", + registry=self.registry, + ) + self.metrics_event_loop_lag = Gauge( + "dashboard_event_loop_lag", + "Event loop lag in seconds.", + tuple(COMPONENT_METRICS_TAG_KEYS), + unit="seconds", + namespace="ray", + registry=self.registry, + ) + self.metrics_dashboard_cpu = Gauge( + "component_cpu", + "Dashboard CPU percentage usage.", + tuple(COMPONENT_METRICS_TAG_KEYS), + unit="percentage", + namespace="ray", + registry=self.registry, + ) + self.metrics_dashboard_mem_uss = Gauge( + "component_uss", + "USS usage of all components on the node.", + tuple(COMPONENT_METRICS_TAG_KEYS), + unit="mb", + namespace="ray", + registry=self.registry, + ) + self.metrics_dashboard_mem_rss = Gauge( + "component_rss", + "RSS usage of all components on the node.", + tuple(COMPONENT_METRICS_TAG_KEYS), + unit="mb", + namespace="ray", + registry=self.registry, + ) + +except ImportError: + + class DashboardPrometheusMetrics(object): + def __getattr__(self, attr): + return NullMetric() diff --git a/lib/python3.12/site-packages/ray/dashboard/head.py b/lib/python3.12/site-packages/ray/dashboard/head.py new file mode 100644 index 0000000000000000000000000000000000000000..94a8bb3cf380f7919015e0ac8b80ee1d3de7d407 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/head.py @@ -0,0 +1,474 @@ +import asyncio +import logging +import os +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from typing import TYPE_CHECKING, List, Optional, Set, Tuple + +import ray +import ray.dashboard.consts as dashboard_consts +import ray.dashboard.utils as dashboard_utils +import ray.experimental.internal_kv as internal_kv +from ray._common.network_utils import build_address +from ray._common.usage.usage_lib import TagKey, record_extra_usage_tag +from ray._private import ray_constants +from ray._private.async_utils import enable_monitor_loop_lag +from ray._private.ray_constants import env_integer +from ray._raylet import GcsClient +from ray.dashboard.consts import ( + AVAILABLE_COMPONENT_NAMES_FOR_METRICS, + DASHBOARD_METRIC_PORT, +) +from ray.dashboard.dashboard_metrics import DashboardPrometheusMetrics +from ray.dashboard.utils import ( + DashboardHeadModule, + DashboardHeadModuleConfig, + async_loop_forever, +) + +import psutil + +try: + import prometheus_client +except ImportError: + prometheus_client = None + +if TYPE_CHECKING: + from ray.dashboard.subprocesses.handle import SubprocessModuleHandle + +logger = logging.getLogger(__name__) + +# NOTE: Executor in this head is intentionally constrained to just 1 thread by +# default to limit its concurrency, therefore reducing potential for +# GIL contention +RAY_DASHBOARD_DASHBOARD_HEAD_TPE_MAX_WORKERS = env_integer( + "RAY_DASHBOARD_DASHBOARD_HEAD_TPE_MAX_WORKERS", 1 +) + + +class DashboardHead: + def __init__( + self, + http_host: str, + http_port: int, + http_port_retries: int, + gcs_address: str, + cluster_id_hex: str, + node_ip_address: str, + log_dir: str, + logging_level: int, + logging_format: str, + logging_filename: str, + logging_rotate_bytes: int, + logging_rotate_backup_count: int, + temp_dir: str, + session_dir: str, + minimal: bool, + serve_frontend: bool, + modules_to_load: Optional[Set[str]] = None, + ): + """ + Args: + http_host: The host address for the Http server. + http_port: The port for the Http server. + http_port_retries: The maximum retry to bind ports for the Http server. + gcs_address: The GCS address in the {address}:{port} format. + log_dir: The log directory. E.g., /tmp/session_latest/logs. + logging_level: The logging level (e.g. logging.INFO, logging.DEBUG) + logging_format: The format string for log messages + logging_filename: The name of the log file + logging_rotate_bytes: Max size in bytes before rotating log file + logging_rotate_backup_count: Number of backup files to keep when rotating + temp_dir: The temp directory. E.g., /tmp. + session_dir: The session directory. E.g., tmp/session_latest. + minimal: Whether or not it will load the minimal modules. + serve_frontend: If configured, frontend HTML is + served from the dashboard. + modules_to_load: A set of module name in string to load. + By default (None), it loads all available modules. + Note that available modules could be changed depending on + minimal flags. + """ + self.minimal = minimal + self.serve_frontend = serve_frontend + # If it is the minimal mode, we shouldn't serve frontend. + if self.minimal: + self.serve_frontend = False + # Public attributes are accessible for all head modules. + # Walkaround for issue: https://github.com/ray-project/ray/issues/7084 + self.http_host = "127.0.0.1" if http_host == "localhost" else http_host + self.http_port = http_port + self.http_port_retries = http_port_retries + self._modules_to_load = modules_to_load + self._modules_loaded = False + self.metrics = None + + self._executor = ThreadPoolExecutor( + max_workers=RAY_DASHBOARD_DASHBOARD_HEAD_TPE_MAX_WORKERS, + thread_name_prefix="dashboard_head_executor", + ) + + assert gcs_address is not None + self.gcs_address = gcs_address + self.cluster_id_hex = cluster_id_hex + self.log_dir = log_dir + self.logging_level = logging_level + self.logging_format = logging_format + self.logging_filename = logging_filename + self.logging_rotate_bytes = logging_rotate_bytes + self.logging_rotate_backup_count = logging_rotate_backup_count + self.temp_dir = temp_dir + self.session_dir = session_dir + self.session_name = Path(session_dir).name + self.gcs_error_subscriber = None + self.gcs_log_subscriber = None + self.ip = node_ip_address + self.pid = os.getpid() + self.dashboard_proc = psutil.Process() + + # If the dashboard is started as non-minimal version, http server should + # be configured to expose APIs. + self.http_server = None + + async def _configure_http_server( + self, + dashboard_head_modules: List[DashboardHeadModule], + subprocess_module_handles: List["SubprocessModuleHandle"], + ): + from ray.dashboard.http_server_head import HttpServerDashboardHead + + self.http_server = HttpServerDashboardHead( + self.ip, + self.http_host, + self.http_port, + self.http_port_retries, + self.gcs_address, + self.session_name, + self.metrics, + ) + await self.http_server.run(dashboard_head_modules, subprocess_module_handles) + + @property + def http_session(self): + if not self._modules_loaded and not self.http_server: + # When the dashboard is still starting up, this property gets + # called as part of the method_route_table_factory magic. In + # this case, the property is not actually used but the magic + # method calls every property to look for a route to add to + # the global route table. It should be okay for http_server + # to still be None at this point. + return None + assert self.http_server, "Accessing unsupported API in a minimal ray." + return self.http_server.http_session + + @async_loop_forever(dashboard_consts.GCS_CHECK_ALIVE_INTERVAL_SECONDS) + async def _gcs_check_alive(self): + try: + # If gcs is permanently dead, gcs client will exit the process + # (see gcs_rpc_client.h) + await self.gcs_client.async_check_alive(node_ids=[], timeout=None) + except Exception: + logger.warning("Failed to check gcs aliveness, will retry", exc_info=True) + + def _load_modules( + self, modules_to_load: Optional[Set[str]] = None + ) -> Tuple[List[DashboardHeadModule], List["SubprocessModuleHandle"]]: + """ + If minimal, only load DashboardHeadModule. + If non-minimal, load both kinds of modules: DashboardHeadModule, SubprocessModule. + + If modules_to_load is not None, only load the modules in the set. + """ + dashboard_head_modules = self._load_dashboard_head_modules(modules_to_load) + subprocess_module_handles = self._load_subprocess_module_handles( + modules_to_load + ) + + all_names = {type(m).__name__ for m in dashboard_head_modules} | { + h.module_cls.__name__ for h in subprocess_module_handles + } + assert len(all_names) == len(dashboard_head_modules) + len( + subprocess_module_handles + ), "Duplicate module names. A module name can't be a DashboardHeadModule and a SubprocessModule at the same time." + + # Verify modules are loaded as expected. + if modules_to_load is not None and all_names != modules_to_load: + assert False, ( + f"Actual loaded modules {all_names}, doesn't match the requested modules " + f"to load, {modules_to_load}." + ) + + self._modules_loaded = True + return dashboard_head_modules, subprocess_module_handles + + def _load_dashboard_head_modules( + self, modules_to_load: Optional[Set[str]] = None + ) -> List[DashboardHeadModule]: + """Load `DashboardHeadModule`s. + + Args: + modules: A list of module names to load. By default (None), + it loads all modules. + """ + modules = [] + head_cls_list = dashboard_utils.get_all_modules(DashboardHeadModule) + + config = DashboardHeadModuleConfig( + minimal=self.minimal, + cluster_id_hex=self.cluster_id_hex, + session_name=self.session_name, + gcs_address=self.gcs_address, + log_dir=self.log_dir, + temp_dir=self.temp_dir, + session_dir=self.session_dir, + ip=self.ip, + http_host=self.http_host, + http_port=self.http_port, + ) + + # Select modules to load. + if modules_to_load is not None: + head_cls_list = [ + cls for cls in head_cls_list if cls.__name__ in modules_to_load + ] + + logger.info(f"DashboardHeadModules to load: {modules_to_load}.") + + for cls in head_cls_list: + logger.info(f"Loading {DashboardHeadModule.__name__}: {cls}.") + c = cls(config) + modules.append(c) + + logger.info(f"Loaded {len(modules)} dashboard head modules: {modules}.") + return modules + + def _load_subprocess_module_handles( + self, modules_to_load: Optional[Set[str]] = None + ) -> List["SubprocessModuleHandle"]: + """ + If minimal, return an empty list. + If non-minimal, load `SubprocessModule`s by creating Handles to them. + + Args: + modules: A list of module names to load. By default (None), + it loads all modules. + """ + if self.minimal: + logger.info("Subprocess modules not loaded in minimal mode.") + return [] + + from ray.dashboard.subprocesses.handle import SubprocessModuleHandle + from ray.dashboard.subprocesses.module import ( + SubprocessModule, + SubprocessModuleConfig, + ) + + handles = [] + subprocess_cls_list = dashboard_utils.get_all_modules(SubprocessModule) + + loop = ray._common.utils.get_or_create_event_loop() + config = SubprocessModuleConfig( + cluster_id_hex=self.cluster_id_hex, + gcs_address=self.gcs_address, + session_name=self.session_name, + temp_dir=self.temp_dir, + session_dir=self.session_dir, + logging_level=self.logging_level, + logging_format=self.logging_format, + log_dir=self.log_dir, + logging_filename=self.logging_filename, + logging_rotate_bytes=self.logging_rotate_bytes, + logging_rotate_backup_count=self.logging_rotate_backup_count, + socket_dir=str(Path(self.session_dir) / "sockets"), + ) + + # Select modules to load. + if modules_to_load is not None: + subprocess_cls_list = [ + cls for cls in subprocess_cls_list if cls.__name__ in modules_to_load + ] + + for cls in subprocess_cls_list: + logger.info(f"Loading {SubprocessModule.__name__}: {cls}.") + handle = SubprocessModuleHandle(loop, cls, config) + handles.append(handle) + + logger.info(f"Loaded {len(handles)} subprocess modules: {handles}.") + return handles + + async def _setup_metrics(self, gcs_client): + metrics = DashboardPrometheusMetrics() + + # Setup prometheus metrics export server + assert internal_kv._internal_kv_initialized() + assert gcs_client is not None + address = build_address(self.ip, DASHBOARD_METRIC_PORT) + await gcs_client.async_internal_kv_put( + "DashboardMetricsAddress".encode(), address.encode(), True, namespace=None + ) + if prometheus_client: + try: + logger.info( + "Starting dashboard metrics server on port {}".format( + DASHBOARD_METRIC_PORT + ) + ) + kwargs = {"addr": "127.0.0.1"} if self.ip == "127.0.0.1" else {} + prometheus_client.start_http_server( + port=DASHBOARD_METRIC_PORT, + registry=metrics.registry, + **kwargs, + ) + except Exception: + logger.exception( + "An exception occurred while starting the metrics server." + ) + elif not prometheus_client: + logger.warning( + "`prometheus_client` not found, so metrics will not be exported." + ) + + return metrics + + @dashboard_utils.async_loop_forever(dashboard_consts.METRICS_RECORD_INTERVAL_S) + async def _record_dashboard_metrics( + self, subprocess_module_handles: List["SubprocessModuleHandle"] + ): + labels = { + "ip": self.ip, + "pid": self.pid, + "Version": ray.__version__, + "Component": "dashboard", + "SessionName": self.session_name, + } + assert "dashboard" in AVAILABLE_COMPONENT_NAMES_FOR_METRICS + self._record_cpu_mem_metrics_for_proc(self.dashboard_proc) + for subprocess_module_handle in subprocess_module_handles: + assert subprocess_module_handle.process is not None + proc = psutil.Process(subprocess_module_handle.process.pid) + self._record_cpu_mem_metrics_for_proc( + proc, subprocess_module_handle.module_cls.__name__ + ) + + loop = ray._common.utils.get_or_create_event_loop() + + self.metrics.metrics_event_loop_tasks.labels(**labels).set( + len(asyncio.all_tasks(loop)) + ) + + # Report the max lag since the last export, if any. + if self._event_loop_lag_s_max is not None: + self.metrics.metrics_event_loop_lag.labels(**labels).set( + float(self._event_loop_lag_s_max) + ) + self._event_loop_lag_s_max = None + + def _record_cpu_mem_metrics_for_proc( + self, proc: psutil.Process, module_name: str = "" + ): + labels = { + "ip": self.ip, + "pid": proc.pid, + "Version": ray.__version__, + "Component": "dashboard" if not module_name else "dashboard_" + module_name, + "SessionName": self.session_name, + } + proc_attrs = proc.as_dict(attrs=["cpu_percent", "memory_full_info"]) + self.metrics.metrics_dashboard_cpu.labels(**labels).set( + float(proc_attrs.get("cpu_percent", 0.0)) + ) + # memory_full_info is None on Mac due to the permission issue + # (https://github.com/giampaolo/psutil/issues/883) + if proc_attrs.get("memory_full_info") is not None: + self.metrics.metrics_dashboard_mem_uss.labels(**labels).set( + float(proc_attrs.get("memory_full_info").uss) / 1.0e6 + ) + self.metrics.metrics_dashboard_mem_rss.labels(**labels).set( + float(proc_attrs.get("memory_full_info").rss) / 1.0e6 + ) + + async def run(self): + gcs_address = self.gcs_address + + # Dashboard will handle connection failure automatically + self.gcs_client = GcsClient(address=gcs_address, cluster_id=self.cluster_id_hex) + internal_kv._initialize_internal_kv(self.gcs_client) + + dashboard_head_modules, subprocess_module_handles = self._load_modules( + self._modules_to_load + ) + # Parallel start all subprocess modules. + for handle in subprocess_module_handles: + handle.start_module() + # Wait for all subprocess modules to be ready. + for handle in subprocess_module_handles: + handle.wait_for_module_ready() + + if not self.minimal: + self.metrics = await self._setup_metrics(self.gcs_client) + self._event_loop_lag_s_max: Optional[float] = None + + def on_new_lag(lag_s): + # Record the lag. It's exported in `record_dashboard_metrics` + self._event_loop_lag_s_max = max(self._event_loop_lag_s_max or 0, lag_s) + + enable_monitor_loop_lag(on_new_lag) + + self.record_dashboard_metrics_task = asyncio.create_task( + self._record_dashboard_metrics(subprocess_module_handles) + ) + + try: + assert internal_kv._internal_kv_initialized() + # Note: We always record the usage, but it is not reported + # if the usage stats is disabled. + record_extra_usage_tag(TagKey.DASHBOARD_USED, "False") + except Exception as e: + logger.warning( + "Failed to record the dashboard usage. " + "This error message is harmless and can be ignored. " + f"Error: {e}" + ) + + http_host, http_port = self.http_host, self.http_port + if self.serve_frontend: + logger.info("Initialize the http server.") + await self._configure_http_server( + dashboard_head_modules, subprocess_module_handles + ) + http_host, http_port = self.http_server.get_address() + logger.info( + f"http server initialized at {build_address(http_host, http_port)}" + ) + else: + logger.info("http server disabled.") + + # We need to expose dashboard's node's ip for other worker nodes + # if it's listening to all interfaces. + dashboard_http_host = ( + self.ip + if self.http_host != ray_constants.DEFAULT_DASHBOARD_IP + else http_host + ) + # This synchronous code inside an async context is not great. + # It is however acceptable, because this only gets run once + # during initialization and therefore cannot block the event loop. + # This could be done better in the future, including + # removing the polling on the Ray side, by communicating the + # server address to Ray via stdin / stdout or a pipe. + self.gcs_client.internal_kv_put( + ray_constants.DASHBOARD_ADDRESS.encode(), + build_address(dashboard_http_host, http_port).encode(), + True, + namespace=ray_constants.KV_NAMESPACE_DASHBOARD, + ) + + concurrent_tasks = [ + self._gcs_check_alive(), + ] + for m in dashboard_head_modules: + concurrent_tasks.append(m.run()) + await asyncio.gather(*concurrent_tasks) + + if self.http_server: + await self.http_server.cleanup() diff --git a/lib/python3.12/site-packages/ray/dashboard/http_server_agent.py b/lib/python3.12/site-packages/ray/dashboard/http_server_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..b9146066933f796dd4389b36273f55c044965abc --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/http_server_agent.py @@ -0,0 +1,137 @@ +import asyncio +import logging +import random +from typing import List, Optional + +from packaging.version import Version + +import ray.dashboard.optional_utils as dashboard_optional_utils +from ray._common.network_utils import build_address, is_localhost +from ray._common.utils import get_or_create_event_loop +from ray.dashboard.optional_deps import aiohttp, aiohttp_cors, hdrs + +logger = logging.getLogger(__name__) +routes = dashboard_optional_utils.DashboardAgentRouteTable + + +class HttpServerAgent: + def __init__(self, ip: str, listen_port: int) -> None: + self.ip = ip + self.listen_port = listen_port + self.http_host = None + self.http_port = None + self.http_session = None + self.runner = None + + async def _start_site_with_retry( + self, max_retries: int = 5, base_delay: float = 0.1 + ) -> aiohttp.web.TCPSite: + """Start the TCP site with retry logic and exponential backoff. + + Args: + max_retries: Maximum number of retry attempts + base_delay: Base delay in seconds for exponential backoff + + Returns: + The started site object + + Raises: + OSError: If all retry attempts fail + """ + last_exception: Optional[OSError] = None + + for attempt in range(max_retries + 1): # +1 for initial attempt + try: + site = aiohttp.web.TCPSite( + self.runner, + self.ip, + self.listen_port, + ) + await site.start() + if not is_localhost(self.ip): + local_site = aiohttp.web.TCPSite( + self.runner, + "127.0.0.1", + self.listen_port, + ) + await local_site.start() + if attempt > 0: + logger.info( + f"Successfully started agent on port {self.listen_port} " + f"after {attempt} retry attempts" + ) + return site + + except OSError as e: + last_exception = e + if attempt < max_retries: + # Calculate exponential backoff with jitter + delay = base_delay * (2**attempt) + random.uniform(0, 0.1) + logger.warning( + f"Failed to bind to port {self.listen_port} (attempt {attempt + 1}/" + f"{max_retries + 1}). Retrying in {delay:.2f}s. Error: {e}" + ) + await asyncio.sleep(delay) + else: + logger.exception( + f"Agent port #{self.listen_port} failed to bind after " + f"{max_retries + 1} attempts." + ) + break + + # If we get here, all retries failed + raise last_exception + + async def start(self, modules: List) -> None: + # Create a http session for all modules. + # aiohttp<4.0.0 uses a 'loop' variable, aiohttp>=4.0.0 doesn't anymore + if Version(aiohttp.__version__) < Version("4.0.0"): + self.http_session = aiohttp.ClientSession(loop=get_or_create_event_loop()) + else: + self.http_session = aiohttp.ClientSession() + + # Bind routes for every module so that each module + # can use decorator-style routes. + for c in modules: + dashboard_optional_utils.DashboardAgentRouteTable.bind(c) + + app = aiohttp.web.Application() + app.add_routes(routes=routes.bound_routes()) + + # Enable CORS on all routes. + cors = aiohttp_cors.setup( + app, + defaults={ + "*": aiohttp_cors.ResourceOptions( + allow_credentials=True, + expose_headers="*", + allow_methods="*", + allow_headers=("Content-Type", "X-Header"), + ) + }, + ) + for route in list(app.router.routes()): + cors.add(route) + + self.runner = aiohttp.web.AppRunner(app) + await self.runner.setup() + + # Start the site with retry logic + site = await self._start_site_with_retry() + + self.http_host, self.http_port, *_ = site._server.sockets[0].getsockname() + logger.info( + "Dashboard agent http address: %s", + build_address(self.http_host, self.http_port), + ) + + # Dump registered http routes. + dump_routes = [r for r in app.router.routes() if r.method != hdrs.METH_HEAD] + for r in dump_routes: + logger.info(r) + logger.info("Registered %s routes.", len(dump_routes)) + + async def cleanup(self) -> None: + # Wait for finish signal. + await self.runner.cleanup() + await self.http_session.close() diff --git a/lib/python3.12/site-packages/ray/dashboard/http_server_head.py b/lib/python3.12/site-packages/ray/dashboard/http_server_head.py new file mode 100644 index 0000000000000000000000000000000000000000..39d8dcb8d6dd254a5dc12964a58f677356b7a1b2 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/http_server_head.py @@ -0,0 +1,452 @@ +import asyncio +import errno +import ipaddress +import logging +import os +import pathlib +import posixpath +import sys +import time +from math import floor +from typing import List, Set + +from packaging.version import Version + +import ray +import ray.dashboard.optional_utils as dashboard_optional_utils +import ray.dashboard.timezone_utils as timezone_utils +import ray.dashboard.utils as dashboard_utils +from ray import ray_constants +from ray._common.network_utils import build_address, parse_address +from ray._common.usage.usage_lib import TagKey, record_extra_usage_tag +from ray._common.utils import get_or_create_event_loop +from ray._private.authentication import ( + authentication_constants, + authentication_utils as auth_utils, +) +from ray._private.authentication.http_token_authentication import ( + get_token_auth_middleware, +) +from ray._raylet import AuthenticationMode, get_authentication_mode +from ray.dashboard.dashboard_metrics import DashboardPrometheusMetrics +from ray.dashboard.head import DashboardHeadModule + +# All third-party dependencies that are not included in the minimal Ray +# installation must be included in this file. This allows us to determine if +# the agent has the necessary dependencies to be started. +from ray.dashboard.optional_deps import aiohttp, hdrs +from ray.dashboard.subprocesses.handle import SubprocessModuleHandle +from ray.dashboard.subprocesses.routes import SubprocessRouteTable + +# Logger for this module. It should be configured at the entry point +# into the program using Ray. Ray provides a default configuration at +# entry/init points. +logger = logging.getLogger(__name__) +routes = dashboard_optional_utils.DashboardHeadRouteTable + +# Env var that enables follow_symlinks for serving UI static files. +# This is an advanced setting that should only be used with special Ray installations +# where the dashboard build files are symlinked to a different directory. +# This is not recommended for most users and can pose a security risk. +# Please reference the aiohttp docs here: +# https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.UrlDispatcher.add_static +ENV_VAR_FOLLOW_SYMLINKS = "RAY_DASHBOARD_BUILD_FOLLOW_SYMLINKS" +FOLLOW_SYMLINKS_ENABLED = os.environ.get(ENV_VAR_FOLLOW_SYMLINKS) == "1" +if FOLLOW_SYMLINKS_ENABLED: + logger.warning( + "Enabling RAY_DASHBOARD_BUILD_FOLLOW_SYMLINKS is not recommended as it " + "allows symlinks to directories outside the dashboard build folder. " + "You may accidentally expose files on your system outside of the " + "build directory." + ) + + +def setup_static_dir(): + build_dir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "client", "build" + ) + module_name = os.path.basename(os.path.dirname(__file__)) + if not os.path.isdir(build_dir): + raise dashboard_utils.FrontendNotFoundError( + errno.ENOENT, + "Dashboard build directory not found. If installing " + "from source, please follow the additional steps " + "required to build the dashboard" + f"(cd python/ray/{module_name}/client " + "&& npm ci " + "&& npm run build)", + build_dir, + ) + + static_dir = os.path.join(build_dir, "static") + routes.static("/static", static_dir, follow_symlinks=FOLLOW_SYMLINKS_ENABLED) + return build_dir + + +class HttpServerDashboardHead: + def __init__( + self, + ip: str, + http_host: str, + http_port: int, + http_port_retries: int, + gcs_address: str, + session_name: str, + metrics: DashboardPrometheusMetrics, + ): + self.ip = ip + self.http_host = http_host + self.http_port = http_port + self.http_port_retries = http_port_retries + self.head_node_ip = parse_address(gcs_address)[0] + self.metrics = metrics + self._session_name = session_name + + # Below attirubtes are filled after `run` API is invoked. + self.runner = None + + # Setup Dashboard Routes + try: + build_dir = setup_static_dir() + logger.info("Setup static dir for dashboard: %s", build_dir) + except dashboard_utils.FrontendNotFoundError as ex: + # Not to raise FrontendNotFoundError due to NPM incompatibilities + # with Windows. + # Please refer to ci.sh::build_dashboard_front_end() + if sys.platform in ["win32", "cygwin"]: + logger.warning(ex) + else: + raise ex + dashboard_optional_utils.DashboardHeadRouteTable.bind(self) + + # Create a http session for all modules. + # aiohttp<4.0.0 uses a 'loop' variable, aiohttp>=4.0.0 doesn't anymore + if Version(aiohttp.__version__) < Version("4.0.0"): + self.http_session = aiohttp.ClientSession(loop=get_or_create_event_loop()) + else: + self.http_session = aiohttp.ClientSession() + + @routes.get("/") + async def get_index(self, req) -> aiohttp.web.FileResponse: + try: + # This API will be no-op after the first report. + # Note: We always record the usage, but it is not reported + # if the usage stats is disabled. + record_extra_usage_tag(TagKey.DASHBOARD_USED, "True") + except Exception as e: + logger.warning( + "Failed to record the dashboard usage. " + "This error message is harmless and can be ignored. " + f"Error: {e}" + ) + resp = aiohttp.web.FileResponse( + os.path.join( + os.path.dirname(os.path.abspath(__file__)), "client/build/index.html" + ) + ) + resp.headers["Cache-Control"] = "no-store" + return resp + + @routes.get("/favicon.ico") + async def get_favicon(self, req) -> aiohttp.web.FileResponse: + return aiohttp.web.FileResponse( + os.path.join( + os.path.dirname(os.path.abspath(__file__)), "client/build/favicon.ico" + ) + ) + + @routes.get("/timezone") + async def get_timezone(self, req) -> aiohttp.web.Response: + try: + current_timezone = timezone_utils.get_current_timezone_info() + return aiohttp.web.json_response(current_timezone) + + except Exception as e: + logger.error(f"Error getting timezone: {e}") + return aiohttp.web.Response( + status=500, text="Internal Server Error:" + str(e) + ) + + @routes.get("/api/authentication_mode") + async def get_authentication_mode(self, req) -> aiohttp.web.Response: + try: + mode = get_authentication_mode() + if mode == AuthenticationMode.TOKEN: + mode_str = "token" + elif mode == AuthenticationMode.K8S: + mode_str = "k8s" + else: + mode_str = "disabled" + + response = aiohttp.web.json_response({"authentication_mode": mode_str}) + + # If auth is disabled, clear any existing authentication cookie + if mode_str == "disabled": + response.set_cookie( + authentication_constants.AUTHENTICATION_TOKEN_COOKIE_NAME, + "", + max_age=0, + path="/", + ) + + return response + except Exception as e: + logger.error(f"Error getting authentication mode: {e}") + return aiohttp.web.Response( + status=500, text="Internal Server Error: " + str(e) + ) + + @routes.post("/api/authenticate") + async def authenticate(self, req) -> aiohttp.web.Response: + """ + Authenticate a user by validating their token and setting a secure HttpOnly cookie. + + This endpoint accepts a token via the Authorization header, validates it, + and if valid, sets an HttpOnly cookie for subsequent requests from the web dashboard. + """ + try: + # Check if token authentication is enabled + if not auth_utils.is_token_auth_enabled(): + return aiohttp.web.Response( + status=401, + text="Unauthorized: Token authentication is not enabled", + ) + + # Get token from Authorization header + auth_header = req.headers.get( + authentication_constants.AUTHORIZATION_HEADER_NAME, "" + ) + + if not auth_header: + return aiohttp.web.Response( + status=401, + text="Unauthorized: Missing authentication token", + ) + + # Validate the token + if not auth_utils.validate_request_token(auth_header): + return aiohttp.web.Response( + status=403, + text="Forbidden: Invalid authentication token", + ) + + # Token is valid - extract the token value (remove "Bearer " prefix if present) + token = auth_header + if auth_header.lower().startswith( + authentication_constants.AUTHORIZATION_BEARER_PREFIX.lower() + ): + token = auth_header[ + len(authentication_constants.AUTHORIZATION_BEARER_PREFIX) : + ] # Remove "Bearer " prefix + + # Create successful response + response = aiohttp.web.json_response( + {"status": "authenticated", "message": "Token is valid"} + ) + + # Set secure HttpOnly cookie + # Check if the connection is secure (HTTPS) + is_secure = req.scheme == "https" + + response.set_cookie( + authentication_constants.AUTHENTICATION_TOKEN_COOKIE_NAME, + token, + max_age=authentication_constants.AUTHENTICATION_TOKEN_COOKIE_MAX_AGE, # 30 days (matching previous behavior) + path="/", + httponly=True, # Prevents JavaScript access (XSS protection) + samesite="Strict", # Prevents CSRF attacks + secure=is_secure, # Only send over HTTPS if connection is secure + ) + + return response + + except Exception as e: + logger.error(f"Error during authentication: {e}") + return aiohttp.web.Response( + status=500, text="Internal Server Error: " + str(e) + ) + + def get_address(self): + assert self.http_host and self.http_port + return self.http_host, self.http_port + + @aiohttp.web.middleware + async def path_clean_middleware(self, request, handler): + if request.path.startswith("/static") or request.path.startswith("/logs"): + parent = pathlib.PurePosixPath( + "/logs" if request.path.startswith("/logs") else "/static" + ) + + # If the destination is not relative to the expected directory, + # then the user is attempting path traversal, so deny the request. + request_path = pathlib.PurePosixPath(posixpath.realpath(request.path)) + if request_path != parent and parent not in request_path.parents: + logger.info( + f"Rejecting {request_path=} because it is not relative to {parent=}" + ) + raise aiohttp.web.HTTPForbidden() + return await handler(request) + + def get_browsers_no_post_put_middleware(self, whitelisted_paths: Set[str]): + """Create middleware that blocks POST/PUT requests from browsers. + + Args: + whitelisted_paths: Set of paths that are allowed to accept POST/PUT + from browsers (e.g., {"/api/authenticate"}) + + Returns: + An aiohttp middleware function + """ + + @aiohttp.web.middleware + async def browsers_no_post_put_middleware(request, handler): + # Allow whitelisted paths + if request.path in whitelisted_paths: + return await handler(request) + + if ( + # Deny mutating requests from browsers. + # See `is_browser_request` for details of the check. + dashboard_optional_utils.is_browser_request(request) + and request.method in [hdrs.METH_POST, hdrs.METH_PUT] + ): + return aiohttp.web.Response( + status=405, text="Method Not Allowed for browser traffic." + ) + + return await handler(request) + + return browsers_no_post_put_middleware + + @aiohttp.web.middleware + async def metrics_middleware(self, request, handler): + start_time = time.monotonic() + + try: + response = await handler(request) + status_tag = f"{floor(response.status / 100)}xx" + return response + except (Exception, asyncio.CancelledError): + status_tag = "5xx" + raise + finally: + resp_time = time.monotonic() - start_time + try: + self.metrics.metrics_request_duration.labels( + endpoint=handler.__name__, + http_status=status_tag, + Version=ray.__version__, + SessionName=self._session_name, + Component="dashboard", + ).observe(resp_time) + self.metrics.metrics_request_count.labels( + method=request.method, + endpoint=handler.__name__, + http_status=status_tag, + Version=ray.__version__, + SessionName=self._session_name, + Component="dashboard", + ).inc() + except Exception as e: + logger.exception(f"Error emitting api metrics: {e}") + + @aiohttp.web.middleware + async def cache_control_static_middleware(self, request, handler): + if request.path.startswith("/static"): + response = await handler(request) + response.headers["Cache-Control"] = "max-age=31536000" + return response + return await handler(request) + + async def run( + self, + dashboard_head_modules: List[DashboardHeadModule], + subprocess_module_handles: List[SubprocessModuleHandle], + ): + # Bind http routes of each module. + for m in dashboard_head_modules: + dashboard_optional_utils.DashboardHeadRouteTable.bind(m) + + for h in subprocess_module_handles: + SubprocessRouteTable.bind(h) + + # Public endpoints that don't require authentication. + # These are needed for the dashboard to load and request an auth token. + public_exact_paths = { + "/", # Root index.html + "/favicon.ico", + "/api/authentication_mode", + "/api/authenticate", # Token authentication endpoint + "/api/healthz", # General healthcheck + "/api/gcs_healthz", # GCS health check + "/api/local_raylet_healthz", # Raylet health check + "/-/healthz", # Serve health check + } + public_path_prefixes = ("/static/",) # Static assets (JS, CSS, images) + + # Paths that are allowed to accept POST/PUT requests from browsers + browser_post_put_allowed_paths = { + "/api/authenticate", # Token authentication endpoint + } + + # Http server should be initialized after all modules loaded. + # working_dir uploads for job submission can be up to 100MiB. + + app = aiohttp.web.Application( + client_max_size=ray_constants.DASHBOARD_CLIENT_MAX_SIZE, + middlewares=[ + self.metrics_middleware, + get_token_auth_middleware( + aiohttp, public_exact_paths, public_path_prefixes + ), + self.path_clean_middleware, + self.get_browsers_no_post_put_middleware( + browser_post_put_allowed_paths + ), + self.cache_control_static_middleware, + ], + ) + app.add_routes(routes=routes.bound_routes()) + app.add_routes(routes=SubprocessRouteTable.bound_routes()) + + self.runner = aiohttp.web.AppRunner( + app, + access_log_format=( + "%a %t '%r' %s %b bytes %D us '%{Referer}i' '%{User-Agent}i'" + ), + ) + await self.runner.setup() + last_ex = None + for i in range(1 + self.http_port_retries): + try: + site = aiohttp.web.TCPSite(self.runner, self.http_host, self.http_port) + await site.start() + break + except OSError as e: + last_ex = e + self.http_port += 1 + logger.warning("Try to use port %s: %s", self.http_port, e) + else: + raise Exception( + f"Failed to find a valid port for dashboard after " + f"{self.http_port_retries} retries: {last_ex}" + ) + self.http_host, self.http_port, *_ = site._server.sockets[0].getsockname() + self.http_host = ( + self.ip + if ipaddress.ip_address(self.http_host).is_unspecified + else self.http_host + ) + logger.info( + "Dashboard head http address: %s", + build_address(self.http_host, self.http_port), + ) + # Dump registered http routes. + dump_routes = [r for r in app.router.routes() if r.method != hdrs.METH_HEAD] + for r in dump_routes: + logger.info(r) + logger.info("Registered %s routes.", len(dump_routes)) + + async def cleanup(self): + # Wait for finish signal. + await self.runner.cleanup() diff --git a/lib/python3.12/site-packages/ray/dashboard/k8s_utils.py b/lib/python3.12/site-packages/ray/dashboard/k8s_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f9c5da030f4417ffde81b176a2b73a9296a6d442 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/k8s_utils.py @@ -0,0 +1,111 @@ +import logging + +from ray._private.utils import get_num_cpus + +logger = logging.getLogger(__name__) + +CPU_USAGE_PATH = "/sys/fs/cgroup/cpuacct/cpuacct.usage" +CPU_USAGE_PATH_V2 = "/sys/fs/cgroup/cpu.stat" +PROC_STAT_PATH = "/proc/stat" + +container_num_cpus = None +host_num_cpus = None + +last_cpu_usage = None +last_system_usage = None + + +def cpu_percent(): + """Estimate CPU usage percent for Ray pod managed by Kubernetes + Operator. + + Computed by the following steps + (1) Replicate the logic used by 'docker stats' cli command. + See https://github.com/docker/cli/blob/c0a6b1c7b30203fbc28cd619acb901a95a80e30e/cli/command/container/stats_helpers.go#L166. + (2) Divide by the number of CPUs available to the container, so that + e.g. full capacity use of 2 CPUs will read as 100%, + rather than 200%. + + Step (1) above works by + dividing delta in cpu usage by + delta in total host cpu usage, averaged over host's cpus. + + Since deltas are not initially available, return 0.0 on first call. + """ # noqa + global last_system_usage + global last_cpu_usage + try: + cpu_usage = _cpu_usage() + system_usage = _system_usage() + # Return 0.0 on first call. + if last_system_usage is None: + cpu_percent = 0.0 + else: + cpu_delta = cpu_usage - last_cpu_usage + # "System time passed." (Typically close to clock time.) + system_delta = (system_usage - last_system_usage) / _host_num_cpus() + + quotient = cpu_delta / system_delta + cpu_percent = round(quotient * 100 / get_num_cpus(), 1) + last_system_usage = system_usage + last_cpu_usage = cpu_usage + # Computed percentage might be slightly above 100%. + return min(cpu_percent, 100.0) + except Exception: + logger.exception("Error computing CPU usage of Ray Kubernetes pod.") + return 0.0 + + +def _cpu_usage(): + """Compute total cpu usage of the container in nanoseconds + by reading from cpuacct in cgroups v1 or cpu.stat in cgroups v2.""" + try: + # cgroups v1 + return int(open(CPU_USAGE_PATH).read()) + except FileNotFoundError: + # cgroups v2 + cpu_stat_text = open(CPU_USAGE_PATH_V2).read() + # e.g. "usage_usec 16089294616" + cpu_stat_first_line = cpu_stat_text.split("\n")[0] + # get the second word of the first line, cast as an integer + # this is the CPU usage is microseconds + cpu_usec = int(cpu_stat_first_line.split()[1]) + # Convert to nanoseconds and return. + return cpu_usec * 1000 + + +def _system_usage(): + """ + Computes total CPU usage of the host in nanoseconds. + + Logic taken from here: + https://github.com/moby/moby/blob/b42ac8d370a8ef8ec720dff0ca9dfb3530ac0a6a/daemon/stats/collector_unix.go#L31 + + See also the /proc/stat entry here: + https://man7.org/linux/man-pages/man5/proc.5.html + """ # noqa + cpu_summary_str = open(PROC_STAT_PATH).read().split("\n")[0] + parts = cpu_summary_str.split() + assert parts[0] == "cpu" + usage_data = parts[1:8] + total_clock_ticks = sum(int(entry) for entry in usage_data) + # 100 clock ticks per second, 10^9 ns per second + usage_ns = total_clock_ticks * 10**7 + return usage_ns + + +def _host_num_cpus(): + """Number of physical CPUs, obtained by parsing /proc/stat.""" + global host_num_cpus + if host_num_cpus is None: + proc_stat_lines = open(PROC_STAT_PATH).read().split("\n") + split_proc_stat_lines = [line.split() for line in proc_stat_lines] + cpu_lines = [ + split_line + for split_line in split_proc_stat_lines + if len(split_line) > 0 and "cpu" in split_line[0] + ] + # Number of lines starting with a word including 'cpu', subtracting + # 1 for the first summary line. + host_num_cpus = len(cpu_lines) - 1 + return host_num_cpus diff --git a/lib/python3.12/site-packages/ray/dashboard/memory_utils.py b/lib/python3.12/site-packages/ray/dashboard/memory_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e81532f5af9eb25a64863afe7a74a701671f8113 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/memory_utils.py @@ -0,0 +1,523 @@ +import base64 +import logging +from collections import defaultdict +from enum import Enum +from typing import List + +import ray +from ray._private.internal_api import node_stats +from ray._raylet import ActorID, JobID, TaskID +from ray.dashboard.utils import node_stats_to_dict + +logger = logging.getLogger(__name__) + +# These values are used to calculate if objectRefs are actor handles. +TASKID_BYTES_SIZE = TaskID.size() +ACTORID_BYTES_SIZE = ActorID.size() +JOBID_BYTES_SIZE = JobID.size() + + +def decode_object_ref_if_needed(object_ref: str) -> bytes: + """Decode objectRef bytes string. + + gRPC reply contains an objectRef that is encodded by Base64. + This function is used to decode the objectRef. + Note that there are times that objectRef is already decoded as + a hex string. In this case, just convert it to a binary number. + """ + if object_ref.endswith("="): + # If the object ref ends with =, that means it is base64 encoded. + # Object refs will always have = as a padding + # when it is base64 encoded because objectRef is always 20B. + return base64.standard_b64decode(object_ref) + else: + return ray._common.utils.hex_to_binary(object_ref) + + +class SortingType(Enum): + PID = 1 + OBJECT_SIZE = 3 + REFERENCE_TYPE = 4 + + +class GroupByType(Enum): + NODE_ADDRESS = "node" + STACK_TRACE = "stack_trace" + + +class ReferenceType(Enum): + # We don't use enum because enum is not json serializable. + ACTOR_HANDLE = "ACTOR_HANDLE" + PINNED_IN_MEMORY = "PINNED_IN_MEMORY" + LOCAL_REFERENCE = "LOCAL_REFERENCE" + USED_BY_PENDING_TASK = "USED_BY_PENDING_TASK" + CAPTURED_IN_OBJECT = "CAPTURED_IN_OBJECT" + UNKNOWN_STATUS = "UNKNOWN_STATUS" + + +def get_sorting_type(sort_by: str): + """Translate string input into SortingType instance""" + sort_by = sort_by.upper() + if sort_by == "PID": + return SortingType.PID + elif sort_by == "OBJECT_SIZE": + return SortingType.OBJECT_SIZE + elif sort_by == "REFERENCE_TYPE": + return SortingType.REFERENCE_TYPE + else: + raise Exception( + "The sort-by input provided is not one of\ + PID, OBJECT_SIZE, or REFERENCE_TYPE." + ) + + +def get_group_by_type(group_by: str): + """Translate string input into GroupByType instance""" + group_by = group_by.upper() + if group_by == "NODE_ADDRESS": + return GroupByType.NODE_ADDRESS + elif group_by == "STACK_TRACE": + return GroupByType.STACK_TRACE + else: + raise Exception( + "The group-by input provided is not one of\ + NODE_ADDRESS or STACK_TRACE." + ) + + +class MemoryTableEntry: + def __init__( + self, *, object_ref: dict, node_address: str, is_driver: bool, pid: int + ): + # worker info + self.is_driver = is_driver + self.pid = pid + self.node_address = node_address + + # object info + self.task_status = object_ref.get("taskStatus", "?") + if self.task_status == "NIL": + self.task_status = "-" + self.attempt_number = int(object_ref.get("attemptNumber", 0)) + 1 + self.object_size = int(object_ref.get("objectSize", -1)) + self.call_site = object_ref.get("callSite", "") + if len(self.call_site) == 0: + self.call_site = "disabled" + self.object_ref = ray.ObjectRef( + decode_object_ref_if_needed(object_ref["objectId"]) + ) + + # reference info + self.local_ref_count = int(object_ref.get("localRefCount", 0)) + self.pinned_in_memory = bool(object_ref.get("pinnedInMemory", False)) + self.submitted_task_ref_count = int(object_ref.get("submittedTaskRefCount", 0)) + self.contained_in_owned = [ + ray.ObjectRef(decode_object_ref_if_needed(object_ref)) + for object_ref in object_ref.get("containedInOwned", []) + ] + self.reference_type = self._get_reference_type() + + def is_valid(self) -> bool: + # If the entry doesn't have a reference type or some invalid state, + # (e.g., no object ref presented), it is considered invalid. + if ( + not self.pinned_in_memory + and self.local_ref_count == 0 + and self.submitted_task_ref_count == 0 + and len(self.contained_in_owned) == 0 + ): + return False + elif self.object_ref.is_nil(): + return False + else: + return True + + def group_key(self, group_by_type: GroupByType) -> str: + if group_by_type == GroupByType.NODE_ADDRESS: + return self.node_address + elif group_by_type == GroupByType.STACK_TRACE: + return self.call_site + else: + raise ValueError(f"group by type {group_by_type} is invalid.") + + def _get_reference_type(self) -> str: + if self._is_object_ref_actor_handle(): + return ReferenceType.ACTOR_HANDLE.value + if self.pinned_in_memory: + return ReferenceType.PINNED_IN_MEMORY.value + elif self.submitted_task_ref_count > 0: + return ReferenceType.USED_BY_PENDING_TASK.value + elif self.local_ref_count > 0: + return ReferenceType.LOCAL_REFERENCE.value + elif len(self.contained_in_owned) > 0: + return ReferenceType.CAPTURED_IN_OBJECT.value + else: + return ReferenceType.UNKNOWN_STATUS.value + + def _is_object_ref_actor_handle(self) -> bool: + object_ref_hex = self.object_ref.hex() + + # We need to multiply 2 because we need bits size instead of bytes size. + taskid_random_bits_size = (TASKID_BYTES_SIZE - ACTORID_BYTES_SIZE) * 2 + actorid_random_bits_size = (ACTORID_BYTES_SIZE - JOBID_BYTES_SIZE) * 2 + + # random (8B) | ActorID(6B) | flag (2B) | index (6B) + # ActorID(6B) == ActorRandomByte(4B) + JobID(2B) + # If random bytes are all 'f', but ActorRandomBytes + # are not all 'f', that means it is an actor creation + # task, which is an actor handle. + random_bits = object_ref_hex[:taskid_random_bits_size] + actor_random_bits = object_ref_hex[ + taskid_random_bits_size : taskid_random_bits_size + actorid_random_bits_size + ] + if random_bits == "f" * 16 and not actor_random_bits == "f" * 24: + return True + else: + return False + + def as_dict(self): + return { + "object_ref": self.object_ref.hex(), + "pid": self.pid, + "node_ip_address": self.node_address, + "object_size": self.object_size, + "reference_type": self.reference_type, + "call_site": self.call_site, + "task_status": self.task_status, + "attempt_number": self.attempt_number, + "local_ref_count": self.local_ref_count, + "pinned_in_memory": self.pinned_in_memory, + "submitted_task_ref_count": self.submitted_task_ref_count, + "contained_in_owned": [ + object_ref.hex() for object_ref in self.contained_in_owned + ], + "type": "Driver" if self.is_driver else "Worker", + } + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return str(self.as_dict()) + + +class MemoryTable: + def __init__( + self, + entries: List[MemoryTableEntry], + group_by_type: GroupByType = GroupByType.NODE_ADDRESS, + sort_by_type: SortingType = SortingType.PID, + ): + self.table = entries + # Group is a list of memory tables grouped by a group key. + self.group = {} + self.summary = defaultdict(int) + # NOTE YOU MUST SORT TABLE BEFORE GROUPING. + # self._group_by(..)._sort_by(..) != self._sort_by(..)._group_by(..) + if group_by_type and sort_by_type: + self.setup(group_by_type, sort_by_type) + elif group_by_type: + self._group_by(group_by_type) + elif sort_by_type: + self._sort_by(sort_by_type) + + def setup(self, group_by_type: GroupByType, sort_by_type: SortingType): + """Setup memory table. + + This will sort entries first and group them after. + Sort order will be still kept. + """ + self._sort_by(sort_by_type)._group_by(group_by_type) + for group_memory_table in self.group.values(): + group_memory_table.summarize() + self.summarize() + return self + + def insert_entry(self, entry: MemoryTableEntry): + self.table.append(entry) + + def summarize(self): + # Reset summary. + total_object_size = 0 + total_local_ref_count = 0 + total_pinned_in_memory = 0 + total_used_by_pending_task = 0 + total_captured_in_objects = 0 + total_actor_handles = 0 + + for entry in self.table: + if entry.object_size > 0: + total_object_size += entry.object_size + if entry.reference_type == ReferenceType.LOCAL_REFERENCE.value: + total_local_ref_count += 1 + elif entry.reference_type == ReferenceType.PINNED_IN_MEMORY.value: + total_pinned_in_memory += 1 + elif entry.reference_type == ReferenceType.USED_BY_PENDING_TASK.value: + total_used_by_pending_task += 1 + elif entry.reference_type == ReferenceType.CAPTURED_IN_OBJECT.value: + total_captured_in_objects += 1 + elif entry.reference_type == ReferenceType.ACTOR_HANDLE.value: + total_actor_handles += 1 + + self.summary = { + "total_object_size": total_object_size, + "total_local_ref_count": total_local_ref_count, + "total_pinned_in_memory": total_pinned_in_memory, + "total_used_by_pending_task": total_used_by_pending_task, + "total_captured_in_objects": total_captured_in_objects, + "total_actor_handles": total_actor_handles, + } + return self + + def _sort_by(self, sorting_type: SortingType): + if sorting_type == SortingType.PID: + self.table.sort(key=lambda entry: entry.pid) + elif sorting_type == SortingType.OBJECT_SIZE: + self.table.sort(key=lambda entry: entry.object_size) + elif sorting_type == SortingType.REFERENCE_TYPE: + self.table.sort(key=lambda entry: entry.reference_type) + else: + raise ValueError(f"Give sorting type: {sorting_type} is invalid.") + return self + + def _group_by(self, group_by_type: GroupByType): + """Group entries and summarize the result. + + NOTE: Each group is another MemoryTable. + """ + # Reset group + self.group = {} + + # Build entries per group. + group = defaultdict(list) + for entry in self.table: + group[entry.group_key(group_by_type)].append(entry) + + # Build a group table. + for group_key, entries in group.items(): + self.group[group_key] = MemoryTable( + entries, group_by_type=None, sort_by_type=None + ) + for group_key, group_memory_table in self.group.items(): + group_memory_table.summarize() + return self + + def as_dict(self): + return { + "summary": self.summary, + "group": { + group_key: { + "entries": group_memory_table.get_entries(), + "summary": group_memory_table.summary, + } + for group_key, group_memory_table in self.group.items() + }, + } + + def get_entries(self) -> List[dict]: + return [entry.as_dict() for entry in self.table] + + def __repr__(self): + return str(self.as_dict()) + + def __str__(self): + return self.__repr__() + + +def construct_memory_table( + workers_stats: List, + group_by: GroupByType = GroupByType.NODE_ADDRESS, + sort_by=SortingType.OBJECT_SIZE, +) -> MemoryTable: + memory_table_entries = [] + for core_worker_stats in workers_stats: + pid = core_worker_stats["pid"] + is_driver = core_worker_stats.get("workerType") == "DRIVER" + node_address = core_worker_stats["ipAddress"] + object_refs = core_worker_stats.get("objectRefs", []) + + for object_ref in object_refs: + memory_table_entry = MemoryTableEntry( + object_ref=object_ref, + node_address=node_address, + is_driver=is_driver, + pid=pid, + ) + if memory_table_entry.is_valid(): + memory_table_entries.append(memory_table_entry) + memory_table = MemoryTable( + memory_table_entries, group_by_type=group_by, sort_by_type=sort_by + ) + return memory_table + + +def track_reference_size(group): + """Returns dictionary mapping reference type + to memory usage for a given memory table group.""" + d = defaultdict(int) + table_name = { + "LOCAL_REFERENCE": "total_local_ref_count", + "PINNED_IN_MEMORY": "total_pinned_in_memory", + "USED_BY_PENDING_TASK": "total_used_by_pending_task", + "CAPTURED_IN_OBJECT": "total_captured_in_objects", + "ACTOR_HANDLE": "total_actor_handles", + } + for entry in group["entries"]: + size = entry["object_size"] + if size == -1: + # size not recorded + size = 0 + d[table_name[entry["reference_type"]]] += size + return d + + +def memory_summary( + state, + group_by="NODE_ADDRESS", + sort_by="OBJECT_SIZE", + line_wrap=True, + unit="B", + num_entries=None, +) -> str: + # Get terminal size + import shutil + + size = shutil.get_terminal_size((80, 20)).columns + line_wrap_threshold = 137 + + # Unit conversions + units = {"B": 10**0, "KB": 10**3, "MB": 10**6, "GB": 10**9} + + # Fetch core memory worker stats, store as a dictionary + core_worker_stats = [] + for raylet in state.node_table(): + if not raylet["Alive"]: + continue + try: + stats = node_stats_to_dict( + node_stats(raylet["NodeManagerAddress"], raylet["NodeManagerPort"]) + ) + except RuntimeError: + continue + core_worker_stats.extend(stats["coreWorkersStats"]) + assert type(stats) is dict and "coreWorkersStats" in stats + + # Build memory table with "group_by" and "sort_by" parameters + group_by, sort_by = get_group_by_type(group_by), get_sorting_type(sort_by) + memory_table = construct_memory_table( + core_worker_stats, group_by, sort_by + ).as_dict() + assert "summary" in memory_table and "group" in memory_table + + # Build memory summary + mem = "" + group_by, sort_by = group_by.name.lower().replace( + "_", " " + ), sort_by.name.lower().replace("_", " ") + summary_labels = [ + "Mem Used by Objects", + "Local References", + "Pinned", + "Used by task", + "Captured in Objects", + "Actor Handles", + ] + summary_string = "{:<19} {:<16} {:<12} {:<13} {:<19} {:<13}\n" + + object_ref_labels = [ + "IP Address", + "PID", + "Type", + "Call Site", + "Status", + "Attempt", + "Size", + "Reference Type", + "Object Ref", + ] + object_ref_string = "{:<13} | {:<8} | {:<7} | {:<9} \ +| {:<9} | {:<8} | {:<8} | {:<14} | {:<10}\n" + + if size > line_wrap_threshold and line_wrap: + object_ref_string = "{:<15} {:<5} {:<6} {:<22} {:<14} {:<8} {:<6} \ +{:<18} {:<56}\n" + + mem += f"Grouping by {group_by}...\ + Sorting by {sort_by}...\ + Display {num_entries if num_entries is not None else 'all'} \ +entries per group...\n\n\n" + + for key, group in memory_table["group"].items(): + # Group summary + summary = group["summary"] + ref_size = track_reference_size(group) + for k, v in summary.items(): + if k == "total_object_size": + summary[k] = str(v / units[unit]) + f" {unit}" + else: + summary[k] = str(v) + f", ({ref_size[k] / units[unit]} {unit})" + mem += f"--- Summary for {group_by}: {key} ---\n" + mem += summary_string.format(*summary_labels) + mem += summary_string.format(*summary.values()) + "\n" + + # Memory table per group + mem += f"--- Object references for {group_by}: {key} ---\n" + mem += object_ref_string.format(*object_ref_labels) + n = 1 # Counter for num entries per group + for entry in group["entries"]: + if num_entries is not None and n > num_entries: + break + entry["object_size"] = ( + str(entry["object_size"] / units[unit]) + f" {unit}" + if entry["object_size"] > -1 + else "?" + ) + num_lines = 1 + if size > line_wrap_threshold and line_wrap: + call_site_length = 22 + if len(entry["call_site"]) == 0: + entry["call_site"] = ["disabled"] + else: + entry["call_site"] = [ + entry["call_site"][i : i + call_site_length] + for i in range(0, len(entry["call_site"]), call_site_length) + ] + + task_status_length = 12 + entry["task_status"] = [ + entry["task_status"][i : i + task_status_length] + for i in range(0, len(entry["task_status"]), task_status_length) + ] + num_lines = max(len(entry["call_site"]), len(entry["task_status"])) + + else: + mem += "\n" + object_ref_values = [ + entry["node_ip_address"], + entry["pid"], + entry["type"], + entry["call_site"], + entry["task_status"], + entry["attempt_number"], + entry["object_size"], + entry["reference_type"], + entry["object_ref"], + ] + for i in range(len(object_ref_values)): + if not isinstance(object_ref_values[i], list): + object_ref_values[i] = [object_ref_values[i]] + object_ref_values[i].extend( + ["" for x in range(num_lines - len(object_ref_values[i]))] + ) + for i in range(num_lines): + row = [elem[i] for elem in object_ref_values] + mem += object_ref_string.format(*row) + mem += "\n" + n += 1 + + mem += ( + "To record callsite information for each ObjectRef created, set " + "env variable RAY_record_ref_creation_sites=1\n\n" + ) + + return mem diff --git a/lib/python3.12/site-packages/ray/dashboard/optional_deps.py b/lib/python3.12/site-packages/ray/dashboard/optional_deps.py new file mode 100644 index 0000000000000000000000000000000000000000..c8f60a7bbeda8a5be9d543a941b9f70b4bfe6264 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/optional_deps.py @@ -0,0 +1,27 @@ +# These imports determine whether or not a user has the required dependencies +# to launch the optional dashboard API server. +# If any of these imports fail, the dashboard API server will not be launched. +# Please add important dashboard-api dependencies to this list. + +import aiohttp # noqa: F401 +import aiohttp.web # noqa: F401 +import aiohttp_cors # noqa: F401 +import grpc # noqa: F401 + +# These checks have to come first because aiohttp looks +# for opencensus, too, and raises a different error otherwise. +import opencensus # noqa: F401 +import opentelemetry # noqa: F401 +import opentelemetry.exporter.prometheus # noqa: F401 +import opentelemetry.proto # noqa: F401 +import prometheus_client # noqa: F401 +import pydantic # noqa: F401 +from aiohttp import hdrs # noqa: F401 +from aiohttp.typedefs import PathLike # noqa: F401 +from aiohttp.web import ( + Request, # noqa: F401 + RouteDef, # noqa: F401 +) + +# Adding new modules should also be reflected in the +# python/ray/tests/test_minimal_install.py diff --git a/lib/python3.12/site-packages/ray/dashboard/optional_utils.py b/lib/python3.12/site-packages/ray/dashboard/optional_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a15d63573610754744988a1fb0d1edceebfbb843 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/optional_utils.py @@ -0,0 +1,220 @@ +""" +Optional utils module contains utility methods +that require optional dependencies. +""" +import asyncio +import collections +import functools +import inspect +import logging +import os +import time +import traceback +from collections import namedtuple +from typing import Callable, Union + +from aiohttp.web import Request, Response + +import ray +import ray.dashboard.consts as dashboard_consts +import ray.dashboard.utils as dashboard_utils +from ray._private.ray_constants import env_bool +from ray._raylet import RAY_INTERNAL_DASHBOARD_NAMESPACE + +# All third-party dependencies that are not included in the minimal Ray +# installation must be included in this file. This allows us to determine if +# the agent has the necessary dependencies to be started. +from ray.dashboard.optional_deps import aiohttp, hdrs +from ray.dashboard.routes import method_route_table_factory, rest_response +from ray.dashboard.utils import ( + DashboardAgentModule, + DashboardHeadModule, +) + +try: + create_task = asyncio.create_task +except AttributeError: + create_task = asyncio.ensure_future + + +logger = logging.getLogger(__name__) + +DashboardHeadRouteTable = method_route_table_factory() +DashboardAgentRouteTable = method_route_table_factory() + + +# The cache value type used by aiohttp_cache. +_AiohttpCacheValue = namedtuple("AiohttpCacheValue", ["data", "expiration", "task"]) +# The methods with no request body used by aiohttp_cache. +_AIOHTTP_CACHE_NOBODY_METHODS = {hdrs.METH_GET, hdrs.METH_DELETE} + + +def aiohttp_cache( + ttl_seconds=dashboard_consts.AIOHTTP_CACHE_TTL_SECONDS, + maxsize=dashboard_consts.AIOHTTP_CACHE_MAX_SIZE, + enable=not env_bool(dashboard_consts.AIOHTTP_CACHE_DISABLE_ENVIRONMENT_KEY, False), +): + assert maxsize > 0 + cache = collections.OrderedDict() + + def _wrapper(handler): + if enable: + + @functools.wraps(handler) + async def _cache_handler(*args) -> aiohttp.web.Response: + # Make the route handler as a bound method. + # The args may be: + # * (Request, ) + # * (self, Request) + req = args[-1] + # If nocache=1 in query string, bypass cache. + if req.query.get("nocache") == "1": + return await handler(*args) + + # Make key. + if req.method in _AIOHTTP_CACHE_NOBODY_METHODS: + key = req.path_qs + else: + key = (req.path_qs, await req.read()) + # Query cache. + value = cache.get(key) + if value is not None: + cache.move_to_end(key) + if not value.task.done() or value.expiration >= time.time(): + # Update task not done or the data is not expired. + return aiohttp.web.Response(**value.data) + + def _update_cache(task): + try: + response = task.result() + except Exception: + response = rest_response( + status_code=dashboard_utils.HTTPStatusCode.INTERNAL_ERROR, + message=traceback.format_exc(), + ) + data = { + "status": response.status, + "headers": dict(response.headers), + "body": response.body, + } + cache[key] = _AiohttpCacheValue( + data, time.time() + ttl_seconds, task + ) + cache.move_to_end(key) + if len(cache) > maxsize: + cache.popitem(last=False) + return response + + task = create_task(handler(*args)) + task.add_done_callback(_update_cache) + if value is None: + return await task + else: + return aiohttp.web.Response(**value.data) + + suffix = f"[cache ttl={ttl_seconds}, max_size={maxsize}]" + _cache_handler.__name__ += suffix + _cache_handler.__qualname__ += suffix + return _cache_handler + else: + return handler + + if inspect.iscoroutinefunction(ttl_seconds): + target_func = ttl_seconds + ttl_seconds = dashboard_consts.AIOHTTP_CACHE_TTL_SECONDS + return _wrapper(target_func) + else: + return _wrapper + + +def is_browser_request(req: Request) -> bool: + """Best-effort detection if the request was made by a browser. + + Uses three heuristics: + 1) If the `User-Agent` header starts with 'Mozilla'. This heuristic is weak, + but hard for a browser to bypass e.g., fetch/xhr and friends cannot alter the + user agent, but requests made with an HTTP library can stumble into this if + they choose to user a browser-like user agent. At the time of writing, all + common browsers' user agents start with 'Mozilla'. + 2) If any of the `Sec-Fetch-*` headers are present. + 3) If any of the various CORS headers are present + """ + return req.headers.get("User-Agent", "").startswith("Mozilla") or any( + h in req.headers + for h in ( + # Origin and Referer are sent by browser user agents to give + # information about the requesting origin + "Referer", + "Origin", + # Sec-Fetch headers are sent with many but not all `fetch` + # requests, and will eventually be sent on all requests. + "Sec-Fetch-Mode", + "Sec-Fetch-Dest", + "Sec-Fetch-Site", + "Sec-Fetch-User", + # CORS headers specifying which other headers are modified + "Access-Control-Request-Method", + "Access-Control-Request-Headers", + ) + ) + + +def deny_browser_requests() -> Callable: + """Reject any requests that appear to be made by a browser.""" + + def decorator_factory(f: Callable) -> Callable: + @functools.wraps(f) + async def decorator(self, req: Request): + if is_browser_request(req): + return Response( + text="Browser requests not allowed", + status=aiohttp.web.HTTPMethodNotAllowed.status_code, + ) + + return await f(self, req) + + return decorator + + return decorator_factory + + +def init_ray_and_catch_exceptions() -> Callable: + """Decorator to be used on methods that require being connected to Ray.""" + + def decorator_factory(f: Callable) -> Callable: + @functools.wraps(f) + async def decorator( + self: Union[DashboardAgentModule, DashboardHeadModule], *args, **kwargs + ): + try: + if not ray.is_initialized(): + try: + address = self.gcs_address + logger.info(f"Connecting to ray with address={address}") + # Set the gcs rpc timeout to shorter + os.environ["RAY_gcs_server_request_timeout_seconds"] = str( + dashboard_consts.GCS_RPC_TIMEOUT_SECONDS + ) + # Init ray without logging to driver + # to avoid infinite logging issue. + ray.init( + address=address, + log_to_driver=False, + configure_logging=False, + namespace=RAY_INTERNAL_DASHBOARD_NAMESPACE, + _skip_env_hook=True, + ) + except Exception as e: + ray.shutdown() + raise e from None + return await f(self, *args, **kwargs) + except Exception as e: + logger.exception(f"Unexpected error in handler: {e}") + return Response( + text=traceback.format_exc(), + status=aiohttp.web.HTTPInternalServerError.status_code, + ) + + return decorator + + return decorator_factory diff --git a/lib/python3.12/site-packages/ray/dashboard/routes.py b/lib/python3.12/site-packages/ray/dashboard/routes.py new file mode 100644 index 0000000000000000000000000000000000000000..cce72f2dea11713cfbc8df9b12988d620f42756d --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/routes.py @@ -0,0 +1,212 @@ +import abc +import collections +import functools +import inspect +import json +import logging +import os +import traceback +from typing import Any + +from ray.dashboard.optional_deps import PathLike, RouteDef, aiohttp, hdrs +from ray.dashboard.utils import CustomEncoder, HTTPStatusCode, to_google_style + +logger = logging.getLogger(__name__) + + +class BaseRouteTable(abc.ABC): + """A base class to bind http route to a target instance. Subclass should implement + the _register_route method. It should define how the handler interacts with + _BindInfo.instance. + + Subclasses must declare their own _bind_map and _routes properties to avoid + conflicts. + """ + + class _BindInfo: + def __init__(self, filename, lineno, instance): + self.filename = filename + self.lineno = lineno + self.instance = instance + + @classmethod + @property + @abc.abstractmethod + def _bind_map(cls): + pass + + @classmethod + @property + @abc.abstractmethod + def _routes(cls): + pass + + @classmethod + @abc.abstractmethod + def _register_route(cls, method, path, **kwargs): + pass + + @classmethod + @abc.abstractmethod + def bind(cls, instance): + pass + + @classmethod + def routes(cls): + return cls._routes + + @classmethod + def bound_routes(cls): + bound_items = [] + for r in cls._routes._items: + if isinstance(r, RouteDef): + route_method = r.handler.__route_method__ + route_path = r.handler.__route_path__ + instance = cls._bind_map[route_method][route_path].instance + if instance is not None: + bound_items.append(r) + else: + bound_items.append(r) + routes = aiohttp.web.RouteTableDef() + routes._items = bound_items + return routes + + @classmethod + def head(cls, path, **kwargs): + return cls._register_route(hdrs.METH_HEAD, path, **kwargs) + + @classmethod + def get(cls, path, **kwargs): + return cls._register_route(hdrs.METH_GET, path, **kwargs) + + @classmethod + def post(cls, path, **kwargs): + return cls._register_route(hdrs.METH_POST, path, **kwargs) + + @classmethod + def put(cls, path, **kwargs): + return cls._register_route(hdrs.METH_PUT, path, **kwargs) + + @classmethod + def patch(cls, path, **kwargs): + return cls._register_route(hdrs.METH_PATCH, path, **kwargs) + + @classmethod + def delete(cls, path, **kwargs): + return cls._register_route(hdrs.METH_DELETE, path, **kwargs) + + @classmethod + def view(cls, path, **kwargs): + return cls._register_route(hdrs.METH_ANY, path, **kwargs) + + @classmethod + def static(cls, prefix: str, path: PathLike, **kwargs: Any) -> None: + cls._routes.static(prefix, path, **kwargs) + + +def method_route_table_factory(): + """ + Return a method-based route table class, for in-process HeadModule objects. + """ + + class MethodRouteTable(BaseRouteTable): + """A helper class to bind http route to class method. Each _BindInfo.instance + is a class instance, and for an inbound request, we invoke the async handler + method.""" + + _bind_map = collections.defaultdict(dict) + _routes = aiohttp.web.RouteTableDef() + + @classmethod + def _register_route(cls, method, path, **kwargs): + def _wrapper(handler): + if path in cls._bind_map[method]: + bind_info = cls._bind_map[method][path] + raise Exception( + f"Duplicated route path: {path}, " + f"previous one registered at " + f"{bind_info.filename}:{bind_info.lineno}" + ) + + bind_info = cls._BindInfo( + handler.__code__.co_filename, handler.__code__.co_firstlineno, None + ) + + @functools.wraps(handler) + async def _handler_route(*args) -> aiohttp.web.Response: + try: + # Make the route handler as a bound method. + # The args may be: + # * (Request, ) + # * (self, Request) + req = args[-1] + return await handler(bind_info.instance, req) + except Exception: + logger.exception("Handle %s %s failed.", method, path) + return rest_response( + status_code=HTTPStatusCode.INTERNAL_ERROR, + message=traceback.format_exc(), + ) + + cls._bind_map[method][path] = bind_info + _handler_route.__route_method__ = method + _handler_route.__route_path__ = path + return cls._routes.route(method, path, **kwargs)(_handler_route) + + return _wrapper + + @classmethod + def bind(cls, instance): + def predicate(o): + if inspect.ismethod(o): + return hasattr(o, "__route_method__") and hasattr( + o, "__route_path__" + ) + return False + + handler_routes = inspect.getmembers(instance, predicate) + for _, h in handler_routes: + cls._bind_map[h.__func__.__route_method__][ + h.__func__.__route_path__ + ].instance = instance + + return MethodRouteTable + + +def rest_response( + status_code: HTTPStatusCode, + message: str, + convert_google_style: bool = True, + **kwargs, +) -> aiohttp.web.Response: + """ + Args: + status_code: HTTPStatusCode + The HTTP status code of the response. + message: str + The message of the response. + convert_google_style: bool + Whether to convert the response to google style. + + Returns: + aiohttp.web.Response + """ + # In the dev context we allow a dev server running on a + # different port to consume the API, meaning we need to allow + # cross-origin access + if os.environ.get("RAY_DASHBOARD_DEV") == "1": + headers = {"Access-Control-Allow-Origin": "*"} + else: + headers = {} + + success = status_code == HTTPStatusCode.OK + return aiohttp.web.json_response( + { + "result": success, + "msg": message, + "data": to_google_style(kwargs) if convert_google_style else kwargs, + }, + dumps=functools.partial(json.dumps, cls=CustomEncoder), + headers=headers, + status=status_code, + ) diff --git a/lib/python3.12/site-packages/ray/dashboard/state_aggregator.py b/lib/python3.12/site-packages/ray/dashboard/state_aggregator.py new file mode 100644 index 0000000000000000000000000000000000000000..a33dd4e7b3be39410c39e0491fb7d9b5ac7f7f52 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/state_aggregator.py @@ -0,0 +1,671 @@ +import asyncio +import logging +from concurrent.futures import ThreadPoolExecutor +from itertools import islice +from typing import List, Optional + +import ray.dashboard.memory_utils as memory_utils +from ray import NodeID +from ray._common.utils import get_or_create_event_loop +from ray._private.profiling import chrome_tracing_dump +from ray._private.ray_constants import env_integer +from ray.dashboard.state_api_utils import do_filter +from ray.dashboard.utils import compose_state_message +from ray.runtime_env import RuntimeEnv +from ray.util.state.common import ( + RAY_MAX_LIMIT_FROM_API_SERVER, + ActorState, + ActorSummaries, + JobState, + ListApiOptions, + ListApiResponse, + NodeState, + ObjectState, + ObjectSummaries, + PlacementGroupState, + RuntimeEnvState, + StateSummary, + SummaryApiOptions, + SummaryApiResponse, + TaskState, + TaskSummaries, + WorkerState, + protobuf_message_to_dict, + protobuf_to_task_state_dict, +) +from ray.util.state.state_manager import DataSourceUnavailable, StateDataSourceClient + +logger = logging.getLogger(__name__) + +GCS_QUERY_FAILURE_WARNING = ( + "Failed to query data from GCS. It is due to " + "(1) GCS is unexpectedly failed. " + "(2) GCS is overloaded. " + "(3) There's an unexpected network issue. " + "Please check the gcs_server.out log to find the root cause." +) +NODE_QUERY_FAILURE_WARNING = ( + "Failed to query data from {type}. " + "Queried {total} {type} " + "and {network_failures} {type} failed to reply. It is due to " + "(1) {type} is unexpectedly failed. " + "(2) {type} is overloaded. " + "(3) There's an unexpected network issue. Please check the " + "{log_command} to find the root cause." +) + + +# TODO(sang): Move the class to state/state_manager.py. +# TODO(sang): Remove *State and replaces with Pydantic or protobuf. +# (depending on API interface standardization). +class StateAPIManager: + """A class to query states from data source, caches, and post-processes + the entries. + """ + + def __init__( + self, + state_data_source_client: StateDataSourceClient, + thread_pool_executor: ThreadPoolExecutor, + ): + self._client = state_data_source_client + self._thread_pool_executor = thread_pool_executor + + @property + def data_source_client(self): + return self._client + + async def list_actors(self, *, option: ListApiOptions) -> ListApiResponse: + """List all actor information from the cluster. + + Returns: + {actor_id -> actor_data_in_dict} + actor_data_in_dict's schema is in ActorState + + """ + try: + reply = await self._client.get_all_actor_info( + timeout=option.timeout, filters=option.filters + ) + except DataSourceUnavailable: + raise DataSourceUnavailable(GCS_QUERY_FAILURE_WARNING) + + def transform(reply) -> ListApiResponse: + result = [] + for message in reply.actor_table_data: + # Note: this is different from actor_table_data_to_dict in actor_head.py + # because we set preserving_proto_field_name=True so fields are + # snake_case, while actor_table_data_to_dict in actor_head.py is + # camelCase. + # TODO(ryw): modify actor_table_data_to_dict to use snake_case, and + # consolidate the code. + data = protobuf_message_to_dict( + message=message, + fields_to_decode=[ + "actor_id", + "owner_id", + "job_id", + "node_id", + "placement_group_id", + ], + ) + result.append(data) + + num_after_truncation = len(result) + reply.num_filtered + result = do_filter(result, option.filters, ActorState, option.detail) + num_filtered = len(result) + + # Sort to make the output deterministic. + result.sort(key=lambda entry: entry["actor_id"]) + result = list(islice(result, option.limit)) + return ListApiResponse( + result=result, + total=reply.total, + num_after_truncation=num_after_truncation, + num_filtered=num_filtered, + ) + + return await get_or_create_event_loop().run_in_executor( + self._thread_pool_executor, transform, reply + ) + + async def list_placement_groups(self, *, option: ListApiOptions) -> ListApiResponse: + """List all placement group information from the cluster. + + Returns: + {pg_id -> pg_data_in_dict} + pg_data_in_dict's schema is in PlacementGroupState + """ + try: + reply = await self._client.get_all_placement_group_info( + timeout=option.timeout + ) + except DataSourceUnavailable: + raise DataSourceUnavailable(GCS_QUERY_FAILURE_WARNING) + + def transform(reply) -> ListApiResponse: + result = [] + for message in reply.placement_group_table_data: + data = protobuf_message_to_dict( + message=message, + fields_to_decode=[ + "placement_group_id", + "creator_job_id", + "node_id", + ], + ) + result.append(data) + num_after_truncation = len(result) + + result = do_filter( + result, option.filters, PlacementGroupState, option.detail + ) + num_filtered = len(result) + # Sort to make the output deterministic. + result.sort(key=lambda entry: entry["placement_group_id"]) + return ListApiResponse( + result=list(islice(result, option.limit)), + total=reply.total, + num_after_truncation=num_after_truncation, + num_filtered=num_filtered, + ) + + return await get_or_create_event_loop().run_in_executor( + self._thread_pool_executor, transform, reply + ) + + async def list_nodes(self, *, option: ListApiOptions) -> ListApiResponse: + """List all node information from the cluster. + + Returns: + {node_id -> node_data_in_dict} + node_data_in_dict's schema is in NodeState + """ + try: + reply = await self._client.get_all_node_info( + timeout=option.timeout, filters=option.filters + ) + except DataSourceUnavailable: + raise DataSourceUnavailable(GCS_QUERY_FAILURE_WARNING) + + def transform(reply) -> ListApiResponse: + result = [] + for message in reply.node_info_list: + data = protobuf_message_to_dict( + message=message, fields_to_decode=["node_id"] + ) + data["node_ip"] = data["node_manager_address"] + data["start_time_ms"] = int(data["start_time_ms"]) + data["end_time_ms"] = int(data["end_time_ms"]) + death_info = data.get("death_info", {}) + data["state_message"] = compose_state_message( + death_info.get("reason", None), + death_info.get("reason_message", None), + ) + + result.append(data) + + num_after_truncation = len(result) + reply.num_filtered + result = do_filter(result, option.filters, NodeState, option.detail) + num_filtered = len(result) + + # Sort to make the output deterministic. + result.sort(key=lambda entry: entry["node_id"]) + result = list(islice(result, option.limit)) + return ListApiResponse( + result=result, + total=reply.total, + num_after_truncation=num_after_truncation, + num_filtered=num_filtered, + ) + + return await get_or_create_event_loop().run_in_executor( + self._thread_pool_executor, transform, reply + ) + + async def list_workers(self, *, option: ListApiOptions) -> ListApiResponse: + """List all worker information from the cluster. + + Returns: + {worker_id -> worker_data_in_dict} + worker_data_in_dict's schema is in WorkerState + """ + try: + reply = await self._client.get_all_worker_info( + timeout=option.timeout, + filters=option.filters, + ) + except DataSourceUnavailable: + raise DataSourceUnavailable(GCS_QUERY_FAILURE_WARNING) + + def transform(reply) -> ListApiResponse: + + result = [] + for message in reply.worker_table_data: + data = protobuf_message_to_dict( + message=message, fields_to_decode=["worker_id", "node_id"] + ) + data["worker_id"] = data["worker_address"]["worker_id"] + data["node_id"] = data["worker_address"]["node_id"] + data["ip"] = data["worker_address"]["ip_address"] + data["start_time_ms"] = int(data["start_time_ms"]) + data["end_time_ms"] = int(data["end_time_ms"]) + data["worker_launch_time_ms"] = int(data["worker_launch_time_ms"]) + data["worker_launched_time_ms"] = int(data["worker_launched_time_ms"]) + result.append(data) + + num_after_truncation = len(result) + reply.num_filtered + result = do_filter(result, option.filters, WorkerState, option.detail) + num_filtered = len(result) + # Sort to make the output deterministic. + result.sort(key=lambda entry: entry["worker_id"]) + result = list(islice(result, option.limit)) + return ListApiResponse( + result=result, + total=reply.total, + num_after_truncation=num_after_truncation, + num_filtered=num_filtered, + ) + + return await get_or_create_event_loop().run_in_executor( + self._thread_pool_executor, transform, reply + ) + + async def list_jobs(self, *, option: ListApiOptions) -> ListApiResponse: + try: + reply = await self._client.get_job_info(timeout=option.timeout) + except DataSourceUnavailable: + raise DataSourceUnavailable(GCS_QUERY_FAILURE_WARNING) + + def transform(reply) -> ListApiResponse: + result = [job.dict() for job in reply] + total = len(result) + result = do_filter(result, option.filters, JobState, option.detail) + num_filtered = len(result) + result.sort(key=lambda entry: entry["job_id"] or "") + result = list(islice(result, option.limit)) + return ListApiResponse( + result=result, + total=total, + num_after_truncation=total, + num_filtered=num_filtered, + ) + + return await get_or_create_event_loop().run_in_executor( + self._thread_pool_executor, transform, reply + ) + + async def list_tasks(self, *, option: ListApiOptions) -> ListApiResponse: + """List all task information from the cluster. + + Returns: + {task_id -> task_data_in_dict} + task_data_in_dict's schema is in TaskState + """ + try: + reply = await self._client.get_all_task_info( + timeout=option.timeout, + filters=option.filters, + exclude_driver=option.exclude_driver, + ) + except DataSourceUnavailable: + raise DataSourceUnavailable(GCS_QUERY_FAILURE_WARNING) + + def transform(reply) -> ListApiResponse: + """ + Transforms from proto to dict, applies filters, sorts, and truncates. + This function is executed in a separate thread. + """ + result = [ + protobuf_to_task_state_dict(message) for message in reply.events_by_task + ] + + # Num pre-truncation is the number of tasks returned from + # source + num filtered on source + num_after_truncation = len(result) + num_total = len(result) + reply.num_status_task_events_dropped + + # Only certain filters are done on GCS, so here the filter function is still + # needed to apply all the filters + result = do_filter(result, option.filters, TaskState, option.detail) + num_filtered = len(result) + + result.sort(key=lambda entry: entry["task_id"]) + result = list(islice(result, option.limit)) + + # TODO(rickyx): we could do better with the warning logic. It's messy now. + return ListApiResponse( + result=result, + total=num_total, + num_after_truncation=num_after_truncation, + num_filtered=num_filtered, + ) + + # In the error case + if reply.status.code != 0: + return ListApiResponse( + result=[], + total=0, + num_after_truncation=0, + num_filtered=0, + warnings=[reply.status.message], + ) + + return await get_or_create_event_loop().run_in_executor( + self._thread_pool_executor, transform, reply + ) + + async def list_objects(self, *, option: ListApiOptions) -> ListApiResponse: + """List all object information from the cluster. + + Returns: + {object_id -> object_data_in_dict} + object_data_in_dict's schema is in ObjectState + """ + all_node_info_reply = await self._client.get_all_node_info( + timeout=option.timeout, + limit=None, + filters=[("state", "=", "ALIVE")], + ) + tasks = [ + self._client.get_object_info( + node_info.node_manager_address, + node_info.node_manager_port, + timeout=option.timeout, + ) + for node_info in all_node_info_reply.node_info_list + ] + + replies = await asyncio.gather( + *tasks, + return_exceptions=True, + ) + + def transform(replies) -> ListApiResponse: + unresponsive_nodes = 0 + worker_stats = [] + total_objects = 0 + for reply in replies: + if isinstance(reply, DataSourceUnavailable): + unresponsive_nodes += 1 + continue + elif isinstance(reply, Exception): + raise reply + + total_objects += reply.total + for core_worker_stat in reply.core_workers_stats: + # NOTE: Set preserving_proto_field_name=False here because + # `construct_memory_table` requires a dictionary that has + # modified protobuf name + # (e.g., workerId instead of worker_id) as a key. + worker_stats.append( + protobuf_message_to_dict( + message=core_worker_stat, + fields_to_decode=["object_id"], + preserving_proto_field_name=False, + ) + ) + + partial_failure_warning = None + if len(tasks) > 0 and unresponsive_nodes > 0: + warning_msg = NODE_QUERY_FAILURE_WARNING.format( + type="raylet", + total=len(tasks), + network_failures=unresponsive_nodes, + log_command="raylet.out", + ) + if unresponsive_nodes == len(tasks): + raise DataSourceUnavailable(warning_msg) + partial_failure_warning = ( + f"The returned data may contain incomplete result. {warning_msg}" + ) + + result = [] + memory_table = memory_utils.construct_memory_table(worker_stats) + for entry in memory_table.table: + data = entry.as_dict() + # `construct_memory_table` returns object_ref field which is indeed + # object_id. We do transformation here. + # TODO(sang): Refactor `construct_memory_table`. + data["object_id"] = data["object_ref"] + del data["object_ref"] + data["ip"] = data["node_ip_address"] + del data["node_ip_address"] + data["type"] = data["type"].upper() + data["task_status"] = ( + "NIL" if data["task_status"] == "-" else data["task_status"] + ) + result.append(data) + + # Add callsite warnings if it is not configured. + callsite_warning = [] + callsite_enabled = env_integer("RAY_record_ref_creation_sites", 0) + if not callsite_enabled: + callsite_warning.append( + "Callsite is not being recorded. " + "To record callsite information for each ObjectRef created, set " + "env variable RAY_record_ref_creation_sites=1 during `ray start` " + "and `ray.init`." + ) + + num_after_truncation = len(result) + result = do_filter(result, option.filters, ObjectState, option.detail) + num_filtered = len(result) + # Sort to make the output deterministic. + result.sort(key=lambda entry: entry["object_id"]) + result = list(islice(result, option.limit)) + return ListApiResponse( + result=result, + partial_failure_warning=partial_failure_warning, + total=total_objects, + num_after_truncation=num_after_truncation, + num_filtered=num_filtered, + warnings=callsite_warning, + ) + + return await get_or_create_event_loop().run_in_executor( + self._thread_pool_executor, transform, replies + ) + + async def list_runtime_envs(self, *, option: ListApiOptions) -> ListApiResponse: + """List all runtime env information from the cluster. + + Returns: + A list of runtime env information in the cluster. + The schema of returned "dict" is equivalent to the + `RuntimeEnvState` protobuf message. + We don't have id -> data mapping like other API because runtime env + doesn't have unique ids. + """ + live_node_info_reply = await self._client.get_all_node_info( + timeout=option.timeout, + limit=None, + filters=[("state", "=", "ALIVE")], + ) + node_infos = [ + node_info + for node_info in live_node_info_reply.node_info_list + if node_info.runtime_env_agent_port is not None + ] + tasks = [ + self._client.get_runtime_envs_info( + node_info.node_manager_address, + node_info.runtime_env_agent_port, + timeout=option.timeout, + ) + for node_info in node_infos + ] + + replies = await asyncio.gather( + *tasks, + return_exceptions=True, + ) + + def transform(replies) -> ListApiResponse: + result = [] + unresponsive_nodes = 0 + total_runtime_envs = 0 + for node_info, reply in zip(node_infos, replies): + if isinstance(reply, DataSourceUnavailable): + unresponsive_nodes += 1 + continue + elif isinstance(reply, Exception): + raise reply + + total_runtime_envs += reply.total + states = reply.runtime_env_states + for state in states: + data = protobuf_message_to_dict(message=state, fields_to_decode=[]) + # Need to deserialize this field. + data["runtime_env"] = RuntimeEnv.deserialize( + data["runtime_env"] + ).to_dict() + data["node_id"] = NodeID(node_info.node_id).hex() + result.append(data) + + partial_failure_warning = None + if len(tasks) > 0 and unresponsive_nodes > 0: + warning_msg = NODE_QUERY_FAILURE_WARNING.format( + type="agent", + total=len(tasks), + network_failures=unresponsive_nodes, + log_command="dashboard_agent.log", + ) + if unresponsive_nodes == len(tasks): + raise DataSourceUnavailable(warning_msg) + partial_failure_warning = ( + f"The returned data may contain incomplete result. {warning_msg}" + ) + num_after_truncation = len(result) + result = do_filter(result, option.filters, RuntimeEnvState, option.detail) + num_filtered = len(result) + + # Sort to make the output deterministic. + def sort_func(entry): + # If creation time is not there yet (runtime env is failed + # to be created or not created yet, they are the highest priority. + # Otherwise, "bigger" creation time is coming first. + if "creation_time_ms" not in entry: + return float("inf") + elif entry["creation_time_ms"] is None: + return float("inf") + else: + return float(entry["creation_time_ms"]) + + result.sort(key=sort_func, reverse=True) + result = list(islice(result, option.limit)) + return ListApiResponse( + result=result, + partial_failure_warning=partial_failure_warning, + total=total_runtime_envs, + num_after_truncation=num_after_truncation, + num_filtered=num_filtered, + ) + + return await get_or_create_event_loop().run_in_executor( + self._thread_pool_executor, transform, replies + ) + + async def summarize_tasks(self, option: SummaryApiOptions) -> SummaryApiResponse: + summary_by = option.summary_by or "func_name" + if summary_by not in ["func_name", "lineage"]: + raise ValueError('summary_by must be one of "func_name" or "lineage".') + + # For summary, try getting as many entries as possible to minimze data loss. + result = await self.list_tasks( + option=ListApiOptions( + timeout=option.timeout, + limit=RAY_MAX_LIMIT_FROM_API_SERVER, + filters=option.filters, + detail=summary_by == "lineage", + ) + ) + + if summary_by == "func_name": + summary_results = TaskSummaries.to_summary_by_func_name(tasks=result.result) + else: + # We will need the actors info for actor tasks. + actors = await self.list_actors( + option=ListApiOptions( + timeout=option.timeout, + limit=RAY_MAX_LIMIT_FROM_API_SERVER, + detail=True, + ) + ) + summary_results = TaskSummaries.to_summary_by_lineage( + tasks=result.result, actors=actors.result + ) + summary = StateSummary(node_id_to_summary={"cluster": summary_results}) + warnings = result.warnings + if ( + summary_results.total_actor_scheduled + + summary_results.total_actor_tasks + + summary_results.total_tasks + < result.num_filtered + ): + warnings = warnings or [] + warnings.append( + "There is missing data in this aggregation. " + "Possibly due to task data being evicted to preserve memory." + ) + return SummaryApiResponse( + total=result.total, + result=summary, + partial_failure_warning=result.partial_failure_warning, + warnings=warnings, + num_after_truncation=result.num_after_truncation, + num_filtered=result.num_filtered, + ) + + async def summarize_actors(self, option: SummaryApiOptions) -> SummaryApiResponse: + # For summary, try getting as many entries as possible to minimze data loss. + result = await self.list_actors( + option=ListApiOptions( + timeout=option.timeout, + limit=RAY_MAX_LIMIT_FROM_API_SERVER, + filters=option.filters, + ) + ) + summary = StateSummary( + node_id_to_summary={ + "cluster": ActorSummaries.to_summary(actors=result.result) + } + ) + return SummaryApiResponse( + total=result.total, + result=summary, + partial_failure_warning=result.partial_failure_warning, + warnings=result.warnings, + num_after_truncation=result.num_after_truncation, + num_filtered=result.num_filtered, + ) + + async def summarize_objects(self, option: SummaryApiOptions) -> SummaryApiResponse: + # For summary, try getting as many entries as possible to minimize data loss. + result = await self.list_objects( + option=ListApiOptions( + timeout=option.timeout, + limit=RAY_MAX_LIMIT_FROM_API_SERVER, + filters=option.filters, + ) + ) + summary = StateSummary( + node_id_to_summary={ + "cluster": ObjectSummaries.to_summary(objects=result.result) + } + ) + return SummaryApiResponse( + total=result.total, + result=summary, + partial_failure_warning=result.partial_failure_warning, + warnings=result.warnings, + num_after_truncation=result.num_after_truncation, + num_filtered=result.num_filtered, + ) + + async def generate_task_timeline(self, job_id: Optional[str]) -> List[dict]: + filters = [("job_id", "=", job_id)] if job_id else None + result = await self.list_tasks( + option=ListApiOptions(detail=True, filters=filters, limit=10000) + ) + return chrome_tracing_dump(result.result) diff --git a/lib/python3.12/site-packages/ray/dashboard/state_api_utils.py b/lib/python3.12/site-packages/ray/dashboard/state_api_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b794bf840d320d528e62cd302ae45e6ca1844c94 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/state_api_utils.py @@ -0,0 +1,266 @@ +import dataclasses +from dataclasses import asdict, fields +from typing import Awaitable, Callable, List, Tuple + +import aiohttp.web + +from ray.dashboard.optional_utils import rest_response +from ray.dashboard.utils import HTTPStatusCode +from ray.util.state.common import ( + DEFAULT_LIMIT, + DEFAULT_RPC_TIMEOUT, + RAY_MAX_LIMIT_FROM_API_SERVER, + ListApiOptions, + ListApiResponse, + PredicateType, + StateSchema, + SummaryApiOptions, + SummaryApiResponse, + SupportedFilterType, + filter_fields, +) +from ray.util.state.exception import DataSourceUnavailable +from ray.util.state.util import convert_string_to_type + + +def do_reply( + status_code: HTTPStatusCode, error_message: str, result: ListApiResponse, **kwargs +): + return rest_response( + status_code=status_code, + message=error_message, + result=result, + convert_google_style=False, + **kwargs, + ) + + +async def handle_list_api( + list_api_fn: Callable[[ListApiOptions], Awaitable[ListApiResponse]], + req: aiohttp.web.Request, +): + try: + result = await list_api_fn(option=options_from_req(req)) + return do_reply( + status_code=HTTPStatusCode.OK, + error_message="", + result=asdict(result), + ) + except ValueError as e: + return do_reply( + status_code=HTTPStatusCode.BAD_REQUEST, + error_message=str(e), + result=None, + ) + except DataSourceUnavailable as e: + return do_reply( + status_code=HTTPStatusCode.INTERNAL_ERROR, + error_message=str(e), + result=None, + ) + + +def _get_filters_from_req( + req: aiohttp.web.Request, +) -> List[Tuple[str, PredicateType, SupportedFilterType]]: + filter_keys = req.query.getall("filter_keys", []) + filter_predicates = req.query.getall("filter_predicates", []) + filter_values = req.query.getall("filter_values", []) + assert len(filter_keys) == len(filter_values) + filters = [] + for key, predicate, val in zip(filter_keys, filter_predicates, filter_values): + filters.append((key, predicate, val)) + return filters + + +def options_from_req(req: aiohttp.web.Request) -> ListApiOptions: + """Obtain `ListApiOptions` from the aiohttp request.""" + limit = int( + req.query.get("limit") if req.query.get("limit") is not None else DEFAULT_LIMIT + ) + + if limit > RAY_MAX_LIMIT_FROM_API_SERVER: + raise ValueError( + f"Given limit {limit} exceeds the supported " + f"Given limit {limit} exceeds the supported " + f"limit {RAY_MAX_LIMIT_FROM_API_SERVER}. Use a lower limit, or set the " + f"`RAY_MAX_LIMIT_FROM_API_SERVER` environment variable to a larger value." + ) + + timeout = int(req.query.get("timeout", 30)) + filters = _get_filters_from_req(req) + detail = convert_string_to_type(req.query.get("detail", False), bool) + exclude_driver = convert_string_to_type(req.query.get("exclude_driver", True), bool) + + return ListApiOptions( + limit=limit, + timeout=timeout, + filters=filters, + detail=detail, + exclude_driver=exclude_driver, + ) + + +def summary_options_from_req(req: aiohttp.web.Request) -> SummaryApiOptions: + timeout = int(req.query.get("timeout", DEFAULT_RPC_TIMEOUT)) + filters = _get_filters_from_req(req) + summary_by = req.query.get("summary_by", None) + return SummaryApiOptions(timeout=timeout, filters=filters, summary_by=summary_by) + + +async def handle_summary_api( + summary_fn: Callable[[SummaryApiOptions], SummaryApiResponse], + req: aiohttp.web.Request, +): + result = await summary_fn(option=summary_options_from_req(req)) + return do_reply( + status_code=HTTPStatusCode.OK, + error_message="", + result=asdict(result), + ) + + +def convert_filters_type( + filter: List[Tuple[str, PredicateType, SupportedFilterType]], + schema: StateSchema, +) -> List[Tuple[str, PredicateType, SupportedFilterType]]: + """Convert the given filter's type to SupportedFilterType. + + This method is necessary because click can only accept a single type + for its tuple (which is string in this case). + + Args: + filter: A list of filter which is a tuple of (key, val). + schema: The state schema. It is used to infer the type of the column for filter. + + Returns: + A new list of filters with correct types that match the schema. + """ + new_filter = [] + if dataclasses.is_dataclass(schema): + schema = {field.name: field.type for field in fields(schema)} + else: + schema = schema.schema_dict() + + for col, predicate, val in filter: + if col in schema: + column_type = schema[col] + try: + isinstance(val, column_type) + except TypeError: + # Calling `isinstance` to the Literal type raises a TypeError. + # Ignore this case. + pass + else: + if isinstance(val, column_type): + # Do nothing. + pass + elif column_type is int or column_type == "integer": + try: + val = convert_string_to_type(val, int) + except ValueError: + raise ValueError( + f"Invalid filter `--filter {col} {val}` for a int type " + "column. Please provide an integer filter " + f"`--filter {col} [int]`" + ) + elif column_type is float or column_type == "number": + try: + val = convert_string_to_type( + val, + float, + ) + except ValueError: + raise ValueError( + f"Invalid filter `--filter {col} {val}` for a float " + "type column. Please provide an integer filter " + f"`--filter {col} [float]`" + ) + elif column_type is bool or column_type == "boolean": + try: + val = convert_string_to_type(val, bool) + except ValueError: + raise ValueError( + f"Invalid filter `--filter {col} {val}` for a boolean " + "type column. Please provide " + f"`--filter {col} [True|true|1]` for True or " + f"`--filter {col} [False|false|0]` for False." + ) + new_filter.append((col, predicate, val)) + return new_filter + + +def do_filter( + data: List[dict], + filters: List[Tuple[str, PredicateType, SupportedFilterType]], + state_dataclass: StateSchema, + detail: bool, +) -> List[dict]: + """Return the filtered data given filters. + + Args: + data: A list of state data. + filters: A list of KV tuple to filter data (key, val). The data is filtered + if data[key] != val. + state_dataclass: The state schema. + + Returns: + A list of filtered state data in dictionary. Each state data's + unnecessary columns are filtered by the given state_dataclass schema. + """ + filters = convert_filters_type(filters, state_dataclass) + result = [] + for datum in data: + match = True + for filter_column, filter_predicate, filter_value in filters: + filterable_columns = state_dataclass.filterable_columns() + filter_column = filter_column.lower() + if filter_column not in filterable_columns: + raise ValueError( + f"The given filter column {filter_column} is not supported. " + "Enter filters with –-filter key=value " + "or –-filter key!=value " + f"Supported filter columns: {filterable_columns}" + ) + + if filter_column not in datum: + match = False + elif filter_predicate == "=": + if isinstance(filter_value, str) and isinstance( + datum[filter_column], str + ): + # Case insensitive match for string filter values. + match = datum[filter_column].lower() == filter_value.lower() + elif isinstance(filter_value, str) and isinstance( + datum[filter_column], bool + ): + match = datum[filter_column] == convert_string_to_type( + filter_value, bool + ) + elif isinstance(filter_value, str) and isinstance( + datum[filter_column], int + ): + match = datum[filter_column] == convert_string_to_type( + filter_value, int + ) + else: + match = datum[filter_column] == filter_value + elif filter_predicate == "!=": + if isinstance(filter_value, str) and isinstance( + datum[filter_column], str + ): + match = datum[filter_column].lower() != filter_value.lower() + else: + match = datum[filter_column] != filter_value + else: + raise ValueError( + f"Unsupported filter predicate {filter_predicate} is given. " + "Available predicates: =, !=." + ) + + if not match: + break + + if match: + result.append(filter_fields(datum, state_dataclass, detail)) + return result diff --git a/lib/python3.12/site-packages/ray/dashboard/subprocesses/__init__.py b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39d27915a452262f57a242c7732d7d48965b1831 Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/handle.cpython-312.pyc b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/handle.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ac5cc375de78be71b4422fa126e39cf993c3fe8 Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/handle.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/module.cpython-312.pyc b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/module.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e67c834f1ba77c5afa2a49e9999f03852d373be Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/module.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/routes.cpython-312.pyc b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8488f19495514829378b297f0b7881b285a0a33b Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/routes.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b55548d296b67b43dc3949afda2a54309a28db0b Binary files /dev/null and b/lib/python3.12/site-packages/ray/dashboard/subprocesses/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/dashboard/subprocesses/handle.py b/lib/python3.12/site-packages/ray/dashboard/subprocesses/handle.py new file mode 100644 index 0000000000000000000000000000000000000000..3dbb1843db9ad9bc4dbcec0193d6769ec694cd42 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/subprocesses/handle.py @@ -0,0 +1,341 @@ +import asyncio +import logging +import multiprocessing +import os +from typing import Optional, Union + +import multidict + +import ray.dashboard.consts as dashboard_consts +from ray.dashboard.optional_deps import aiohttp +from ray.dashboard.subprocesses.module import ( + SubprocessModule, + SubprocessModuleConfig, + run_module, +) +from ray.dashboard.subprocesses.utils import ( + ResponseType, + get_http_session_to_module, + module_logging_filename, +) + +""" +This file contains code run in the parent process. It can start a subprocess and send +messages to it. Requires non-minimal Ray. +""" + +logger = logging.getLogger(__name__) + + +def filter_hop_by_hop_headers( + headers: Union[dict[str, str], multidict.CIMultiDictProxy[str]], +) -> dict[str, str]: + """ + Filter out hop-by-hop headers from the headers dict. + """ + HOP_BY_HOP_HEADERS = { + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", + } + if isinstance(headers, multidict.CIMultiDictProxy): + headers = dict(headers) + filtered_headers = { + key: value + for key, value in headers.items() + if key.lower() not in HOP_BY_HOP_HEADERS + } + return filtered_headers + + +class SubprocessModuleHandle: + """ + A handle to a module created as a subprocess. Can send messages to the module and + receive responses. It only acts as a proxy to the aiohttp server running in the + subprocess. On destruction, the subprocess is terminated. + + Lifecycle: + 1. In SubprocessModuleHandle creation, the subprocess is started and runs an aiohttp + server. + 2. User must call start_module() and wait_for_module_ready() first. + 3. SubprocessRouteTable.bind(handle) + 4. app.add_routes(routes=SubprocessRouteTable.bound_routes()) + 5. Run the app. + + Health check (_do_periodic_health_check): + Every 1s, do a health check by _do_once_health_check. If the module is + unhealthy: + 1. log the exception + 2. log the last N lines of the log file + 3. fail all active requests + 4. restart the module + + TODO(ryw): define policy for health check: + - check period (Now: 1s) + - define unhealthy. (Now: process exits. TODO: check_health() for event loop hang) + - check number of failures in a row before we deem it unhealthy (Now: N/A) + - "max number of restarts"? (Now: infinite) + """ + + # Class variable. Force using spawn because Ray C bindings have static variables + # that need to be re-initialized for a new process. + mp_context = multiprocessing.get_context("spawn") + + def __init__( + self, + loop: asyncio.AbstractEventLoop, + module_cls: type[SubprocessModule], + config: SubprocessModuleConfig, + ): + self.loop = loop + self.module_cls = module_cls + self.config = config + + # Increment this when the module is restarted. + self.incarnation = 0 + # Runtime states, set by start_module() and wait_for_module_ready(), + # reset by destroy_module(). + self.parent_conn = None + self.process = None + self.http_client_session: Optional[aiohttp.ClientSession] = None + self.health_check_task = None + + def str_for_state(self, incarnation: int, pid: Optional[int]): + return f"SubprocessModuleHandle(module_cls={self.module_cls.__name__}, incarnation={incarnation}, pid={pid})" + + def __str__(self): + return self.str_for_state( + self.incarnation, self.process.pid if self.process else None + ) + + def start_module(self): + """ + Start the module. Should be non-blocking. + """ + self.parent_conn, child_conn = self.mp_context.Pipe() + if not os.path.exists(self.config.socket_dir): + os.makedirs(self.config.socket_dir) + self.process = self.mp_context.Process( + target=run_module, + args=( + self.module_cls, + self.config, + self.incarnation, + child_conn, + ), + daemon=True, + name=f"{self.module_cls.__name__}-{self.incarnation}", + ) + self.process.start() + child_conn.close() + + def wait_for_module_ready(self): + """ + Wait for the module to be ready. This is called after start_module() + and can be blocking. + """ + if self.parent_conn.poll(dashboard_consts.SUBPROCESS_MODULE_WAIT_READY_TIMEOUT): + try: + self.parent_conn.recv() + except EOFError: + raise RuntimeError( + f"Module {self.module_cls.__name__} failed to start. " + "Received EOF from pipe." + ) + self.parent_conn.close() + self.parent_conn = None + else: + raise RuntimeError( + f"Module {self.module_cls.__name__} failed to start. " + f"Timeout after {dashboard_consts.SUBPROCESS_MODULE_WAIT_READY_TIMEOUT} seconds." + ) + + module_name = self.module_cls.__name__ + self.http_client_session = get_http_session_to_module( + module_name, self.config.socket_dir, self.config.session_name + ) + + self.health_check_task = self.loop.create_task(self._do_periodic_health_check()) + + async def destroy_module(self): + """ + Destroy the module. This is called when the module is unhealthy. + """ + self.incarnation += 1 + + if self.parent_conn: + self.parent_conn.close() + self.parent_conn = None + + if self.process: + self.process.kill() + self.process.join() + self.process = None + + if self.http_client_session: + await self.http_client_session.close() + self.http_client_session = None + + if self.health_check_task: + self.health_check_task.cancel() + self.health_check_task = None + + async def _health_check(self) -> aiohttp.web.Response: + """ + Do internal health check. The module should respond immediately with a 200 OK. + This can be used to measure module responsiveness in RTT, it also indicates + subprocess event loop lag. + + Currently you get a 200 OK with body = b'success'. Later if we want we can add more + observability payloads. + """ + resp = await self.http_client_session.get("http://localhost/api/healthz") + return aiohttp.web.Response( + status=resp.status, + headers=filter_hop_by_hop_headers(resp.headers), + body=await resp.read(), + ) + + async def _do_once_health_check(self): + """ + Do a health check once. We check for: + 1. if the process exits, it's considered died. + 2. if the health check endpoint returns non-200, it's considered unhealthy. + + """ + if self.process.exitcode is not None: + raise RuntimeError(f"Process exited with code {self.process.exitcode}") + resp = await self._health_check() + if resp.status != 200: + raise RuntimeError(f"Health check failed: status code is {resp.status}") + + async def _do_periodic_health_check(self): + """ + Every 1s, do a health check. If the module is unhealthy: + 1. log the exception + 2. log the last N lines of the log file + 3. restart the module + """ + while True: + try: + await self._do_once_health_check() + except Exception: + filename = module_logging_filename( + self.module_cls.__name__, self.config.logging_filename + ) + logger.exception( + f"Module {self.module_cls.__name__} is unhealthy. Please refer to " + f"{self.config.log_dir}/{filename} for more details. Failing all " + "active requests." + ) + await self.destroy_module() + self.start_module() + self.wait_for_module_ready() + return + await asyncio.sleep(1) + + async def proxy_request( + self, request: aiohttp.web.Request, resp_type: ResponseType = ResponseType.HTTP + ) -> aiohttp.web.StreamResponse: + """ + Sends a new request to the subprocess and returns the response. + """ + if resp_type == ResponseType.HTTP: + return await self.proxy_http(request) + if resp_type == ResponseType.STREAM: + return await self.proxy_stream(request) + if resp_type == ResponseType.WEBSOCKET: + return await self.proxy_websocket(request) + raise ValueError(f"Unknown response type: {resp_type}") + + async def proxy_http(self, request: aiohttp.web.Request) -> aiohttp.web.Response: + """ + Proxy handler for non-streaming HTTP API + It forwards the method, query string, headers, and body to the backend. + """ + url = f"http://localhost{request.path_qs}" + body = await request.read() + + async with self.http_client_session.request( + request.method, + url, + data=body, + headers=filter_hop_by_hop_headers(request.headers), + allow_redirects=False, + ) as backend_resp: + resp_body = await backend_resp.read() + return aiohttp.web.Response( + status=backend_resp.status, + headers=filter_hop_by_hop_headers(backend_resp.headers), + body=resp_body, + ) + + async def proxy_stream( + self, request: aiohttp.web.Request + ) -> aiohttp.web.StreamResponse: + """ + Proxy handler for streaming HTTP API. + It forwards the method, query string, and body to the backend. + """ + url = f"http://localhost{request.path_qs}" + body = await request.read() + async with self.http_client_session.request( + request.method, + url, + data=body, + headers=filter_hop_by_hop_headers(request.headers), + ) as backend_resp: + proxy_resp = aiohttp.web.StreamResponse( + status=backend_resp.status, + headers=filter_hop_by_hop_headers(backend_resp.headers), + ) + await proxy_resp.prepare(request) + + async for chunk, _ in backend_resp.content.iter_chunks(): + await proxy_resp.write(chunk) + await proxy_resp.write_eof() + return proxy_resp + + async def proxy_websocket( + self, request: aiohttp.web.Request + ) -> aiohttp.web.StreamResponse: + """ + Proxy handler for WebSocket API + It establishes a WebSocket connection with the client and simultaneously connects + to the backend server's WebSocket endpoint. Messages are forwarded in single + direction from the backend to the client. + If the backend responds with normal HTTP response, then try to treat it as a normal + HTTP request and calls proxy_http instead. + + TODO: Support bidirectional communication if needed. We only support one direction + because it's sufficient for the current use case. + """ + url = f"http://localhost{request.path_qs}" + + try: + async with self.http_client_session.ws_connect( + url, headers=filter_hop_by_hop_headers(request.headers) + ) as ws_to_backend: + ws_from_client = aiohttp.web.WebSocketResponse() + await ws_from_client.prepare(request) + async for msg in ws_to_backend: + if msg.type == aiohttp.WSMsgType.TEXT: + await ws_from_client.send_str(msg.data) + elif msg.type == aiohttp.WSMsgType.BINARY: + await ws_from_client.send_bytes(msg.data) + else: + logger.error(f"Unknown msg type: {msg.type}") + await ws_from_client.close() + return ws_from_client + except aiohttp.WSServerHandshakeError as e: + logger.warning(f"WebSocket handshake error: {repr(e)}") + # Try to treat it as a normal HTTP request + return await self.proxy_http(request) + except Exception as e: + logger.error(f"WebSocket proxy error: {repr(e)}") + raise aiohttp.web.HTTPInternalServerError(reason="WebSocket proxy error") diff --git a/lib/python3.12/site-packages/ray/dashboard/subprocesses/module.py b/lib/python3.12/site-packages/ray/dashboard/subprocesses/module.py new file mode 100644 index 0000000000000000000000000000000000000000..21e655c1f098e5b9a39d7e5c8f19d62403eb7e92 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/subprocesses/module.py @@ -0,0 +1,290 @@ +import abc +import asyncio +import inspect +import logging +import multiprocessing +import multiprocessing.connection +import os +import sys +from dataclasses import dataclass + +import aiohttp + +import ray +from ray import ray_constants +from ray._private import logging_utils +from ray._private.gcs_utils import GcsChannel +from ray._private.ray_logging import setup_component_logger +from ray._raylet import GcsClient +from ray.dashboard.subprocesses.utils import ( + get_named_pipe_path, + get_socket_path, + module_logging_filename, +) + +logger = logging.getLogger(__name__) + + +@dataclass +class SubprocessModuleConfig: + """ + Configuration for a SubprocessModule. + Pickleable. + """ + + cluster_id_hex: str + gcs_address: str + session_name: str + temp_dir: str + session_dir: str + # Logger configs. Will be set up in subprocess entrypoint `run_module`. + logging_level: str + logging_format: str + log_dir: str + # Name of the "base" log file. Its stem is appended with the Module.__name__. + # e.g. when logging_filename = "dashboard.log", and Module is JobHead, + # we will set up logger with name "dashboard_JobHead.log". This name will again be + # appended with .1 and .2 for rotation. + logging_filename: str + logging_rotate_bytes: int + logging_rotate_backup_count: int + # The directory where the socket file will be created. + socket_dir: str + + +class SubprocessModule(abc.ABC): + """ + A Dashboard Head Module that runs in a subprocess as a standalone aiohttp server. + """ + + def __init__( + self, + config: SubprocessModuleConfig, + ): + """ + Initialize current module when DashboardHead loading modules. + :param dashboard_head: The DashboardHead instance. + """ + self._config = config + self._parent_process = multiprocessing.parent_process() + # Lazy init + self._gcs_client = None + self._aiogrpc_gcs_channel = None + self._parent_process_death_detection_task = None + self._http_session = None + + async def _detect_parent_process_death(self): + """ + Detect parent process liveness. Only returns when parent process is dead. + """ + while True: + if not self._parent_process.is_alive(): + logger.warning( + f"Parent process {self._parent_process.pid} died. Exiting..." + ) + return + await asyncio.sleep(1) + + @staticmethod + def is_minimal_module(): + """ + Currently all SubprocessModule classes should be non-minimal. + + We require this because SubprocessModuleHandle tracks aiohttp requests and + responses. To ease this, we can define another SubprocessModuleMinimalHandle + that doesn't track requests and responses, but still provides Queue interface + and health check. + TODO(ryw): If needed, create SubprocessModuleMinimalHandle. + """ + return False + + async def run(self): + """ + Start running the module. + This method should be called first before the module starts receiving requests. + """ + app = aiohttp.web.Application( + client_max_size=ray_constants.DASHBOARD_CLIENT_MAX_SIZE, + ) + routes: list[aiohttp.web.RouteDef] = [ + aiohttp.web.get("/api/healthz", self._internal_module_health_check) + ] + handlers = inspect.getmembers( + self, + lambda x: ( + inspect.ismethod(x) + and hasattr(x, "__route_method__") + and hasattr(x, "__route_path__") + ), + ) + for _, handler in handlers: + routes.append( + aiohttp.web.route( + handler.__route_method__, + handler.__route_path__, + handler, + ) + ) + app.add_routes(routes) + runner = aiohttp.web.AppRunner(app, access_log=None) + await runner.setup() + + module_name = self.__class__.__name__ + if sys.platform == "win32": + named_pipe_path = get_named_pipe_path( + module_name, self._config.session_name + ) + site = aiohttp.web.NamedPipeSite(runner, named_pipe_path) + logger.info(f"Started aiohttp server over {named_pipe_path}.") + else: + socket_path = get_socket_path(self._config.socket_dir, module_name) + site = aiohttp.web.UnixSite(runner, socket_path) + logger.info(f"Started aiohttp server over {socket_path}.") + await site.start() + + @property + def gcs_client(self): + if self._gcs_client is None: + if not ray.experimental.internal_kv._internal_kv_initialized(): + gcs_client = GcsClient( + address=self._config.gcs_address, + cluster_id=self._config.cluster_id_hex, + ) + ray.experimental.internal_kv._initialize_internal_kv(gcs_client) + self._gcs_client = ray.experimental.internal_kv.internal_kv_get_gcs_client() + return self._gcs_client + + @property + def aiogrpc_gcs_channel(self): + if self._aiogrpc_gcs_channel is None: + gcs_channel = GcsChannel(gcs_address=self._config.gcs_address, aio=True) + gcs_channel.connect() + self._aiogrpc_gcs_channel = gcs_channel.channel() + return self._aiogrpc_gcs_channel + + @property + def session_name(self): + """ + Return the Ray session name. It's not related to the aiohttp session. + """ + return self._config.session_name + + @property + def temp_dir(self): + return self._config.temp_dir + + @property + def session_dir(self): + return self._config.session_dir + + @property + def log_dir(self): + return self._config.log_dir + + @property + def http_session(self): + if self._http_session is None: + self._http_session = aiohttp.ClientSession() + return self._http_session + + @property + def gcs_address(self): + return self._config.gcs_address + + async def _internal_module_health_check(self, request): + return aiohttp.web.Response( + text="success", + content_type="application/text", + ) + + +async def run_module_inner( + cls: type[SubprocessModule], + config: SubprocessModuleConfig, + incarnation: int, + child_conn: multiprocessing.connection.Connection, +): + + module_name = cls.__name__ + + logger.info( + f"Starting module {module_name} with incarnation {incarnation} and config {config}" + ) + + try: + module = cls(config) + module._parent_process_death_detection_task = asyncio.create_task( + module._detect_parent_process_death() + ) + module._parent_process_death_detection_task.add_done_callback( + lambda _: sys.exit() + ) + await module.run() + child_conn.send(None) + child_conn.close() + logger.info(f"Module {module_name} initialized, receiving messages...") + except Exception as e: + logger.exception(f"Error creating module {module_name}") + raise e + + +def run_module( + cls: type[SubprocessModule], + config: SubprocessModuleConfig, + incarnation: int, + child_conn: multiprocessing.connection.Connection, +): + """ + Entrypoint for a subprocess module. + """ + module_name = cls.__name__ + current_proctitle = ray._raylet.getproctitle() + ray._raylet.setproctitle( + f"ray-dashboard-{module_name}-{incarnation} ({current_proctitle})" + ) + logging_filename = module_logging_filename(module_name, config.logging_filename) + setup_component_logger( + logging_level=config.logging_level, + logging_format=config.logging_format, + log_dir=config.log_dir, + filename=logging_filename, + max_bytes=config.logging_rotate_bytes, + backup_count=config.logging_rotate_backup_count, + ) + + if config.logging_filename: + stdout_filename = module_logging_filename( + module_name, config.logging_filename, extension=".out" + ) + stderr_filename = module_logging_filename( + module_name, config.logging_filename, extension=".err" + ) + logging_utils.redirect_stdout_stderr_if_needed( + os.path.join(config.log_dir, stdout_filename), + os.path.join(config.log_dir, stderr_filename), + config.logging_rotate_bytes, + config.logging_rotate_backup_count, + ) + + loop = asyncio.new_event_loop() + task = loop.create_task( + run_module_inner( + cls, + config, + incarnation, + child_conn, + ) + ) + # TODO: do graceful shutdown. + # 1. define a stop token. + # 2. join the loop to wait for all pending tasks to finish, up until a timeout. + # 3. close the loop and exit. + + def sigterm_handler(signum, frame): + logger.warning(f"Exiting with signal {signum} immediately...") + sys.exit(signum) + + ray._private.utils.set_sigterm_handler(sigterm_handler) + + loop.run_until_complete(task) + loop.run_forever() diff --git a/lib/python3.12/site-packages/ray/dashboard/subprocesses/routes.py b/lib/python3.12/site-packages/ray/dashboard/subprocesses/routes.py new file mode 100644 index 0000000000000000000000000000000000000000..25bd59c4d2fe3088fef09a6b71c9795025de460c --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/subprocesses/routes.py @@ -0,0 +1,96 @@ +import collections +import inspect + +from ray.dashboard.optional_deps import aiohttp +from ray.dashboard.routes import BaseRouteTable +from ray.dashboard.subprocesses.handle import SubprocessModuleHandle +from ray.dashboard.subprocesses.utils import ResponseType + + +class SubprocessRouteTable(BaseRouteTable): + """ + A route table to bind http route to SubprocessModuleHandle and SubprocessModule. + + This class is used in cls object: all the decorator methods are @classmethod, and + the routes are binded to the cls object. + + Note we have 2 handlers: + 1. the child side handler, that is `handler` that contains real logic and + is executed in the child process. It's added with __route_method__ and + __route_path__ attributes. The child process runs a standalone aiohttp + server. + 2. the parent side handler, that just sends the request to the + SubprocessModuleHandle at cls._bind_map[method][path].instance. + + With modifications: + - __route_method__ and __route_path__ are added to both side's handlers. + - method and path are added to self._bind_map. + + Lifecycle of a request: + 1. Parent receives an aiohttp request. + 2. Router finds by [method][path] and calls parent_side_handler. + 3. `parent_side_handler` bookkeeps the request with a Future and sends a + request to the subprocess. + 4. Subprocesses receives the response and sends it back to the parent. + 5. Parent responds to the aiohttp request with the response from the subprocess. + """ + + _bind_map = collections.defaultdict(dict) + _routes = aiohttp.web.RouteTableDef() + + @classmethod + def bind(cls, instance: "SubprocessModuleHandle"): + # __route_method__ and __route_path__ are added to SubprocessModule's methods, + # not the SubprocessModuleHandle's methods. + def predicate(o): + if inspect.isfunction(o): + return hasattr(o, "__route_method__") and hasattr(o, "__route_path__") + return False + + handler_routes = inspect.getmembers(instance.module_cls, predicate) + for _, h in handler_routes: + cls._bind_map[h.__route_method__][h.__route_path__].instance = instance + + @classmethod + def _register_route( + cls, method, path, resp_type: ResponseType = ResponseType.HTTP, **kwargs + ): + """ + Register a route to the module and return the decorated handler. + """ + + def _wrapper(handler): + if path in cls._bind_map[method]: + bind_info = cls._bind_map[method][path] + raise Exception( + f"Duplicated route path: {path}, " + f"previous one registered at " + f"{bind_info.filename}:{bind_info.lineno}" + ) + + bind_info = cls._BindInfo( + handler.__code__.co_filename, handler.__code__.co_firstlineno, None + ) + + cls._bind_map[method][path] = bind_info + + async def parent_side_handler( + request: aiohttp.web.Request, + ) -> aiohttp.web.Response: + bind_info = cls._bind_map[method][path] + subprocess_module_handle = bind_info.instance + return await subprocess_module_handle.proxy_request(request, resp_type) + + # Used in bind(). + handler.__route_method__ = method + handler.__route_path__ = path + # Used in bound_routes(). + parent_side_handler.__route_method__ = method + parent_side_handler.__route_path__ = path + parent_side_handler.__name__ = handler.__name__ + + cls._routes.route(method, path)(parent_side_handler) + + return handler + + return _wrapper diff --git a/lib/python3.12/site-packages/ray/dashboard/subprocesses/utils.py b/lib/python3.12/site-packages/ray/dashboard/subprocesses/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..5d52a95ee097ad0551ccf3fdd8a00497f20afff1 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/subprocesses/utils.py @@ -0,0 +1,67 @@ +import enum +import os +import sys +from typing import TypeVar + +import aiohttp + +from ray._private.utils import validate_socket_filepath + +K = TypeVar("K") +V = TypeVar("V") + + +class ResponseType(enum.Enum): + HTTP = "http" + STREAM = "stream" + WEBSOCKET = "websocket" + + +def module_logging_filename( + module_name: str, logging_filename: str, extension: str = "" +) -> str: + """ + Parse logging_filename = STEM EXTENSION, + return STEM _ MODULE_NAME _ EXTENSION + + If logging_filename is empty, return empty string. + If extension is empty, use the extension from logging_filename. + + Example: + module_name = "TestModule" + logging_filename = "dashboard.log" + STEM = "dashboard" + EXTENSION = ".log" + return "dashboard_TestModule.log" + """ + if not logging_filename: + return "" + stem, ext = os.path.splitext(logging_filename) + if not extension: + extension = ext + return f"{stem}_{module_name}{extension}" + + +def get_socket_path(socket_dir: str, module_name: str) -> str: + socket_path = os.path.join(socket_dir, "dash_" + module_name) + validate_socket_filepath(socket_path) + return socket_path + + +def get_named_pipe_path(module_name: str, session_name: str) -> str: + return r"\\.\pipe\dash_" + module_name + "_" + session_name + + +def get_http_session_to_module( + module_name: str, socket_dir: str, session_name: str +) -> aiohttp.ClientSession: + """ + Get the aiohttp http client session to the subprocess module. + """ + if sys.platform == "win32": + named_pipe_path = get_named_pipe_path(module_name, session_name) + connector = aiohttp.NamedPipeConnector(named_pipe_path) + else: + socket_path = get_socket_path(socket_dir, module_name) + connector = aiohttp.UnixConnector(socket_path) + return aiohttp.ClientSession(connector=connector) diff --git a/lib/python3.12/site-packages/ray/dashboard/timezone_utils.py b/lib/python3.12/site-packages/ray/dashboard/timezone_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..6a0d68b9c1a9989bc62682dd09b7eccaf51fc335 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/timezone_utils.py @@ -0,0 +1,56 @@ +import logging +from datetime import datetime + +logger = logging.getLogger(__name__) + +timezones = [ + {"offset": "-12:00", "value": "Etc/+12"}, + {"offset": "-11:00", "value": "Pacific/Pago_Pago"}, + {"offset": "-10:00", "value": "Pacific/Honolulu"}, + {"offset": "-09:00", "value": "America/Anchorage"}, + {"offset": "-08:00", "value": "America/Los_Angeles"}, + {"offset": "-07:00", "value": "America/Phoenix"}, + {"offset": "-06:00", "value": "America/Guatemala"}, + {"offset": "-05:00", "value": "America/Bogota"}, + {"offset": "-04:00", "value": "America/Halifax"}, + {"offset": "-03:30", "value": "America/St_Johns"}, + {"offset": "-03:00", "value": "America/Sao_Paulo"}, + {"offset": "-02:00", "value": "America/Godthab"}, + {"offset": "-01:00", "value": "Atlantic/Azores"}, + {"offset": "+00:00", "value": "Europe/London"}, + {"offset": "+01:00", "value": "Europe/Amsterdam"}, + {"offset": "+02:00", "value": "Asia/Amman"}, + {"offset": "+03:00", "value": "Asia/Baghdad"}, + {"offset": "+03:30", "value": "Asia/Tehran"}, + {"offset": "+04:00", "value": "Asia/Dubai"}, + {"offset": "+04:30", "value": "Asia/Kabul"}, + {"offset": "+05:00", "value": "Asia/Karachi"}, + {"offset": "+05:30", "value": "Asia/Kolkata"}, + {"offset": "+05:45", "value": "Asia/Kathmandu"}, + {"offset": "+06:00", "value": "Asia/Almaty"}, + {"offset": "+06:30", "value": "Asia/Yangon"}, + {"offset": "+07:00", "value": "Asia/Bangkok"}, + {"offset": "+08:00", "value": "Asia/Shanghai"}, + {"offset": "+09:00", "value": "Asia/Irkutsk"}, + {"offset": "+09:30", "value": "Australia/Adelaide"}, + {"offset": "+10:00", "value": "Australia/Brisbane"}, + {"offset": "+11:00", "value": "Asia/Magadan"}, + {"offset": "+12:00", "value": "Pacific/Auckland"}, + {"offset": "+13:00", "value": "Pacific/Tongatapu"}, +] + + +def get_current_timezone_info(): + current_tz = datetime.now().astimezone().tzinfo + offset = current_tz.utcoffset(None) + hours, remainder = divmod(offset.total_seconds(), 3600) + minutes = remainder // 60 + sign = "+" if hours >= 0 else "-" + current_offset = f"{sign}{abs(int(hours)):02d}:{abs(int(minutes)):02d}" + + current_timezone = next( + (tz for tz in timezones if tz["offset"] == current_offset), + {"offset": None, "value": None}, + ) + + return current_timezone diff --git a/lib/python3.12/site-packages/ray/dashboard/utils.py b/lib/python3.12/site-packages/ray/dashboard/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f146a682c89c0d0fab0950d85b687e39b7131d19 --- /dev/null +++ b/lib/python3.12/site-packages/ray/dashboard/utils.py @@ -0,0 +1,770 @@ +import abc +import asyncio +import datetime +import functools +import importlib +import json +import logging +import os +import pkgutil +from abc import ABCMeta, abstractmethod +from base64 import b64decode +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from enum import IntEnum +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +from ray._common.utils import binary_to_hex + +if TYPE_CHECKING: + from ray.core.generated.node_manager_pb2 import GetNodeStatsReply + +from packaging.version import Version + +import ray +import ray._private.protobuf_compat +import ray._private.ray_constants as ray_constants +import ray._private.services as services +import ray.experimental.internal_kv as internal_kv +from ray._common.network_utils import parse_address +from ray._common.utils import get_or_create_event_loop +from ray._private.gcs_utils import GcsChannel +from ray._private.utils import ( + get_dashboard_dependency_error, + split_address, +) +from ray._raylet import GcsClient + +try: + create_task = asyncio.create_task +except AttributeError: + create_task = asyncio.ensure_future + +logger = logging.getLogger(__name__) + + +class HTTPStatusCode(IntEnum): + # 2xx Success + OK = 200 + + # 4xx Client Errors + BAD_REQUEST = 400 + NOT_FOUND = 404 + TOO_MANY_REQUESTS = 429 + + # 5xx Server Errors + INTERNAL_ERROR = 500 + + +class FrontendNotFoundError(OSError): + pass + + +class DashboardAgentModule(abc.ABC): + def __init__(self, dashboard_agent): + """ + Initialize current module when DashboardAgent loading modules. + :param dashboard_agent: The DashboardAgent instance. + """ + self._dashboard_agent = dashboard_agent + self.session_name = dashboard_agent.session_name + + @abc.abstractmethod + async def run(self, server): + """ + Run the module in an asyncio loop. An agent module can provide + servicers to the server. + :param server: Asyncio GRPC server, or None if ray is minimal. + """ + + @staticmethod + @abc.abstractclassmethod + def is_minimal_module(): + """ + Return True if the module is minimal, meaning it + should work with `pip install ray` that doesn't requires additional + dependencies. + """ + + @property + def gcs_address(self): + return self._dashboard_agent.gcs_address + + +@dataclass +class DashboardHeadModuleConfig: + minimal: bool + cluster_id_hex: str + session_name: str + gcs_address: str + log_dir: str + temp_dir: str + session_dir: str + ip: str + http_host: str + http_port: int + + +class DashboardHeadModule(abc.ABC): + def __init__(self, config: DashboardHeadModuleConfig): + """ + Initialize current module when DashboardHead loading modules. + :param config: The DashboardHeadModuleConfig instance. + """ + self._config = config + self._gcs_client = None + self._aiogrpc_gcs_channel = None # lazy init + self._http_session = None # lazy init + + @property + def minimal(self): + return self._config.minimal + + @property + def session_name(self): + return self._config.session_name + + @property + def gcs_address(self): + return self._config.gcs_address + + @property + def log_dir(self): + return self._config.log_dir + + @property + def temp_dir(self): + return self._config.temp_dir + + @property + def session_dir(self): + return self._config.session_dir + + @property + def ip(self): + return self._config.ip + + @property + def http_host(self): + return self._config.http_host + + @property + def http_port(self): + return self._config.http_port + + @property + def http_session(self): + assert not self._config.minimal, "http_session accessed in minimal Ray." + import aiohttp + + if self._http_session is not None: + return self._http_session + # Create a http session for all modules. + # aiohttp<4.0.0 uses a 'loop' variable, aiohttp>=4.0.0 doesn't anymore + if Version(aiohttp.__version__) < Version("4.0.0"): + self._http_session = aiohttp.ClientSession(loop=get_or_create_event_loop()) + else: + self._http_session = aiohttp.ClientSession() + return self._http_session + + @property + def gcs_client(self): + if self._gcs_client is None: + self._gcs_client = GcsClient( + address=self._config.gcs_address, + cluster_id=self._config.cluster_id_hex, + ) + if not internal_kv._internal_kv_initialized(): + internal_kv._initialize_internal_kv(self._gcs_client) + return self._gcs_client + + @property + def aiogrpc_gcs_channel(self): + # TODO(ryw): once we removed the old gcs client, also remove this. + if self._config.minimal: + return None + if self._aiogrpc_gcs_channel is None: + gcs_channel = GcsChannel(gcs_address=self._config.gcs_address, aio=True) + gcs_channel.connect() + self._aiogrpc_gcs_channel = gcs_channel.channel() + return self._aiogrpc_gcs_channel + + @abc.abstractmethod + async def run(self): + """ + Run the module in an asyncio loop. A head module can provide + servicers to the server. + """ + + @staticmethod + @abc.abstractclassmethod + def is_minimal_module(): + """ + Return True if the module is minimal, meaning it + should work with `pip install ray` that doesn't requires additional + dependencies. + """ + + +class RateLimitedModule(abc.ABC): + """Simple rate limiter + + Inheriting from this class and decorate any class methods will + apply simple rate limit. + It will limit the maximal number of concurrent invocations of **all** the + methods decorated. + + The below Example class will only allow 10 concurrent calls to A() and B() + + E.g.: + + class Example(RateLimitedModule): + def __init__(self): + super().__init__(max_num_call=10) + + @RateLimitedModule.enforce_max_concurrent_calls + async def A(): + ... + + @RateLimitedModule.enforce_max_concurrent_calls + async def B(): + ... + + async def limit_handler_(self): + raise RuntimeError("rate limited reached!") + + """ + + def __init__(self, max_num_call: int, logger: Optional[logging.Logger] = None): + """ + Args: + max_num_call: Maximal number of concurrent invocations of all decorated + functions in the instance. + Setting to -1 will disable rate limiting. + + logger: Logger + """ + self.max_num_call_ = max_num_call + self.num_call_ = 0 + self.logger_ = logger + + @staticmethod + def enforce_max_concurrent_calls(func): + """Decorator to enforce max number of invocations of the decorated func + + NOTE: This should be used as the innermost decorator if there are multiple + ones. + + E.g., when decorating functions already with @routes.get(...), this must be + added below then the routes decorators: + ``` + @routes.get('/') + @RateLimitedModule.enforce_max_concurrent_calls + async def fn(self): + ... + + ``` + """ + + @functools.wraps(func) + async def async_wrapper(self, *args, **kwargs): + if self.max_num_call_ >= 0 and self.num_call_ >= self.max_num_call_: + if self.logger_: + self.logger_.warning( + f"Max concurrent requests reached={self.max_num_call_}" + ) + return await self.limit_handler_() + self.num_call_ += 1 + try: + ret = await func(self, *args, **kwargs) + finally: + self.num_call_ -= 1 + return ret + + # Returning closure here to avoid passing 'self' to the + # 'enforce_max_concurrent_calls' decorator. + return async_wrapper + + @abstractmethod + async def limit_handler_(self): + """Handler that is invoked when max number of concurrent calls reached""" + + +def dashboard_module(enable): + """A decorator for dashboard module.""" + + def _cls_wrapper(cls): + cls.__ray_dashboard_module_enable__ = enable + return cls + + return _cls_wrapper + + +def get_all_modules(module_type): + """ + Get all importable modules that are subclass of a given module type. + """ + logger.info(f"Get all modules by type: {module_type.__name__}") + import ray.dashboard.modules + + should_only_load_minimal_modules = get_dashboard_dependency_error() is not None + + for module_loader, name, ispkg in pkgutil.walk_packages( + ray.dashboard.modules.__path__, ray.dashboard.modules.__name__ + "." + ): + try: + importlib.import_module(name) + except ModuleNotFoundError as e: + logger.info( + f"Module {name} cannot be loaded because " + "we cannot import all dependencies. Install this module using " + "`pip install 'ray[default]'` for the full " + f"dashboard functionality. Error: {e}" + ) + if not should_only_load_minimal_modules: + logger.info( + "Although `pip install 'ray[default]'` is downloaded, " + "module couldn't be imported`" + ) + raise e + + imported_modules = [] + # module_type.__subclasses__() should contain modules that + # we could successfully import. + for m in module_type.__subclasses__(): + if not getattr(m, "__ray_dashboard_module_enable__", True): + continue + if should_only_load_minimal_modules and not m.is_minimal_module(): + continue + imported_modules.append(m) + logger.info(f"Available modules: {imported_modules}") + return imported_modules + + +def to_posix_time(dt): + return (dt - datetime.datetime(1970, 1, 1)).total_seconds() + + +def address_tuple(address): + if isinstance(address, tuple): + return address + ip, port = parse_address(address) + return ip, int(port) + + +def node_stats_to_dict( + message: "GetNodeStatsReply", +) -> Optional[Dict[str, List[Dict[str, Any]]]]: + decode_keys = { + "actorId", + "jobId", + "taskId", + "parentTaskId", + "sourceActorId", + "callerId", + "nodeId", + "workerId", + "placementGroupId", + } + core_workers_stats = message.core_workers_stats + result = message_to_dict(message, decode_keys) + result["coreWorkersStats"] = [ + message_to_dict(m, decode_keys, always_print_fields_with_no_presence=True) + for m in core_workers_stats + ] + return result + + +class CustomEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, bytes): + return binary_to_hex(obj) + if isinstance(obj, Immutable): + return obj.mutable() + # Let the base class default method raise the TypeError + return json.JSONEncoder.default(self, obj) + + +def to_camel_case(snake_str): + """Convert a snake str to camel case.""" + components = snake_str.split("_") + # We capitalize the first letter of each component except the first one + # with the 'title' method and join them together. + return components[0] + "".join(x.title() for x in components[1:]) + + +def to_google_style(d): + """Recursive convert all keys in dict to google style.""" + new_dict = {} + + for k, v in d.items(): + if isinstance(v, dict): + new_dict[to_camel_case(k)] = to_google_style(v) + elif isinstance(v, list): + new_list = [] + for i in v: + if isinstance(i, dict): + new_list.append(to_google_style(i)) + else: + new_list.append(i) + new_dict[to_camel_case(k)] = new_list + else: + new_dict[to_camel_case(k)] = v + return new_dict + + +def message_to_dict(message, decode_keys=None, **kwargs): + """Convert protobuf message to Python dict.""" + + def _decode_keys(d): + for k, v in d.items(): + if isinstance(v, dict): + d[k] = _decode_keys(v) + if isinstance(v, list): + new_list = [] + for i in v: + if isinstance(i, dict): + new_list.append(_decode_keys(i)) + else: + new_list.append(i) + d[k] = new_list + else: + if k in decode_keys: + d[k] = binary_to_hex(b64decode(v)) + else: + d[k] = v + return d + + d = ray._private.protobuf_compat.message_to_dict( + message, use_integers_for_enums=False, **kwargs + ) + if decode_keys: + return _decode_keys(d) + else: + return d + + +class Bunch(dict): + """A dict with attribute-access.""" + + def __getattr__(self, key): + try: + return self.__getitem__(key) + except KeyError: + raise AttributeError(key) + + def __setattr__(self, key, value): + self.__setitem__(key, value) + + +""" +https://docs.python.org/3/library/json.html?highlight=json#json.JSONEncoder + +-------------------+---------------+ + | Python | JSON | + +===================+===============+ + | dict | object | + +-------------------+---------------+ + | list, tuple | array | + +-------------------+---------------+ + | str | string | + +-------------------+---------------+ + | int, float | number | + +-------------------+---------------+ + | True | true | + +-------------------+---------------+ + | False | false | + +-------------------+---------------+ + | None | null | + +-------------------+---------------+ +""" +_json_compatible_types = {dict, list, tuple, str, int, float, bool, type(None), bytes} + + +def is_immutable(self): + raise TypeError("%r objects are immutable" % self.__class__.__name__) + + +def make_immutable(value, strict=True): + value_type = type(value) + if value_type is dict: + return ImmutableDict(value) + if value_type is list: + return ImmutableList(value) + if strict: + if value_type not in _json_compatible_types: + raise TypeError("Type {} can't be immutable.".format(value_type)) + return value + + +class Immutable(metaclass=ABCMeta): + @abstractmethod + def mutable(self): + pass + + +class ImmutableList(Immutable, Sequence): + """Makes a :class:`list` immutable.""" + + __slots__ = ("_list", "_proxy") + + def __init__(self, list_value): + if type(list_value) not in (list, ImmutableList): + raise TypeError(f"{type(list_value)} object is not a list.") + if isinstance(list_value, ImmutableList): + list_value = list_value.mutable() + self._list = list_value + self._proxy = [None] * len(list_value) + + def __reduce_ex__(self, protocol): + return type(self), (self._list,) + + def mutable(self): + return self._list + + def __eq__(self, other): + if isinstance(other, ImmutableList): + other = other.mutable() + return list.__eq__(self._list, other) + + def __ne__(self, other): + if isinstance(other, ImmutableList): + other = other.mutable() + return list.__ne__(self._list, other) + + def __contains__(self, item): + if isinstance(item, Immutable): + item = item.mutable() + return list.__contains__(self._list, item) + + def __getitem__(self, item): + proxy = self._proxy[item] + if proxy is None: + proxy = self._proxy[item] = make_immutable(self._list[item]) + return proxy + + def __len__(self): + return len(self._list) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, list.__repr__(self._list)) + + +class ImmutableDict(Immutable, Mapping): + """Makes a :class:`dict` immutable.""" + + __slots__ = ("_dict", "_proxy") + + def __init__(self, dict_value): + if type(dict_value) not in (dict, ImmutableDict): + raise TypeError(f"{type(dict_value)} object is not a dict.") + if isinstance(dict_value, ImmutableDict): + dict_value = dict_value.mutable() + self._dict = dict_value + self._proxy = {} + + def __reduce_ex__(self, protocol): + return type(self), (self._dict,) + + def mutable(self): + return self._dict + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return make_immutable(default) + + def __eq__(self, other): + if isinstance(other, ImmutableDict): + other = other.mutable() + return dict.__eq__(self._dict, other) + + def __ne__(self, other): + if isinstance(other, ImmutableDict): + other = other.mutable() + return dict.__ne__(self._dict, other) + + def __contains__(self, item): + if isinstance(item, Immutable): + item = item.mutable() + return dict.__contains__(self._dict, item) + + def __getitem__(self, item): + proxy = self._proxy.get(item, None) + if proxy is None: + proxy = self._proxy[item] = make_immutable(self._dict[item]) + return proxy + + def __len__(self) -> int: + return len(self._dict) + + def __iter__(self): + if len(self._proxy) != len(self._dict): + for key in self._dict.keys() - self._proxy.keys(): + self._proxy[key] = make_immutable(self._dict[key]) + return iter(self._proxy) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, dict.__repr__(self._dict)) + + +# Register immutable types. +for immutable_type in Immutable.__subclasses__(): + _json_compatible_types.add(immutable_type) + + +def async_loop_forever(interval_seconds, cancellable=False): + def _wrapper(coro): + @functools.wraps(coro) + async def _looper(*args, **kwargs): + while True: + try: + await coro(*args, **kwargs) + except asyncio.CancelledError as ex: + if cancellable: + logger.info( + f"An async loop forever coroutine " f"is cancelled {coro}." + ) + raise ex + else: + logger.exception( + f"Can not cancel the async loop " + f"forever coroutine {coro}." + ) + except Exception: + logger.exception(f"Error looping coroutine {coro}.") + await asyncio.sleep(interval_seconds) + + return _looper + + return _wrapper + + +def ray_client_address_to_api_server_url(address: str): + """Convert a Ray Client address of a running Ray cluster to its API server URL. + + Args: + address: The Ray Client address, e.g. "ray://my-cluster". + + Returns: + str: The API server URL of the cluster, e.g. "http://:8265". + """ + with ray.init(address=address) as client_context: + dashboard_url = client_context.dashboard_url + + return f"http://{dashboard_url}" + + +def ray_address_to_api_server_url(address: Optional[str]) -> str: + """Parse a Ray cluster address into API server URL. + + When an address is provided, it will be used to query GCS for + API server address from GCS, so a Ray cluster must be running. + + When an address is not provided, it will first try to auto-detect + a running Ray instance, or look for local GCS process. + + Args: + address: Ray cluster bootstrap address or Ray Client address. + Could also be `auto`. + + Returns: + API server HTTP URL. + """ + + address = services.canonicalize_bootstrap_address_or_die(address) + gcs_client = GcsClient(address=address) + + ray.experimental.internal_kv._initialize_internal_kv(gcs_client) + api_server_url = ray._private.utils.internal_kv_get_with_retry( + gcs_client, + ray_constants.DASHBOARD_ADDRESS, + namespace=ray_constants.KV_NAMESPACE_DASHBOARD, + num_retries=20, + ) + + if api_server_url is None: + raise ValueError( + ( + "Couldn't obtain the API server address from GCS. It is likely that " + "the GCS server is down. Check gcs_server.[out | err] to see if it is " + "still alive." + ) + ) + api_server_url = f"http://{api_server_url.decode()}" + return api_server_url + + +def get_address_for_submission_client(address: Optional[str]) -> str: + """Get Ray API server address from Ray bootstrap or Client address. + + If None, it will try to auto-detect a running Ray instance, or look + for local GCS process. + + `address` is always overridden by the RAY_ADDRESS environment + variable, just like the `address` argument in `ray.init()`. + + Args: + address: Ray cluster bootstrap address or Ray Client address. + Could also be "auto". + + Returns: + API server HTTP URL, e.g. "http://:8265". + """ + if api_server_address := os.environ.get( + ray_constants.RAY_API_SERVER_ADDRESS_ENVIRONMENT_VARIABLE + ): + address = api_server_address + logger.debug(f"Using RAY_API_SERVER_ADDRESS={address}") + # Fall back to RAY_ADDRESS if RAY_API_SERVER_ADDRESS not set + elif ray_address := os.environ.get(ray_constants.RAY_ADDRESS_ENVIRONMENT_VARIABLE): + address = ray_address + logger.debug(f"Using RAY_ADDRESS={address}") + + if address and "://" in address: + module_string, _ = split_address(address) + if module_string == "ray": + logger.debug( + f"Retrieving API server address from Ray Client address {address}..." + ) + address = ray_client_address_to_api_server_url(address) + else: + # User specified a non-Ray-Client Ray cluster address. + address = ray_address_to_api_server_url(address) + logger.debug(f"Using API server address {address}.") + return address + + +def compose_state_message( + death_reason: Optional[str], death_reason_message: Optional[str] +) -> Optional[str]: + """Compose node state message based on death information. + + Args: + death_reason: The reason of node death. + This is a string representation of `gcs_pb2.NodeDeathInfo.Reason`. + death_reason_message: The message of node death. + This corresponds to `gcs_pb2.NodeDeathInfo.ReasonMessage`. + """ + if death_reason == "EXPECTED_TERMINATION": + state_message = "Expected termination" + elif death_reason == "UNEXPECTED_TERMINATION": + state_message = "Unexpected termination" + elif death_reason == "AUTOSCALER_DRAIN_PREEMPTED": + state_message = "Terminated due to preemption" + elif death_reason == "AUTOSCALER_DRAIN_IDLE": + state_message = "Terminated due to idle (no Ray activity)" + else: + state_message = None + + if death_reason_message: + if state_message: + state_message += f": {death_reason_message}" + else: + state_message = death_reason_message + return state_message + + +def close_logger_file_descriptor(logger_instance): + for handler in logger_instance.handlers: + handler.close() diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44aa961c38b2aca34215c283c1ce5724f9d3fa9d Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/callbacks.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/callbacks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..429a6a5c4f4e360f98f8a481f8d105d2edf4f637 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/callbacks.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/mock.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/mock.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..721d7e31ed3410d37db27968623c770d2261c03d Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/mock.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/registry.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/registry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b8eb149dfb2141eaaf33d37e50061b2e3bb8182 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/registry.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f2d87135fa1d67dd2873f3262c847ce44f780a1c Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ac3749f7a57f8c3cd82cb03ae61b8cefc4131b8d --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__init__.py @@ -0,0 +1,6 @@ +from ray.rllib.algorithms.bc.bc import BC, BCConfig + +__all__ = [ + "BC", + "BCConfig", +] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b39d77483f2e5bc1ecf482f6e696ebc29914570 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__pycache__/bc.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__pycache__/bc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..614193c6894a212d6d78b12479ec1e376617dac1 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__pycache__/bc.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__pycache__/bc_catalog.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__pycache__/bc_catalog.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f13bf30b5ae6b7e052cc8baab9727153943b0c15 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/__pycache__/bc_catalog.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/bc/bc.py b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/bc.py new file mode 100644 index 0000000000000000000000000000000000000000..e2bc7dc64e6ceed1f5d8b15cc0b75ccea3042781 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/bc.py @@ -0,0 +1,120 @@ +from ray.rllib.algorithms.algorithm_config import AlgorithmConfig +from ray.rllib.algorithms.marwil.marwil import MARWIL, MARWILConfig +from ray.rllib.core.rl_module.rl_module import RLModuleSpec +from ray.rllib.utils.annotations import override +from ray.rllib.utils.typing import RLModuleSpecType + + +class BCConfig(MARWILConfig): + """Defines a configuration class from which a new BC Algorithm can be built + + .. testcode:: + :skipif: True + + from ray.rllib.algorithms.bc import BCConfig + # Run this from the ray directory root. + config = BCConfig().training(lr=0.00001, gamma=0.99) + config = config.offline_data( + input_="./rllib/tests/data/cartpole/large.json") + + # Build an Algorithm object from the config and run 1 training iteration. + algo = config.build() + algo.train() + + .. testcode:: + :skipif: True + + from ray.rllib.algorithms.bc import BCConfig + from ray import tune + config = BCConfig() + # Print out some default values. + print(config.beta) + # Update the config object. + config.training( + lr=tune.grid_search([0.001, 0.0001]), beta=0.75 + ) + # Set the config object's data path. + # Run this from the ray directory root. + config.offline_data( + input_="./rllib/tests/data/cartpole/large.json" + ) + # Set the config object's env, used for evaluation. + config.environment(env="CartPole-v1") + # Use to_dict() to get the old-style python config dict + # when running with tune. + tune.Tuner( + "BC", + param_space=config.to_dict(), + ).fit() + """ + + def __init__(self, algo_class=None): + super().__init__(algo_class=algo_class or BC) + + # fmt: off + # __sphinx_doc_begin__ + # No need to calculate advantages (or do anything else with the rewards). + self.beta = 0.0 + # Advantages (calculated during postprocessing) + # not important for behavioral cloning. + self.postprocess_inputs = False + + # Materialize only the mapped data. This is optimal as long + # as no connector in the connector pipeline holds a state. + self.materialize_data = False + self.materialize_mapped_data = True + # __sphinx_doc_end__ + # fmt: on + + @override(AlgorithmConfig) + def get_default_rl_module_spec(self) -> RLModuleSpecType: + if self.framework_str == "torch": + from ray.rllib.algorithms.bc.torch.default_bc_torch_rl_module import ( + DefaultBCTorchRLModule, + ) + + return RLModuleSpec(module_class=DefaultBCTorchRLModule) + else: + raise ValueError( + f"The framework {self.framework_str} is not supported. " + "Use `torch` instead." + ) + + @override(AlgorithmConfig) + def build_learner_connector( + self, + input_observation_space, + input_action_space, + device=None, + ): + pipeline = super().build_learner_connector( + input_observation_space=input_observation_space, + input_action_space=input_action_space, + device=device, + ) + + # Remove unneeded connectors from the MARWIL connector pipeline. + pipeline.remove("AddOneTsToEpisodesAndTruncate") + pipeline.remove("GeneralAdvantageEstimation") + + return pipeline + + @override(MARWILConfig) + def validate(self) -> None: + # Call super's validation method. + super().validate() + + if self.beta != 0.0: + self._value_error("For behavioral cloning, `beta` parameter must be 0.0!") + + +class BC(MARWIL): + """Behavioral Cloning (derived from MARWIL). + + Uses MARWIL with beta force-set to 0.0. + """ + + @classmethod + @override(MARWIL) + def get_default_config(cls) -> BCConfig: + return BCConfig() diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/bc/bc_catalog.py b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/bc_catalog.py new file mode 100644 index 0000000000000000000000000000000000000000..54a01ddd649cf0a9a1a9460097427f1e26adba5d --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/bc_catalog.py @@ -0,0 +1,112 @@ +# __sphinx_doc_begin__ +import gymnasium as gym + +from ray.rllib.algorithms.ppo.ppo_catalog import _check_if_diag_gaussian +from ray.rllib.core.models.base import Model +from ray.rllib.core.models.catalog import Catalog +from ray.rllib.core.models.configs import FreeLogStdMLPHeadConfig, MLPHeadConfig +from ray.rllib.utils.annotations import OverrideToImplementCustomLogic + + +class BCCatalog(Catalog): + """The Catalog class used to build models for BC. + + BCCatalog provides the following models: + - Encoder: The encoder used to encode the observations. + - Pi Head: The head used for the policy logits. + + The default encoder is chosen by RLlib dependent on the observation space. + See `ray.rllib.core.models.encoders::Encoder` for details. To define the + network architecture use the `model_config_dict[fcnet_hiddens]` and + `model_config_dict[fcnet_activation]`. + + To implement custom logic, override `BCCatalog.build_encoder()` or modify the + `EncoderConfig` at `BCCatalog.encoder_config`. + + Any custom head can be built by overriding the `build_pi_head()` method. + Alternatively, the `PiHeadConfig` can be overridden to build a custom + policy head during runtime. To change solely the network architecture, + `model_config_dict["head_fcnet_hiddens"]` and + `model_config_dict["head_fcnet_activation"]` can be used. + """ + + def __init__( + self, + observation_space: gym.Space, + action_space: gym.Space, + model_config_dict: dict, + ): + """Initializes the BCCatalog. + + Args: + observation_space: The observation space if the Encoder. + action_space: The action space for the Pi Head. + model_cnfig_dict: The model config to use.. + """ + super().__init__( + observation_space=observation_space, + action_space=action_space, + model_config_dict=model_config_dict, + ) + + self.pi_head_hiddens = self._model_config_dict["head_fcnet_hiddens"] + self.pi_head_activation = self._model_config_dict["head_fcnet_activation"] + + # At this time we do not have the precise (framework-specific) action + # distribution class, i.e. we do not know the output dimension of the + # policy head. The config for the policy head is therefore build in the + # `self.build_pi_head()` method. + self.pi_head_config = None + + @OverrideToImplementCustomLogic + def build_pi_head(self, framework: str) -> Model: + """Builds the policy head. + + The default behavior is to build the head from the pi_head_config. + This can be overridden to build a custom policy head as a means of configuring + the behavior of a BC specific RLModule implementation. + + Args: + framework: The framework to use. Either "torch" or "tf2". + + Returns: + The policy head. + """ + + # Define the output dimension via the action distribution. + action_distribution_cls = self.get_action_dist_cls(framework=framework) + if self._model_config_dict["free_log_std"]: + _check_if_diag_gaussian( + action_distribution_cls=action_distribution_cls, framework=framework + ) + is_diag_gaussian = True + else: + is_diag_gaussian = _check_if_diag_gaussian( + action_distribution_cls=action_distribution_cls, + framework=framework, + no_error=True, + ) + required_output_dim = action_distribution_cls.required_input_dim( + space=self.action_space, model_config=self._model_config_dict + ) + # With the action distribution class and the number of outputs defined, + # we can build the config for the policy head. + pi_head_config_cls = ( + FreeLogStdMLPHeadConfig + if self._model_config_dict["free_log_std"] + else MLPHeadConfig + ) + self.pi_head_config = pi_head_config_cls( + input_dims=self._latent_dims, + hidden_layer_dims=self.pi_head_hiddens, + hidden_layer_activation=self.pi_head_activation, + output_layer_dim=required_output_dim, + output_layer_activation="linear", + clip_log_std=is_diag_gaussian, + log_std_clip_param=self._model_config_dict.get("log_std_clip_param", 20), + ) + + return self.pi_head_config.build(framework=framework) + + +# __sphinx_doc_end__ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f9dcd9c01a3f44d830d3221e3a9bb87b985bebc Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/__pycache__/default_bc_torch_rl_module.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/__pycache__/default_bc_torch_rl_module.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c1c588ebfc4722089789214c8f10a9be3040c9c Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/__pycache__/default_bc_torch_rl_module.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/default_bc_torch_rl_module.py b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/default_bc_torch_rl_module.py new file mode 100644 index 0000000000000000000000000000000000000000..03087d1c883deed1451ce729451fbcae6da73601 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/bc/torch/default_bc_torch_rl_module.py @@ -0,0 +1,45 @@ +import abc +from typing import Any, Dict + +from ray.rllib.algorithms.bc.bc_catalog import BCCatalog +from ray.rllib.core.columns import Columns +from ray.rllib.core.models.base import ENCODER_OUT +from ray.rllib.core.rl_module.rl_module import RLModule +from ray.rllib.core.rl_module.torch.torch_rl_module import TorchRLModule +from ray.rllib.utils.annotations import override +from ray.util.annotations import DeveloperAPI + + +@DeveloperAPI +class DefaultBCTorchRLModule(TorchRLModule, abc.ABC): + """The default TorchRLModule used, if no custom RLModule is provided. + + Builds an encoder net based on the observation space. + Builds a pi head based on the action space. + + Passes observations from the input batch through the encoder, then the pi head to + compute action logits. + """ + + def __init__(self, *args, **kwargs): + catalog_class = kwargs.pop("catalog_class", None) + if catalog_class is None: + catalog_class = BCCatalog + super().__init__(*args, **kwargs, catalog_class=catalog_class) + + @override(RLModule) + def setup(self): + # Build model components (encoder and pi head) from catalog. + super().setup() + self._encoder = self.catalog.build_encoder(framework=self.framework) + self._pi_head = self.catalog.build_pi_head(framework=self.framework) + + @override(TorchRLModule) + def _forward(self, batch: Dict, **kwargs) -> Dict[str, Any]: + """Generic BC forward pass (for all phases of training/evaluation).""" + # Encoder embeddings. + encoder_outs = self._encoder(batch) + # Action dist inputs. + return { + Columns.ACTION_DIST_INPUTS: self._pi_head(encoder_outs[ENCODER_OUT]), + } diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d42d80881e08a491ea72f39338eaba8da9f1eec3 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__init__.py @@ -0,0 +1,9 @@ +from ray.rllib.algorithms.cql.cql import CQL, CQLConfig +from ray.rllib.algorithms.cql.cql_torch_policy import CQLTorchPolicy + +__all__ = [ + "CQL", + "CQLConfig", + # @OldAPIStack + "CQLTorchPolicy", +] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7eb73818002dd82571fdfeedd5ea5a001cfceec8 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/cql.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/cql.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..901eaa88af132289bf877082d7191270d22427c6 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/cql.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/cql_tf_policy.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/cql_tf_policy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c2437b376533b20a4c136561f3507720d793e65 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/cql_tf_policy.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/cql_torch_policy.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/cql_torch_policy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c3194e83a4373abe2e520cc90e0387985f32e9f Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/__pycache__/cql_torch_policy.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/cql.py b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/cql.py new file mode 100644 index 0000000000000000000000000000000000000000..681f5210c6dc50de464f8d17ea663cfb738c8bf1 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/cql.py @@ -0,0 +1,378 @@ +import logging +from typing import Optional, Type, Union + +from typing_extensions import Self + +from ray._common.deprecation import ( + DEPRECATED_VALUE, + deprecation_warning, +) +from ray.rllib.algorithms.algorithm_config import AlgorithmConfig, NotProvided +from ray.rllib.algorithms.cql.cql_tf_policy import CQLTFPolicy +from ray.rllib.algorithms.cql.cql_torch_policy import CQLTorchPolicy +from ray.rllib.algorithms.sac.sac import ( + SAC, + SACConfig, +) +from ray.rllib.connectors.common.add_observations_from_episodes_to_batch import ( + AddObservationsFromEpisodesToBatch, +) +from ray.rllib.connectors.learner.add_next_observations_from_episodes_to_train_batch import ( # noqa + AddNextObservationsFromEpisodesToTrainBatch, +) +from ray.rllib.core.learner.learner import Learner +from ray.rllib.core.rl_module.rl_module import RLModuleSpec +from ray.rllib.execution.rollout_ops import ( + synchronous_parallel_sample, +) +from ray.rllib.execution.train_ops import ( + multi_gpu_train_one_step, + train_one_step, +) +from ray.rllib.policy.policy import Policy +from ray.rllib.utils.annotations import OldAPIStack, override +from ray.rllib.utils.framework import try_import_tf, try_import_tfp +from ray.rllib.utils.metrics import ( + LAST_TARGET_UPDATE_TS, + LEARNER_RESULTS, + LEARNER_UPDATE_TIMER, + NUM_AGENT_STEPS_SAMPLED, + NUM_AGENT_STEPS_TRAINED, + NUM_ENV_STEPS_SAMPLED, + NUM_ENV_STEPS_TRAINED, + NUM_TARGET_UPDATES, + OFFLINE_SAMPLING_TIMER, + SAMPLE_TIMER, + SYNCH_WORKER_WEIGHTS_TIMER, + TARGET_NET_UPDATE_TIMER, + TIMERS, +) +from ray.rllib.utils.typing import ResultDict, RLModuleSpecType + +tf1, tf, tfv = try_import_tf() +tfp = try_import_tfp() +logger = logging.getLogger(__name__) + + +class CQLConfig(SACConfig): + """Defines a configuration class from which a CQL can be built. + + .. testcode:: + :skipif: True + + from ray.rllib.algorithms.cql import CQLConfig + config = CQLConfig().training(gamma=0.9, lr=0.01) + config = config.resources(num_gpus=0) + config = config.env_runners(num_env_runners=4) + print(config.to_dict()) + # Build a Algorithm object from the config and run 1 training iteration. + algo = config.build(env="CartPole-v1") + algo.train() + """ + + def __init__(self, algo_class=None): + super().__init__(algo_class=algo_class or CQL) + + # fmt: off + # __sphinx_doc_begin__ + # CQL-specific config settings: + self.bc_iters = 20000 + self.temperature = 1.0 + self.num_actions = 10 + self.lagrangian = False + self.lagrangian_thresh = 5.0 + self.min_q_weight = 5.0 + self.deterministic_backup = True + self.lr = 3e-4 + # Note, the new stack defines learning rates for each component. + # The base learning rate `lr` has to be set to `None`, if using + # the new stack. + self.actor_lr = 1e-4 + self.critic_lr = 1e-3 + self.alpha_lr = 1e-3 + + self.replay_buffer_config = { + "_enable_replay_buffer_api": True, + "type": "MultiAgentPrioritizedReplayBuffer", + "capacity": int(1e6), + # If True prioritized replay buffer will be used. + "prioritized_replay": False, + "prioritized_replay_alpha": 0.6, + "prioritized_replay_beta": 0.4, + "prioritized_replay_eps": 1e-6, + # Whether to compute priorities already on the remote worker side. + "worker_side_prioritization": False, + } + + # Changes to Algorithm's/SACConfig's default: + + # .reporting() + self.min_sample_timesteps_per_iteration = 0 + self.min_train_timesteps_per_iteration = 100 + # fmt: on + # __sphinx_doc_end__ + + self.timesteps_per_iteration = DEPRECATED_VALUE + + @override(SACConfig) + def training( + self, + *, + bc_iters: Optional[int] = NotProvided, + temperature: Optional[float] = NotProvided, + num_actions: Optional[int] = NotProvided, + lagrangian: Optional[bool] = NotProvided, + lagrangian_thresh: Optional[float] = NotProvided, + min_q_weight: Optional[float] = NotProvided, + deterministic_backup: Optional[bool] = NotProvided, + **kwargs, + ) -> Self: + """Sets the training-related configuration. + + Args: + bc_iters: Number of iterations with Behavior Cloning pretraining. + temperature: CQL loss temperature. + num_actions: Number of actions to sample for CQL loss + lagrangian: Whether to use the Lagrangian for Alpha Prime (in CQL loss). + lagrangian_thresh: Lagrangian threshold. + min_q_weight: in Q weight multiplier. + deterministic_backup: If the target in the Bellman update should have an + entropy backup. Defaults to `True`. + + Returns: + This updated AlgorithmConfig object. + """ + # Pass kwargs onto super's `training()` method. + super().training(**kwargs) + + if bc_iters is not NotProvided: + self.bc_iters = bc_iters + if temperature is not NotProvided: + self.temperature = temperature + if num_actions is not NotProvided: + self.num_actions = num_actions + if lagrangian is not NotProvided: + self.lagrangian = lagrangian + if lagrangian_thresh is not NotProvided: + self.lagrangian_thresh = lagrangian_thresh + if min_q_weight is not NotProvided: + self.min_q_weight = min_q_weight + if deterministic_backup is not NotProvided: + self.deterministic_backup = deterministic_backup + + return self + + @override(AlgorithmConfig) + def offline_data(self, **kwargs) -> Self: + + super().offline_data(**kwargs) + + # Check, if the passed in class incorporates the `OfflinePreLearner` + # interface. + if "prelearner_class" in kwargs: + from ray.rllib.offline.offline_data import OfflinePreLearner + + if not issubclass(kwargs.get("prelearner_class"), OfflinePreLearner): + raise ValueError( + f"`prelearner_class` {kwargs.get('prelearner_class')} is not a " + "subclass of `OfflinePreLearner`. Any class passed to " + "`prelearner_class` needs to implement the interface given by " + "`OfflinePreLearner`." + ) + + return self + + @override(SACConfig) + def get_default_learner_class(self) -> Union[Type["Learner"], str]: + if self.framework_str == "torch": + from ray.rllib.algorithms.cql.torch.cql_torch_learner import CQLTorchLearner + + return CQLTorchLearner + else: + raise ValueError( + f"The framework {self.framework_str} is not supported. " + "Use `'torch'` instead." + ) + + @override(AlgorithmConfig) + def build_learner_connector( + self, + input_observation_space, + input_action_space, + device=None, + ): + pipeline = super().build_learner_connector( + input_observation_space=input_observation_space, + input_action_space=input_action_space, + device=device, + ) + + # Prepend the "add-NEXT_OBS-from-episodes-to-train-batch" connector piece (right + # after the corresponding "add-OBS-..." default piece). + pipeline.insert_after( + AddObservationsFromEpisodesToBatch, + AddNextObservationsFromEpisodesToTrainBatch(), + ) + + return pipeline + + @override(SACConfig) + def validate(self) -> None: + # First check, whether old `timesteps_per_iteration` is used. + if self.timesteps_per_iteration != DEPRECATED_VALUE: + deprecation_warning( + old="timesteps_per_iteration", + new="min_train_timesteps_per_iteration", + error=True, + ) + + # Call super's validation method. + super().validate() + + # CQL-torch performs the optimizer steps inside the loss function. + # Using the multi-GPU optimizer will therefore not work (see multi-GPU + # check above) and we must use the simple optimizer for now. + if self.simple_optimizer is not True and self.framework_str == "torch": + self.simple_optimizer = True + + if self.framework_str in ["tf", "tf2"] and tfp is None: + logger.warning( + "You need `tensorflow_probability` in order to run CQL! " + "Install it via `pip install tensorflow_probability`. Your " + f"tf.__version__={tf.__version__ if tf else None}." + "Trying to import tfp results in the following error:" + ) + try_import_tfp(error=True) + + # Assert that for a local learner the number of iterations is 1. Note, + # this is needed because we have no iterators, but instead a single + # batch returned directly from the `OfflineData.sample` method. + if ( + self.num_learners == 0 + and not self.dataset_num_iters_per_learner + and self.enable_rl_module_and_learner + ): + self._value_error( + "When using a single local learner the number of iterations " + "per learner, `dataset_num_iters_per_learner` has to be defined. " + "Set this hyperparameter in the `AlgorithmConfig.offline_data`." + ) + + @override(SACConfig) + def get_default_rl_module_spec(self) -> RLModuleSpecType: + if self.framework_str == "torch": + from ray.rllib.algorithms.cql.torch.default_cql_torch_rl_module import ( + DefaultCQLTorchRLModule, + ) + + return RLModuleSpec(module_class=DefaultCQLTorchRLModule) + else: + raise ValueError( + f"The framework {self.framework_str} is not supported. Use `torch`." + ) + + @property + def _model_config_auto_includes(self): + return super()._model_config_auto_includes | { + "num_actions": self.num_actions, + } + + +class CQL(SAC): + """CQL (derived from SAC).""" + + @classmethod + @override(SAC) + def get_default_config(cls) -> CQLConfig: + return CQLConfig() + + @classmethod + @override(SAC) + def get_default_policy_class( + cls, config: AlgorithmConfig + ) -> Optional[Type[Policy]]: + if config["framework"] == "torch": + return CQLTorchPolicy + else: + return CQLTFPolicy + + @override(SAC) + def training_step(self) -> None: + # Old API stack (Policy, RolloutWorker, Connector). + if not self.config.enable_env_runner_and_connector_v2: + return self._training_step_old_api_stack() + + # Sampling from offline data. + with self.metrics.log_time((TIMERS, OFFLINE_SAMPLING_TIMER)): + # If we should use an iterator in the learner(s). Note, in case of + # multiple learners we must always return a list of iterators. + return_iterator = return_iterator = ( + self.config.num_learners > 0 + or self.config.dataset_num_iters_per_learner != 1 + ) + + # Return an iterator in case we are using remote learners. + batch_or_iterator = self.offline_data.sample( + num_samples=self.config.train_batch_size_per_learner, + num_shards=self.config.num_learners, + # Return an iterator, if a `Learner` should update + # multiple times per RLlib iteration. + return_iterator=return_iterator, + ) + + # Updating the policy. + with self.metrics.log_time((TIMERS, LEARNER_UPDATE_TIMER)): + learner_results = self.learner_group.update( + data_iterators=batch_or_iterator, + minibatch_size=self.config.train_batch_size_per_learner, + num_iters=self.config.dataset_num_iters_per_learner, + ) + + # Log training results. + self.metrics.aggregate(learner_results, key=LEARNER_RESULTS) + + @OldAPIStack + def _training_step_old_api_stack(self) -> ResultDict: + # Collect SampleBatches from sample workers. + with self._timers[SAMPLE_TIMER]: + train_batch = synchronous_parallel_sample(worker_set=self.env_runner_group) + train_batch = train_batch.as_multi_agent() + self._counters[NUM_AGENT_STEPS_SAMPLED] += train_batch.agent_steps() + self._counters[NUM_ENV_STEPS_SAMPLED] += train_batch.env_steps() + + # Postprocess batch before we learn on it. + post_fn = self.config.get("before_learn_on_batch") or (lambda b, *a: b) + train_batch = post_fn(train_batch, self.env_runner_group, self.config) + + # Learn on training batch. + # Use simple optimizer (only for multi-agent or tf-eager; all other + # cases should use the multi-GPU optimizer, even if only using 1 GPU) + if self.config.get("simple_optimizer") is True: + train_results = train_one_step(self, train_batch) + else: + train_results = multi_gpu_train_one_step(self, train_batch) + + # Update target network every `target_network_update_freq` training steps. + cur_ts = self._counters[ + NUM_AGENT_STEPS_TRAINED + if self.config.count_steps_by == "agent_steps" + else NUM_ENV_STEPS_TRAINED + ] + last_update = self._counters[LAST_TARGET_UPDATE_TS] + if cur_ts - last_update >= self.config.target_network_update_freq: + with self._timers[TARGET_NET_UPDATE_TIMER]: + to_update = self.env_runner.get_policies_to_train() + self.env_runner.foreach_policy_to_train( + lambda p, pid: pid in to_update and p.update_target() + ) + self._counters[NUM_TARGET_UPDATES] += 1 + self._counters[LAST_TARGET_UPDATE_TS] = cur_ts + + # Update remote workers's weights after learning on local worker + # (only those policies that were actually trained). + if self.env_runner_group.num_remote_workers() > 0: + with self._timers[SYNCH_WORKER_WEIGHTS_TIMER]: + self.env_runner_group.sync_weights(policies=list(train_results.keys())) + + # Return all collected metrics for the iteration. + return train_results diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/cql_tf_policy.py b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/cql_tf_policy.py new file mode 100644 index 0000000000000000000000000000000000000000..ae6c4f8d4fef2f1776f4183729158faf83f56182 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/cql_tf_policy.py @@ -0,0 +1,426 @@ +""" +TensorFlow policy class used for CQL. +""" +import logging +from functools import partial +from typing import Dict, List, Type, Union + +import gymnasium as gym +import numpy as np +import tree + +import ray +from ray.rllib.algorithms.sac.sac_tf_policy import ( + ActorCriticOptimizerMixin as SACActorCriticOptimizerMixin, + ComputeTDErrorMixin, + _get_dist_class, + apply_gradients as sac_apply_gradients, + build_sac_model, + compute_and_clip_gradients as sac_compute_and_clip_gradients, + get_distribution_inputs_and_class, + postprocess_trajectory, + setup_late_mixins, + stats, + validate_spaces, +) +from ray.rllib.models.modelv2 import ModelV2 +from ray.rllib.models.tf.tf_action_dist import TFActionDistribution +from ray.rllib.policy.policy import Policy +from ray.rllib.policy.sample_batch import SampleBatch +from ray.rllib.policy.tf_mixins import TargetNetworkMixin +from ray.rllib.policy.tf_policy_template import build_tf_policy +from ray.rllib.utils.exploration.random import Random +from ray.rllib.utils.framework import get_variable, try_import_tf, try_import_tfp +from ray.rllib.utils.typing import ( + AlgorithmConfigDict, + LocalOptimizer, + ModelGradients, + TensorType, +) + +tf1, tf, tfv = try_import_tf() +tfp = try_import_tfp() + +logger = logging.getLogger(__name__) + +MEAN_MIN = -9.0 +MEAN_MAX = 9.0 + + +def _repeat_tensor(t: TensorType, n: int): + # Insert new axis at position 1 into tensor t + t_rep = tf.expand_dims(t, 1) + # Repeat tensor t_rep along new axis n times + multiples = tf.concat([[1, n], tf.tile([1], tf.expand_dims(tf.rank(t) - 1, 0))], 0) + t_rep = tf.tile(t_rep, multiples) + # Merge new axis into batch axis + t_rep = tf.reshape(t_rep, tf.concat([[-1], tf.shape(t)[1:]], 0)) + return t_rep + + +# Returns policy tiled actions and log probabilities for CQL Loss +def policy_actions_repeat(model, action_dist, obs, num_repeat=1): + batch_size = tf.shape(tree.flatten(obs)[0])[0] + obs_temp = tree.map_structure(lambda t: _repeat_tensor(t, num_repeat), obs) + logits, _ = model.get_action_model_outputs(obs_temp) + policy_dist = action_dist(logits, model) + actions, logp_ = policy_dist.sample_logp() + logp = tf.expand_dims(logp_, -1) + return actions, tf.reshape(logp, [batch_size, num_repeat, 1]) + + +def q_values_repeat(model, obs, actions, twin=False): + action_shape = tf.shape(actions)[0] + obs_shape = tf.shape(tree.flatten(obs)[0])[0] + num_repeat = action_shape // obs_shape + obs_temp = tree.map_structure(lambda t: _repeat_tensor(t, num_repeat), obs) + if not twin: + preds_, _ = model.get_q_values(obs_temp, actions) + else: + preds_, _ = model.get_twin_q_values(obs_temp, actions) + preds = tf.reshape(preds_, [obs_shape, num_repeat, 1]) + return preds + + +def cql_loss( + policy: Policy, + model: ModelV2, + dist_class: Type[TFActionDistribution], + train_batch: SampleBatch, +) -> Union[TensorType, List[TensorType]]: + logger.info(f"Current iteration = {policy.cur_iter}") + policy.cur_iter += 1 + + # For best performance, turn deterministic off + deterministic = policy.config["_deterministic_loss"] + assert not deterministic + twin_q = policy.config["twin_q"] + discount = policy.config["gamma"] + + # CQL Parameters + bc_iters = policy.config["bc_iters"] + cql_temp = policy.config["temperature"] + num_actions = policy.config["num_actions"] + min_q_weight = policy.config["min_q_weight"] + use_lagrange = policy.config["lagrangian"] + target_action_gap = policy.config["lagrangian_thresh"] + + obs = train_batch[SampleBatch.CUR_OBS] + actions = tf.cast(train_batch[SampleBatch.ACTIONS], tf.float32) + rewards = tf.cast(train_batch[SampleBatch.REWARDS], tf.float32) + next_obs = train_batch[SampleBatch.NEXT_OBS] + terminals = train_batch[SampleBatch.TERMINATEDS] + + model_out_t, _ = model(SampleBatch(obs=obs, _is_training=True), [], None) + + model_out_tp1, _ = model(SampleBatch(obs=next_obs, _is_training=True), [], None) + + target_model_out_tp1, _ = policy.target_model( + SampleBatch(obs=next_obs, _is_training=True), [], None + ) + + action_dist_class = _get_dist_class(policy, policy.config, policy.action_space) + action_dist_inputs_t, _ = model.get_action_model_outputs(model_out_t) + action_dist_t = action_dist_class(action_dist_inputs_t, model) + policy_t, log_pis_t = action_dist_t.sample_logp() + log_pis_t = tf.expand_dims(log_pis_t, -1) + + # Unlike original SAC, Alpha and Actor Loss are computed first. + # Alpha Loss + alpha_loss = -tf.reduce_mean( + model.log_alpha * tf.stop_gradient(log_pis_t + model.target_entropy) + ) + + # Policy Loss (Either Behavior Clone Loss or SAC Loss) + alpha = tf.math.exp(model.log_alpha) + if policy.cur_iter >= bc_iters: + min_q, _ = model.get_q_values(model_out_t, policy_t) + if twin_q: + twin_q_, _ = model.get_twin_q_values(model_out_t, policy_t) + min_q = tf.math.minimum(min_q, twin_q_) + actor_loss = tf.reduce_mean(tf.stop_gradient(alpha) * log_pis_t - min_q) + else: + bc_logp = action_dist_t.logp(actions) + actor_loss = tf.reduce_mean(tf.stop_gradient(alpha) * log_pis_t - bc_logp) + # actor_loss = -tf.reduce_mean(bc_logp) + + # Critic Loss (Standard SAC Critic L2 Loss + CQL Entropy Loss) + # SAC Loss: + # Q-values for the batched actions. + action_dist_inputs_tp1, _ = model.get_action_model_outputs(model_out_tp1) + action_dist_tp1 = action_dist_class(action_dist_inputs_tp1, model) + policy_tp1, _ = action_dist_tp1.sample_logp() + + q_t, _ = model.get_q_values(model_out_t, actions) + q_t_selected = tf.squeeze(q_t, axis=-1) + if twin_q: + twin_q_t, _ = model.get_twin_q_values(model_out_t, actions) + twin_q_t_selected = tf.squeeze(twin_q_t, axis=-1) + + # Target q network evaluation. + q_tp1, _ = policy.target_model.get_q_values(target_model_out_tp1, policy_tp1) + if twin_q: + twin_q_tp1, _ = policy.target_model.get_twin_q_values( + target_model_out_tp1, policy_tp1 + ) + # Take min over both twin-NNs. + q_tp1 = tf.math.minimum(q_tp1, twin_q_tp1) + + q_tp1_best = tf.squeeze(input=q_tp1, axis=-1) + q_tp1_best_masked = (1.0 - tf.cast(terminals, tf.float32)) * q_tp1_best + + # compute RHS of bellman equation + q_t_target = tf.stop_gradient( + rewards + (discount ** policy.config["n_step"]) * q_tp1_best_masked + ) + + # Compute the TD-error (potentially clipped), for priority replay buffer + base_td_error = tf.math.abs(q_t_selected - q_t_target) + if twin_q: + twin_td_error = tf.math.abs(twin_q_t_selected - q_t_target) + td_error = 0.5 * (base_td_error + twin_td_error) + else: + td_error = base_td_error + + critic_loss_1 = tf.keras.losses.MSE(q_t_selected, q_t_target) + if twin_q: + critic_loss_2 = tf.keras.losses.MSE(twin_q_t_selected, q_t_target) + + # CQL Loss (We are using Entropy version of CQL (the best version)) + rand_actions, _ = policy._random_action_generator.get_exploration_action( + action_distribution=action_dist_class( + tf.tile(action_dist_tp1.inputs, (num_actions, 1)), model + ), + timestep=0, + explore=True, + ) + curr_actions, curr_logp = policy_actions_repeat( + model, action_dist_class, model_out_t, num_actions + ) + next_actions, next_logp = policy_actions_repeat( + model, action_dist_class, model_out_tp1, num_actions + ) + + q1_rand = q_values_repeat(model, model_out_t, rand_actions) + q1_curr_actions = q_values_repeat(model, model_out_t, curr_actions) + q1_next_actions = q_values_repeat(model, model_out_t, next_actions) + + if twin_q: + q2_rand = q_values_repeat(model, model_out_t, rand_actions, twin=True) + q2_curr_actions = q_values_repeat(model, model_out_t, curr_actions, twin=True) + q2_next_actions = q_values_repeat(model, model_out_t, next_actions, twin=True) + + random_density = np.log(0.5 ** int(curr_actions.shape[-1])) + cat_q1 = tf.concat( + [ + q1_rand - random_density, + q1_next_actions - tf.stop_gradient(next_logp), + q1_curr_actions - tf.stop_gradient(curr_logp), + ], + 1, + ) + if twin_q: + cat_q2 = tf.concat( + [ + q2_rand - random_density, + q2_next_actions - tf.stop_gradient(next_logp), + q2_curr_actions - tf.stop_gradient(curr_logp), + ], + 1, + ) + + min_qf1_loss_ = ( + tf.reduce_mean(tf.reduce_logsumexp(cat_q1 / cql_temp, axis=1)) + * min_q_weight + * cql_temp + ) + min_qf1_loss = min_qf1_loss_ - (tf.reduce_mean(q_t) * min_q_weight) + if twin_q: + min_qf2_loss_ = ( + tf.reduce_mean(tf.reduce_logsumexp(cat_q2 / cql_temp, axis=1)) + * min_q_weight + * cql_temp + ) + min_qf2_loss = min_qf2_loss_ - (tf.reduce_mean(twin_q_t) * min_q_weight) + + if use_lagrange: + alpha_prime = tf.clip_by_value(model.log_alpha_prime.exp(), 0.0, 1000000.0)[0] + min_qf1_loss = alpha_prime * (min_qf1_loss - target_action_gap) + if twin_q: + min_qf2_loss = alpha_prime * (min_qf2_loss - target_action_gap) + alpha_prime_loss = 0.5 * (-min_qf1_loss - min_qf2_loss) + else: + alpha_prime_loss = -min_qf1_loss + + cql_loss = [min_qf1_loss] + if twin_q: + cql_loss.append(min_qf2_loss) + + critic_loss = [critic_loss_1 + min_qf1_loss] + if twin_q: + critic_loss.append(critic_loss_2 + min_qf2_loss) + + # Save for stats function. + policy.q_t = q_t_selected + policy.policy_t = policy_t + policy.log_pis_t = log_pis_t + policy.td_error = td_error + policy.actor_loss = actor_loss + policy.critic_loss = critic_loss + policy.alpha_loss = alpha_loss + policy.log_alpha_value = model.log_alpha + policy.alpha_value = alpha + policy.target_entropy = model.target_entropy + # CQL Stats + policy.cql_loss = cql_loss + if use_lagrange: + policy.log_alpha_prime_value = model.log_alpha_prime[0] + policy.alpha_prime_value = alpha_prime + policy.alpha_prime_loss = alpha_prime_loss + + # Return all loss terms corresponding to our optimizers. + if use_lagrange: + return actor_loss + tf.math.add_n(critic_loss) + alpha_loss + alpha_prime_loss + return actor_loss + tf.math.add_n(critic_loss) + alpha_loss + + +def cql_stats(policy: Policy, train_batch: SampleBatch) -> Dict[str, TensorType]: + sac_dict = stats(policy, train_batch) + sac_dict["cql_loss"] = tf.reduce_mean(tf.stack(policy.cql_loss)) + if policy.config["lagrangian"]: + sac_dict["log_alpha_prime_value"] = policy.log_alpha_prime_value + sac_dict["alpha_prime_value"] = policy.alpha_prime_value + sac_dict["alpha_prime_loss"] = policy.alpha_prime_loss + return sac_dict + + +class ActorCriticOptimizerMixin(SACActorCriticOptimizerMixin): + def __init__(self, config): + super().__init__(config) + if config["lagrangian"]: + # Eager mode. + if config["framework"] == "tf2": + self._alpha_prime_optimizer = tf.keras.optimizers.Adam( + learning_rate=config["optimization"]["critic_learning_rate"] + ) + # Static graph mode. + else: + self._alpha_prime_optimizer = tf1.train.AdamOptimizer( + learning_rate=config["optimization"]["critic_learning_rate"] + ) + + +def setup_early_mixins( + policy: Policy, + obs_space: gym.spaces.Space, + action_space: gym.spaces.Space, + config: AlgorithmConfigDict, +) -> None: + """Call mixin classes' constructors before Policy's initialization. + + Adds the necessary optimizers to the given Policy. + + Args: + policy: The Policy object. + obs_space (gym.spaces.Space): The Policy's observation space. + action_space (gym.spaces.Space): The Policy's action space. + config: The Policy's config. + """ + policy.cur_iter = 0 + ActorCriticOptimizerMixin.__init__(policy, config) + if config["lagrangian"]: + policy.model.log_alpha_prime = get_variable( + 0.0, framework="tf", trainable=True, tf_name="log_alpha_prime" + ) + policy.alpha_prime_optim = tf.keras.optimizers.Adam( + learning_rate=config["optimization"]["critic_learning_rate"], + ) + # Generic random action generator for calculating CQL-loss. + policy._random_action_generator = Random( + action_space, + model=None, + framework="tf2", + policy_config=config, + num_workers=0, + worker_index=0, + ) + + +def compute_gradients_fn( + policy: Policy, optimizer: LocalOptimizer, loss: TensorType +) -> ModelGradients: + grads_and_vars = sac_compute_and_clip_gradients(policy, optimizer, loss) + + if policy.config["lagrangian"]: + # Eager: Use GradientTape (which is a property of the `optimizer` + # object (an OptimizerWrapper): see rllib/policy/eager_tf_policy.py). + if policy.config["framework"] == "tf2": + tape = optimizer.tape + log_alpha_prime = [policy.model.log_alpha_prime] + alpha_prime_grads_and_vars = list( + zip( + tape.gradient(policy.alpha_prime_loss, log_alpha_prime), + log_alpha_prime, + ) + ) + # Tf1.x: Use optimizer.compute_gradients() + else: + alpha_prime_grads_and_vars = ( + policy._alpha_prime_optimizer.compute_gradients( + policy.alpha_prime_loss, var_list=[policy.model.log_alpha_prime] + ) + ) + + # Clip if necessary. + if policy.config["grad_clip"]: + clip_func = partial(tf.clip_by_norm, clip_norm=policy.config["grad_clip"]) + else: + clip_func = tf.identity + + # Save grads and vars for later use in `build_apply_op`. + policy._alpha_prime_grads_and_vars = [ + (clip_func(g), v) for (g, v) in alpha_prime_grads_and_vars if g is not None + ] + + grads_and_vars += policy._alpha_prime_grads_and_vars + return grads_and_vars + + +def apply_gradients_fn(policy, optimizer, grads_and_vars): + sac_results = sac_apply_gradients(policy, optimizer, grads_and_vars) + + if policy.config["lagrangian"]: + # Eager mode -> Just apply and return None. + if policy.config["framework"] == "tf2": + policy._alpha_prime_optimizer.apply_gradients( + policy._alpha_prime_grads_and_vars + ) + return + # Tf static graph -> Return grouped op. + else: + alpha_prime_apply_op = policy._alpha_prime_optimizer.apply_gradients( + policy._alpha_prime_grads_and_vars, + global_step=tf1.train.get_or_create_global_step(), + ) + return tf.group([sac_results, alpha_prime_apply_op]) + return sac_results + + +# Build a child class of `TFPolicy`, given the custom functions defined +# above. +CQLTFPolicy = build_tf_policy( + name="CQLTFPolicy", + loss_fn=cql_loss, + get_default_config=lambda: ray.rllib.algorithms.cql.cql.CQLConfig(), + validate_spaces=validate_spaces, + stats_fn=cql_stats, + postprocess_fn=postprocess_trajectory, + before_init=setup_early_mixins, + after_init=setup_late_mixins, + make_model=build_sac_model, + extra_learn_fetches_fn=lambda policy: {"td_error": policy.td_error}, + mixins=[ActorCriticOptimizerMixin, TargetNetworkMixin, ComputeTDErrorMixin], + action_distribution_fn=get_distribution_inputs_and_class, + compute_gradients_fn=compute_gradients_fn, + apply_gradients_fn=apply_gradients_fn, +) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/cql_torch_policy.py b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/cql_torch_policy.py new file mode 100644 index 0000000000000000000000000000000000000000..a7fab43bda61cdc5063daa3475e70f7bdbe12866 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/cql_torch_policy.py @@ -0,0 +1,406 @@ +""" +PyTorch policy class used for CQL. +""" +import logging +from typing import Dict, List, Tuple, Type, Union + +import gymnasium as gym +import numpy as np +import tree + +import ray +from ray.rllib.algorithms.sac.sac_tf_policy import ( + postprocess_trajectory, + validate_spaces, +) +from ray.rllib.algorithms.sac.sac_torch_policy import ( + ComputeTDErrorMixin, + _get_dist_class, + action_distribution_fn, + build_sac_model_and_action_dist, + optimizer_fn, + setup_late_mixins, + stats, +) +from ray.rllib.models.modelv2 import ModelV2 +from ray.rllib.models.torch.torch_action_dist import TorchDistributionWrapper +from ray.rllib.policy.policy import Policy +from ray.rllib.policy.policy_template import build_policy_class +from ray.rllib.policy.sample_batch import SampleBatch +from ray.rllib.policy.torch_mixins import TargetNetworkMixin +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.metrics.learner_info import LEARNER_STATS_KEY +from ray.rllib.utils.torch_utils import ( + apply_grad_clipping, + concat_multi_gpu_td_errors, + convert_to_torch_tensor, +) +from ray.rllib.utils.typing import AlgorithmConfigDict, LocalOptimizer, TensorType + +torch, nn = try_import_torch() +F = nn.functional + +logger = logging.getLogger(__name__) + +MEAN_MIN = -9.0 +MEAN_MAX = 9.0 + + +def _repeat_tensor(t: TensorType, n: int): + # Insert new dimension at posotion 1 into tensor t + t_rep = t.unsqueeze(1) + # Repeat tensor t_rep along new dimension n times + t_rep = torch.repeat_interleave(t_rep, n, dim=1) + # Merge new dimension into batch dimension + t_rep = t_rep.view(-1, *t.shape[1:]) + return t_rep + + +# Returns policy tiled actions and log probabilities for CQL Loss +def policy_actions_repeat(model, action_dist, obs, num_repeat=1): + batch_size = tree.flatten(obs)[0].shape[0] + obs_temp = tree.map_structure(lambda t: _repeat_tensor(t, num_repeat), obs) + logits, _ = model.get_action_model_outputs(obs_temp) + policy_dist = action_dist(logits, model) + actions, logp_ = policy_dist.sample_logp() + logp = logp_.unsqueeze(-1) + return actions, logp.view(batch_size, num_repeat, 1) + + +def q_values_repeat(model, obs, actions, twin=False): + action_shape = actions.shape[0] + obs_shape = tree.flatten(obs)[0].shape[0] + num_repeat = int(action_shape / obs_shape) + obs_temp = tree.map_structure(lambda t: _repeat_tensor(t, num_repeat), obs) + if not twin: + preds_, _ = model.get_q_values(obs_temp, actions) + else: + preds_, _ = model.get_twin_q_values(obs_temp, actions) + preds = preds_.view(obs_shape, num_repeat, 1) + return preds + + +def cql_loss( + policy: Policy, + model: ModelV2, + dist_class: Type[TorchDistributionWrapper], + train_batch: SampleBatch, +) -> Union[TensorType, List[TensorType]]: + logger.info(f"Current iteration = {policy.cur_iter}") + policy.cur_iter += 1 + + # Look up the target model (tower) using the model tower. + target_model = policy.target_models[model] + + # For best performance, turn deterministic off + deterministic = policy.config["_deterministic_loss"] + assert not deterministic + twin_q = policy.config["twin_q"] + discount = policy.config["gamma"] + action_low = model.action_space.low[0] + action_high = model.action_space.high[0] + + # CQL Parameters + bc_iters = policy.config["bc_iters"] + cql_temp = policy.config["temperature"] + num_actions = policy.config["num_actions"] + min_q_weight = policy.config["min_q_weight"] + use_lagrange = policy.config["lagrangian"] + target_action_gap = policy.config["lagrangian_thresh"] + + obs = train_batch[SampleBatch.CUR_OBS] + actions = train_batch[SampleBatch.ACTIONS] + rewards = train_batch[SampleBatch.REWARDS].float() + next_obs = train_batch[SampleBatch.NEXT_OBS] + terminals = train_batch[SampleBatch.TERMINATEDS] + + model_out_t, _ = model(SampleBatch(obs=obs, _is_training=True), [], None) + + model_out_tp1, _ = model(SampleBatch(obs=next_obs, _is_training=True), [], None) + + target_model_out_tp1, _ = target_model( + SampleBatch(obs=next_obs, _is_training=True), [], None + ) + + action_dist_class = _get_dist_class(policy, policy.config, policy.action_space) + action_dist_inputs_t, _ = model.get_action_model_outputs(model_out_t) + action_dist_t = action_dist_class(action_dist_inputs_t, model) + policy_t, log_pis_t = action_dist_t.sample_logp() + log_pis_t = torch.unsqueeze(log_pis_t, -1) + + # Unlike original SAC, Alpha and Actor Loss are computed first. + # Alpha Loss + alpha_loss = -(model.log_alpha * (log_pis_t + model.target_entropy).detach()).mean() + + batch_size = tree.flatten(obs)[0].shape[0] + if batch_size == policy.config["train_batch_size"]: + policy.alpha_optim.zero_grad() + alpha_loss.backward() + policy.alpha_optim.step() + + # Policy Loss (Either Behavior Clone Loss or SAC Loss) + alpha = torch.exp(model.log_alpha) + if policy.cur_iter >= bc_iters: + min_q, _ = model.get_q_values(model_out_t, policy_t) + if twin_q: + twin_q_, _ = model.get_twin_q_values(model_out_t, policy_t) + min_q = torch.min(min_q, twin_q_) + actor_loss = (alpha.detach() * log_pis_t - min_q).mean() + else: + bc_logp = action_dist_t.logp(actions) + actor_loss = (alpha.detach() * log_pis_t - bc_logp).mean() + # actor_loss = -bc_logp.mean() + + if batch_size == policy.config["train_batch_size"]: + policy.actor_optim.zero_grad() + actor_loss.backward(retain_graph=True) + policy.actor_optim.step() + + # Critic Loss (Standard SAC Critic L2 Loss + CQL Entropy Loss) + # SAC Loss: + # Q-values for the batched actions. + action_dist_inputs_tp1, _ = model.get_action_model_outputs(model_out_tp1) + action_dist_tp1 = action_dist_class(action_dist_inputs_tp1, model) + policy_tp1, _ = action_dist_tp1.sample_logp() + + q_t, _ = model.get_q_values(model_out_t, train_batch[SampleBatch.ACTIONS]) + q_t_selected = torch.squeeze(q_t, dim=-1) + if twin_q: + twin_q_t, _ = model.get_twin_q_values( + model_out_t, train_batch[SampleBatch.ACTIONS] + ) + twin_q_t_selected = torch.squeeze(twin_q_t, dim=-1) + + # Target q network evaluation. + q_tp1, _ = target_model.get_q_values(target_model_out_tp1, policy_tp1) + if twin_q: + twin_q_tp1, _ = target_model.get_twin_q_values(target_model_out_tp1, policy_tp1) + # Take min over both twin-NNs. + q_tp1 = torch.min(q_tp1, twin_q_tp1) + + q_tp1_best = torch.squeeze(input=q_tp1, dim=-1) + q_tp1_best_masked = (1.0 - terminals.float()) * q_tp1_best + + # compute RHS of bellman equation + q_t_target = ( + rewards + (discount ** policy.config["n_step"]) * q_tp1_best_masked + ).detach() + + # Compute the TD-error (potentially clipped), for priority replay buffer + base_td_error = torch.abs(q_t_selected - q_t_target) + if twin_q: + twin_td_error = torch.abs(twin_q_t_selected - q_t_target) + td_error = 0.5 * (base_td_error + twin_td_error) + else: + td_error = base_td_error + + critic_loss_1 = nn.functional.mse_loss(q_t_selected, q_t_target) + if twin_q: + critic_loss_2 = nn.functional.mse_loss(twin_q_t_selected, q_t_target) + + # CQL Loss (We are using Entropy version of CQL (the best version)) + rand_actions = convert_to_torch_tensor( + torch.FloatTensor(actions.shape[0] * num_actions, actions.shape[-1]).uniform_( + action_low, action_high + ), + policy.device, + ) + curr_actions, curr_logp = policy_actions_repeat( + model, action_dist_class, model_out_t, num_actions + ) + next_actions, next_logp = policy_actions_repeat( + model, action_dist_class, model_out_tp1, num_actions + ) + + q1_rand = q_values_repeat(model, model_out_t, rand_actions) + q1_curr_actions = q_values_repeat(model, model_out_t, curr_actions) + q1_next_actions = q_values_repeat(model, model_out_t, next_actions) + + if twin_q: + q2_rand = q_values_repeat(model, model_out_t, rand_actions, twin=True) + q2_curr_actions = q_values_repeat(model, model_out_t, curr_actions, twin=True) + q2_next_actions = q_values_repeat(model, model_out_t, next_actions, twin=True) + + random_density = np.log(0.5 ** curr_actions.shape[-1]) + cat_q1 = torch.cat( + [ + q1_rand - random_density, + q1_next_actions - next_logp.detach(), + q1_curr_actions - curr_logp.detach(), + ], + 1, + ) + if twin_q: + cat_q2 = torch.cat( + [ + q2_rand - random_density, + q2_next_actions - next_logp.detach(), + q2_curr_actions - curr_logp.detach(), + ], + 1, + ) + + min_qf1_loss_ = ( + torch.logsumexp(cat_q1 / cql_temp, dim=1).mean() * min_q_weight * cql_temp + ) + min_qf1_loss = min_qf1_loss_ - (q_t.mean() * min_q_weight) + if twin_q: + min_qf2_loss_ = ( + torch.logsumexp(cat_q2 / cql_temp, dim=1).mean() * min_q_weight * cql_temp + ) + min_qf2_loss = min_qf2_loss_ - (twin_q_t.mean() * min_q_weight) + + if use_lagrange: + alpha_prime = torch.clamp(model.log_alpha_prime.exp(), min=0.0, max=1000000.0)[ + 0 + ] + min_qf1_loss = alpha_prime * (min_qf1_loss - target_action_gap) + if twin_q: + min_qf2_loss = alpha_prime * (min_qf2_loss - target_action_gap) + alpha_prime_loss = 0.5 * (-min_qf1_loss - min_qf2_loss) + else: + alpha_prime_loss = -min_qf1_loss + + cql_loss = [min_qf1_loss] + if twin_q: + cql_loss.append(min_qf2_loss) + + critic_loss = [critic_loss_1 + min_qf1_loss] + if twin_q: + critic_loss.append(critic_loss_2 + min_qf2_loss) + + if batch_size == policy.config["train_batch_size"]: + policy.critic_optims[0].zero_grad() + critic_loss[0].backward(retain_graph=True) + policy.critic_optims[0].step() + + if twin_q: + policy.critic_optims[1].zero_grad() + critic_loss[1].backward(retain_graph=False) + policy.critic_optims[1].step() + + # Store values for stats function in model (tower), such that for + # multi-GPU, we do not override them during the parallel loss phase. + # SAC stats. + model.tower_stats["q_t"] = q_t_selected + model.tower_stats["policy_t"] = policy_t + model.tower_stats["log_pis_t"] = log_pis_t + model.tower_stats["actor_loss"] = actor_loss + model.tower_stats["critic_loss"] = critic_loss + model.tower_stats["alpha_loss"] = alpha_loss + model.tower_stats["log_alpha_value"] = model.log_alpha + model.tower_stats["alpha_value"] = alpha + model.tower_stats["target_entropy"] = model.target_entropy + # CQL stats. + model.tower_stats["cql_loss"] = cql_loss + + # TD-error tensor in final stats + # will be concatenated and retrieved for each individual batch item. + model.tower_stats["td_error"] = td_error + + if use_lagrange: + model.tower_stats["log_alpha_prime_value"] = model.log_alpha_prime[0] + model.tower_stats["alpha_prime_value"] = alpha_prime + model.tower_stats["alpha_prime_loss"] = alpha_prime_loss + + if batch_size == policy.config["train_batch_size"]: + policy.alpha_prime_optim.zero_grad() + alpha_prime_loss.backward() + policy.alpha_prime_optim.step() + + # Return all loss terms corresponding to our optimizers. + return tuple( + [actor_loss] + + critic_loss + + [alpha_loss] + + ([alpha_prime_loss] if use_lagrange else []) + ) + + +def cql_stats(policy: Policy, train_batch: SampleBatch) -> Dict[str, TensorType]: + # Get SAC loss stats. + stats_dict = stats(policy, train_batch) + + # Add CQL loss stats to the dict. + stats_dict["cql_loss"] = torch.mean( + torch.stack(*policy.get_tower_stats("cql_loss")) + ) + + if policy.config["lagrangian"]: + stats_dict["log_alpha_prime_value"] = torch.mean( + torch.stack(policy.get_tower_stats("log_alpha_prime_value")) + ) + stats_dict["alpha_prime_value"] = torch.mean( + torch.stack(policy.get_tower_stats("alpha_prime_value")) + ) + stats_dict["alpha_prime_loss"] = torch.mean( + torch.stack(policy.get_tower_stats("alpha_prime_loss")) + ) + return stats_dict + + +def cql_optimizer_fn( + policy: Policy, config: AlgorithmConfigDict +) -> Tuple[LocalOptimizer]: + policy.cur_iter = 0 + opt_list = optimizer_fn(policy, config) + if config["lagrangian"]: + log_alpha_prime = nn.Parameter(torch.zeros(1, requires_grad=True).float()) + policy.model.register_parameter("log_alpha_prime", log_alpha_prime) + policy.alpha_prime_optim = torch.optim.Adam( + params=[policy.model.log_alpha_prime], + lr=config["optimization"]["critic_learning_rate"], + eps=1e-7, # to match tf.keras.optimizers.Adam's epsilon default + ) + return tuple( + [policy.actor_optim] + + policy.critic_optims + + [policy.alpha_optim] + + [policy.alpha_prime_optim] + ) + return opt_list + + +def cql_setup_late_mixins( + policy: Policy, + obs_space: gym.spaces.Space, + action_space: gym.spaces.Space, + config: AlgorithmConfigDict, +) -> None: + setup_late_mixins(policy, obs_space, action_space, config) + if config["lagrangian"]: + policy.model.log_alpha_prime = policy.model.log_alpha_prime.to(policy.device) + + +def compute_gradients_fn(policy, postprocessed_batch): + batches = [policy._lazy_tensor_dict(postprocessed_batch)] + model = policy.model + policy._loss(policy, model, policy.dist_class, batches[0]) + stats = {LEARNER_STATS_KEY: policy._convert_to_numpy(cql_stats(policy, batches[0]))} + return [None, stats] + + +def apply_gradients_fn(policy, gradients): + return + + +# Build a child class of `TorchPolicy`, given the custom functions defined +# above. +CQLTorchPolicy = build_policy_class( + name="CQLTorchPolicy", + framework="torch", + loss_fn=cql_loss, + get_default_config=lambda: ray.rllib.algorithms.cql.cql.CQLConfig(), + stats_fn=cql_stats, + postprocess_fn=postprocess_trajectory, + extra_grad_process_fn=apply_grad_clipping, + optimizer_fn=cql_optimizer_fn, + validate_spaces=validate_spaces, + before_loss_init=cql_setup_late_mixins, + make_model_and_action_dist=build_sac_model_and_action_dist, + extra_learn_fetches_fn=concat_multi_gpu_td_errors, + mixins=[TargetNetworkMixin, ComputeTDErrorMixin], + action_distribution_fn=action_distribution_fn, + compute_gradients_fn=compute_gradients_fn, + apply_gradients_fn=apply_gradients_fn, +) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd75b9795485a254e498487a7940c2dac35b0225 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__pycache__/cql_torch_learner.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__pycache__/cql_torch_learner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c7d254634d86c7bdd2849b1983710e12b2ee03b Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__pycache__/cql_torch_learner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__pycache__/default_cql_torch_rl_module.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__pycache__/default_cql_torch_rl_module.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..beedbc7b9b95f54e702c74dd08173c390e210401 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/__pycache__/default_cql_torch_rl_module.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/cql_torch_learner.py b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/cql_torch_learner.py new file mode 100644 index 0000000000000000000000000000000000000000..4c04fb5de8734ee02014b6ba7df637e265961088 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/cql_torch_learner.py @@ -0,0 +1,280 @@ +from typing import Dict + +from ray.rllib.algorithms.cql.cql import CQLConfig +from ray.rllib.algorithms.sac.sac_learner import ( + LOGPS_KEY, + QF_LOSS_KEY, + QF_MAX_KEY, + QF_MEAN_KEY, + QF_MIN_KEY, + QF_PREDS, + QF_TWIN_LOSS_KEY, + QF_TWIN_PREDS, + TD_ERROR_MEAN_KEY, +) +from ray.rllib.algorithms.sac.torch.sac_torch_learner import SACTorchLearner +from ray.rllib.core.columns import Columns +from ray.rllib.core.learner.learner import ( + POLICY_LOSS_KEY, +) +from ray.rllib.utils.annotations import override +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.metrics import ALL_MODULES +from ray.rllib.utils.typing import ModuleID, ParamDict, TensorType +from ray.tune.result import TRAINING_ITERATION + +torch, nn = try_import_torch() + + +class CQLTorchLearner(SACTorchLearner): + @override(SACTorchLearner) + def compute_loss_for_module( + self, + *, + module_id: ModuleID, + config: CQLConfig, + batch: Dict, + fwd_out: Dict[str, TensorType], + ) -> TensorType: + + # TODO (simon, sven): Add upstream information pieces into this timesteps + # call arg to Learner.update_...(). + self.metrics.log_value( + (ALL_MODULES, TRAINING_ITERATION), + 1, + reduce="sum", + ) + # Get the train action distribution for the current policy and current state. + # This is needed for the policy (actor) loss and the `alpha`` loss. + action_dist_class = self.module[module_id].get_train_action_dist_cls() + action_dist_curr = action_dist_class.from_logits( + fwd_out[Columns.ACTION_DIST_INPUTS] + ) + + # Optimize also the hyperparameter `alpha` by using the current policy + # evaluated at the current state (from offline data). Note, in contrast + # to the original SAC loss, here the `alpha` and actor losses are + # calculated first. + # TODO (simon): Check, why log(alpha) is used, prob. just better + # to optimize and monotonic function. Original equation uses alpha. + alpha_loss = -torch.mean( + self.curr_log_alpha[module_id] + * (fwd_out["logp_resampled"].detach() + self.target_entropy[module_id]) + ) + + # Get the current alpha. + alpha = torch.exp(self.curr_log_alpha[module_id]) + # Start training with behavior cloning and turn to the classic Soft-Actor Critic + # after `bc_iters` of training iterations. + if ( + self.metrics.peek((ALL_MODULES, TRAINING_ITERATION), default=0) + >= config.bc_iters + ): + actor_loss = torch.mean( + alpha.detach() * fwd_out["logp_resampled"] - fwd_out["q_curr"] + ) + else: + # Use log-probabilities of the current action distribution to clone + # the behavior policy (selected actions in data) in the first `bc_iters` + # training iterations. + bc_logps_curr = action_dist_curr.logp(batch[Columns.ACTIONS]) + actor_loss = torch.mean( + alpha.detach() * fwd_out["logp_resampled"] - bc_logps_curr + ) + + # The critic loss is composed of the standard SAC Critic L2 loss and the + # CQL entropy loss. + + # Get the Q-values for the actually selected actions in the offline data. + # In the critic loss we use these as predictions. + q_selected = fwd_out[QF_PREDS] + if config.twin_q: + q_twin_selected = fwd_out[QF_TWIN_PREDS] + + if not config.deterministic_backup: + q_next = ( + fwd_out["q_target_next"] + - alpha.detach() * fwd_out["logp_next_resampled"] + ) + else: + q_next = fwd_out["q_target_next"] + + # Now mask all Q-values with terminating next states in the targets. + q_next_masked = (1.0 - batch[Columns.TERMINATEDS].float()) * q_next + + # Compute the right hand side of the Bellman equation. Detach this node + # from the computation graph as we do not want to backpropagate through + # the target network when optimizing the Q loss. + # TODO (simon, sven): Kumar et al. (2020) use here also a reward scaler. + q_selected_target = ( + # TODO (simon): Add an `n_step` option to the `AddNextObsToBatch` connector. + batch[Columns.REWARDS] + # TODO (simon): Implement n_step. + + (config.gamma) * q_next_masked + ).detach() + + # Calculate the TD error. + td_error = torch.abs(q_selected - q_selected_target) + # Calculate a TD-error for twin-Q values, if needed. + if config.twin_q: + td_error += torch.abs(q_twin_selected - q_selected_target) + # Rescale the TD error + td_error *= 0.5 + + # MSBE loss for the critic(s) (i.e. Q, see eqs. (7-8) Haarnoja et al. (2018)). + # Note, this needs a sample from the current policy given the next state. + # Note further, we could also use here the Huber loss instead of the MSE. + # TODO (simon): Add the huber loss as an alternative (SAC uses it). + sac_critic_loss = torch.nn.MSELoss(reduction="mean")( + q_selected, + q_selected_target, + ) + if config.twin_q: + sac_critic_twin_loss = torch.nn.MSELoss(reduction="mean")( + q_twin_selected, + q_selected_target, + ) + + # Now calculate the CQL loss (we use the entropy version of the CQL algorithm). + # Note, the entropy version performs best in shown experiments. + + # Compute the log-probabilities for the random actions (note, we generate random + # actions (from the mu distribution as named in Kumar et al. (2020))). + # Note, all actions, action log-probabilities and Q-values are already computed + # by the module's `_forward_train` method. + # TODO (simon): This is the density for a discrete uniform, however, actions + # come from a continuous one. So actually this density should use (1/(high-low)) + # instead of (1/2). + random_density = torch.log( + torch.pow( + 0.5, + torch.tensor( + fwd_out["actions_curr_repeat"].shape[-1], + device=fwd_out["actions_curr_repeat"].device, + ), + ) + ) + # Merge all Q-values and subtract the log-probabilities (note, we use the + # entropy version of CQL). + q_repeat = torch.cat( + [ + fwd_out["q_rand_repeat"] - random_density, + fwd_out["q_next_repeat"] - fwd_out["logps_next_repeat"].detach(), + fwd_out["q_curr_repeat"] - fwd_out["logps_curr_repeat"].detach(), + ], + dim=1, + ) + cql_loss = ( + torch.logsumexp(q_repeat / config.temperature, dim=1).mean() + * config.min_q_weight + * config.temperature + ) + cql_loss -= q_selected.mean() * config.min_q_weight + # Add the CQL loss term to the SAC loss term. + critic_loss = sac_critic_loss + cql_loss + + # If a twin Q-value function is implemented calculated its CQL loss. + if config.twin_q: + q_twin_repeat = torch.cat( + [ + fwd_out["q_twin_rand_repeat"] - random_density, + fwd_out["q_twin_next_repeat"] + - fwd_out["logps_next_repeat"].detach(), + fwd_out["q_twin_curr_repeat"] + - fwd_out["logps_curr_repeat"].detach(), + ], + dim=1, + ) + cql_twin_loss = ( + torch.logsumexp(q_twin_repeat / config.temperature, dim=1).mean() + * config.min_q_weight + * config.temperature + ) + cql_twin_loss -= q_twin_selected.mean() * config.min_q_weight + # Add the CQL loss term to the SAC loss term. + critic_twin_loss = sac_critic_twin_loss + cql_twin_loss + + # TODO (simon): Check, if we need to implement here also a Lagrangian + # loss. + + total_loss = actor_loss + critic_loss + alpha_loss + + # Add the twin critic loss to the total loss, if needed. + if config.twin_q: + # Reweigh the critic loss terms in the total loss. + total_loss += 0.5 * critic_twin_loss - 0.5 * critic_loss + + # Log important loss stats (reduce=mean (default), but with window=1 + # in order to keep them history free). + self.metrics.log_dict( + { + POLICY_LOSS_KEY: actor_loss, + QF_LOSS_KEY: critic_loss, + # TODO (simon): Add these keys to SAC Learner. + "cql_loss": cql_loss, + "alpha_loss": alpha_loss, + "alpha_value": alpha[0], + "log_alpha_value": torch.log(alpha)[0], + "target_entropy": self.target_entropy[module_id], + LOGPS_KEY: torch.mean( + fwd_out["logp_resampled"] + ), # torch.mean(logps_curr), + QF_MEAN_KEY: torch.mean(fwd_out["q_curr_repeat"]), + QF_MAX_KEY: torch.max(fwd_out["q_curr_repeat"]), + QF_MIN_KEY: torch.min(fwd_out["q_curr_repeat"]), + TD_ERROR_MEAN_KEY: torch.mean(td_error), + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + self._temp_losses[(module_id, POLICY_LOSS_KEY)] = actor_loss + self._temp_losses[(module_id, QF_LOSS_KEY)] = critic_loss + self._temp_losses[(module_id, "alpha_loss")] = alpha_loss + + # TODO (simon): Add loss keys for langrangian, if needed. + # TODO (simon): Add only here then the Langrange parameter optimization. + if config.twin_q: + self.metrics.log_value( + key=(module_id, QF_TWIN_LOSS_KEY), + value=critic_twin_loss, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + self._temp_losses[(module_id, QF_TWIN_LOSS_KEY)] = critic_twin_loss + + # Return the total loss. + return total_loss + + @override(SACTorchLearner) + def compute_gradients( + self, loss_per_module: Dict[ModuleID, TensorType], **kwargs + ) -> ParamDict: + + grads = {} + for module_id in set(loss_per_module.keys()) - {ALL_MODULES}: + # Loop through optimizers registered for this module. + for optim_name, optim in self.get_optimizers_for_module(module_id): + # Zero the gradients. Note, we need to reset the gradients b/c + # each component for a module operates on the same graph. + optim.zero_grad(set_to_none=True) + + # Compute the gradients for the component and module. + loss_tensor = self._temp_losses.pop((module_id, optim_name + "_loss")) + loss_tensor.backward( + retain_graph=False if optim_name in ["policy", "alpha"] else True + ) + # Store the gradients for the component and module. + # TODO (simon): Check another time the graph for overlapping + # gradients. + grads.update( + { + pid: grads[pid] + p.grad.clone() + if pid in grads + else p.grad.clone() + for pid, p in self.filter_param_dict_for_optimizer( + self._params, optim + ).items() + } + ) + + assert not self._temp_losses + return grads diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/default_cql_torch_rl_module.py b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/default_cql_torch_rl_module.py new file mode 100644 index 0000000000000000000000000000000000000000..1c2e7a7a2301e18c2e9eae3b23e30fa0a25d96b5 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/cql/torch/default_cql_torch_rl_module.py @@ -0,0 +1,207 @@ +from typing import Any, Dict, Optional + +import tree + +from ray.rllib.algorithms.sac.sac_catalog import SACCatalog +from ray.rllib.algorithms.sac.sac_learner import ( + QF_PREDS, + QF_TWIN_PREDS, +) +from ray.rllib.algorithms.sac.torch.default_sac_torch_rl_module import ( + DefaultSACTorchRLModule, +) +from ray.rllib.core.columns import Columns +from ray.rllib.core.models.base import ENCODER_OUT +from ray.rllib.utils.annotations import override +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.typing import TensorType + +torch, nn = try_import_torch() + + +class DefaultCQLTorchRLModule(DefaultSACTorchRLModule): + def __init__(self, *args, **kwargs): + catalog_class = kwargs.pop("catalog_class", None) + if catalog_class is None: + catalog_class = SACCatalog + super().__init__(*args, **kwargs, catalog_class=catalog_class) + + @override(DefaultSACTorchRLModule) + def _forward_train(self, batch: Dict) -> Dict[str, Any]: + # Call the super method. + fwd_out = super()._forward_train(batch) + + # Make sure we perform a "straight-through gradient" pass here, + # ignoring the gradients of the q-net, however, still recording + # the gradients of the policy net (which was used to rsample the actions used + # here). This is different from doing `.detach()` or `with torch.no_grads()`, + # as these two methds would fully block all gradient recordings, including + # the needed policy ones. + all_params = list(self.pi_encoder.parameters()) + list(self.pi.parameters()) + # if self.twin_q: + # all_params += list(self.qf_twin.parameters()) + list( + # self.qf_twin_encoder.parameters() + # ) + + for param in all_params: + param.requires_grad = False + + # Compute the repeated actions, action log-probabilites and Q-values for all + # observations. + # First for the random actions (from the mu-distribution as named by Kumar et + # al. (2020)). + low = torch.tensor( + self.action_space.low, + device=fwd_out[QF_PREDS].device, + ) + high = torch.tensor( + self.action_space.high, + device=fwd_out[QF_PREDS].device, + ) + num_samples = batch[Columns.ACTIONS].shape[0] * self.model_config["num_actions"] + actions_rand_repeat = low + (high - low) * torch.rand( + (num_samples, low.shape[0]), device=fwd_out[QF_PREDS].device + ) + + # First for the random actions (from the mu-distribution as named in Kumar + # et al. (2020)) using repeated observations. + rand_repeat_out = self._repeat_actions(batch[Columns.OBS], actions_rand_repeat) + (fwd_out["actions_rand_repeat"], fwd_out["q_rand_repeat"]) = ( + rand_repeat_out[Columns.ACTIONS], + rand_repeat_out[QF_PREDS], + ) + # Sample current and next actions (from the pi distribution as named in Kumar + # et al. (2020)) using repeated observations + # Second for the current observations and the current action distribution. + curr_repeat_out = self._repeat_actions(batch[Columns.OBS]) + ( + fwd_out["actions_curr_repeat"], + fwd_out["logps_curr_repeat"], + fwd_out["q_curr_repeat"], + ) = ( + curr_repeat_out[Columns.ACTIONS], + curr_repeat_out[Columns.ACTION_LOGP], + curr_repeat_out[QF_PREDS], + ) + # Then, for the next observations and the current action distribution. + next_repeat_out = self._repeat_actions(batch[Columns.NEXT_OBS]) + ( + fwd_out["actions_next_repeat"], + fwd_out["logps_next_repeat"], + fwd_out["q_next_repeat"], + ) = ( + next_repeat_out[Columns.ACTIONS], + next_repeat_out[Columns.ACTION_LOGP], + next_repeat_out[QF_PREDS], + ) + if self.twin_q: + # First for the random actions from the mu-distribution. + fwd_out["q_twin_rand_repeat"] = rand_repeat_out[QF_TWIN_PREDS] + # Second for the current observations and the current action distribution. + fwd_out["q_twin_curr_repeat"] = curr_repeat_out[QF_TWIN_PREDS] + # Then, for the next observations and the current action distribution. + fwd_out["q_twin_next_repeat"] = next_repeat_out[QF_TWIN_PREDS] + # Reset the gradient requirements for all Q-function parameters. + for param in all_params: + param.requires_grad = True + + return fwd_out + + def _repeat_tensor(self, tensor: TensorType, repeat: int) -> TensorType: + """Generates a repeated version of a tensor. + + The repetition is done similar `np.repeat` and repeats each value + instead of the complete vector. + + Args: + tensor: The tensor to be repeated. + repeat: How often each value in the tensor should be repeated. + + Returns: + A tensor holding `repeat` repeated values of the input `tensor` + """ + # Insert the new dimension at axis 1 into the tensor. + t_repeat = tensor.unsqueeze(1) + # Repeat the tensor along the new dimension. + t_repeat = torch.repeat_interleave(t_repeat, repeat, dim=1) + # Stack the repeated values into the batch dimension. + t_repeat = t_repeat.view(-1, *tensor.shape[1:]) + # Return the repeated tensor. + return t_repeat + + def _repeat_actions( + self, obs: TensorType, actions: Optional[TensorType] = None + ) -> Dict[str, TensorType]: + """Generated actions and Q-values for repeated observations. + + The `self.model_config["num_actions"]` define a multiplier + used for generating `num_actions` as many actions as the batch size. + Observations are repeated and then a model forward pass is made. + + Args: + obs: A batched observation tensor. + actions: An optional batched actions tensor. + + Returns: + A dictionary holding the (sampled or passed-in actions), the log + probabilities (of sampled actions), the Q-values and if available + the twin-Q values. + """ + output = {} + # Receive the batch size. + batch_size = obs.shape[0] + # Receive the number of action to sample. + num_actions = self.model_config["num_actions"] + # Repeat the observations `num_actions` times. + obs_repeat = tree.map_structure( + lambda t: self._repeat_tensor(t, num_actions), obs + ) + # Generate a batch for the forward pass. + temp_batch = {Columns.OBS: obs_repeat} + if actions is None: + # TODO (simon): Run the forward pass in inference mode. + # Compute the action logits. + pi_encoder_outs = self.pi_encoder(temp_batch) + action_logits = self.pi(pi_encoder_outs[ENCODER_OUT]) + # Generate the squashed Gaussian from the model's logits. + action_dist = self.get_train_action_dist_cls().from_logits(action_logits) + # Sample the actions. Note, we want to make a backward pass through + # these actions. + output[Columns.ACTIONS] = action_dist.rsample() + # Compute the action log-probabilities. + output[Columns.ACTION_LOGP] = action_dist.logp( + output[Columns.ACTIONS] + ).view(batch_size, num_actions, 1) + else: + output[Columns.ACTIONS] = actions + + # Compute all Q-values. + temp_batch.update( + { + Columns.ACTIONS: output[Columns.ACTIONS], + } + ) + output.update( + { + QF_PREDS: self._qf_forward_train_helper( + temp_batch, + self.qf_encoder, + self.qf, + ).view(batch_size, num_actions, 1) + } + ) + # If we have a twin-Q network, compute its Q-values, too. + if self.twin_q: + output.update( + { + QF_TWIN_PREDS: self._qf_forward_train_helper( + temp_batch, + self.qf_twin_encoder, + self.qf_twin, + ).view(batch_size, num_actions, 1) + } + ) + del temp_batch + + # Return + return output diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f6046b585028664e7351193d3ece2e291aa78278 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__init__.py @@ -0,0 +1,10 @@ +from ray.rllib.algorithms.dqn.dqn import DQN, DQNConfig +from ray.rllib.algorithms.dqn.dqn_tf_policy import DQNTFPolicy +from ray.rllib.algorithms.dqn.dqn_torch_policy import DQNTorchPolicy + +__all__ = [ + "DQN", + "DQNConfig", + "DQNTFPolicy", + "DQNTorchPolicy", +] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac25fc3677f29fc899f9823c05fc2d1422ceed99 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/default_dqn_rl_module.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/default_dqn_rl_module.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab659e1cd37a54c79e56021bd2ff01d9beb9e404 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/default_dqn_rl_module.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/distributional_q_tf_model.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/distributional_q_tf_model.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66d5721a4733dcf839b5c71aaddc5f9a63741359 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/distributional_q_tf_model.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e18cc05bd5332b45d77932f7e7f1e8a943aabe44 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_catalog.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_catalog.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e3970328eeb682174e874b2fd504f785a385d7b Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_catalog.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_learner.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_learner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de88b9539d7ad00e6b4691e88e3374581d42fa4b Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_learner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_tf_policy.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_tf_policy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e1317650aa1e463adbbed8fd373957ff36587fe Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_tf_policy.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_torch_model.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_torch_model.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c183cbd4df7e572550d2800c4abab0ab3bfdca33 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_torch_model.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_torch_policy.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_torch_policy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ceb948393a8ad9da9b8fd9a09e673ca20ffdeff Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/__pycache__/dqn_torch_policy.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/default_dqn_rl_module.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/default_dqn_rl_module.py new file mode 100644 index 0000000000000000000000000000000000000000..b4062ead7adfafc768e8a8659f8a5558e03dcb45 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/default_dqn_rl_module.py @@ -0,0 +1,166 @@ +import abc +from typing import Any, Dict, List, Tuple, Union + +from ray.rllib.core.learner.utils import make_target_network +from ray.rllib.core.models.base import Encoder, Model +from ray.rllib.core.rl_module.apis import InferenceOnlyAPI, QNetAPI, TargetNetworkAPI +from ray.rllib.core.rl_module.rl_module import RLModule +from ray.rllib.utils.annotations import ( + OverrideToImplementCustomLogic, + override, +) +from ray.rllib.utils.schedules.scheduler import Scheduler +from ray.rllib.utils.typing import NetworkType, TensorType +from ray.util.annotations import DeveloperAPI + +QF_PREDS = "qf_preds" +ATOMS = "atoms" +QF_LOGITS = "qf_logits" +QF_NEXT_PREDS = "qf_next_preds" +QF_PROBS = "qf_probs" +QF_TARGET_NEXT_PREDS = "qf_target_next_preds" +QF_TARGET_NEXT_PROBS = "qf_target_next_probs" + + +@DeveloperAPI +class DefaultDQNRLModule(RLModule, InferenceOnlyAPI, TargetNetworkAPI, QNetAPI): + @override(RLModule) + def setup(self): + # If a dueling architecture is used. + self.uses_dueling: bool = self.model_config.get("dueling") + # If double Q learning is used. + self.uses_double_q: bool = self.model_config.get("double_q") + # The number of atoms for a distribution support. + self.num_atoms: int = self.model_config.get("num_atoms") + # If distributional learning is requested configure the support. + if self.num_atoms > 1: + self.v_min: float = self.model_config.get("v_min") + self.v_max: float = self.model_config.get("v_max") + # The epsilon scheduler for epsilon greedy exploration. + self.epsilon_schedule = Scheduler( + fixed_value_or_schedule=self.model_config["epsilon"], + framework=self.framework, + ) + + # Build the encoder for the advantage and value streams. Note, + # the same encoder is used. + # Note further, by using the base encoder the correct encoder + # is chosen for the observation space used. + self.encoder = self.catalog.build_encoder(framework=self.framework) + + # Build heads. + self.af = self.catalog.build_af_head(framework=self.framework) + if self.uses_dueling: + # If in a dueling setting setup the value function head. + self.vf = self.catalog.build_vf_head(framework=self.framework) + + @override(InferenceOnlyAPI) + def get_non_inference_attributes(self) -> List[str]: + return ["_target_encoder", "_target_af"] + ( + ["_target_vf"] if self.uses_dueling else [] + ) + + @override(TargetNetworkAPI) + def make_target_networks(self) -> None: + self._target_encoder = make_target_network(self.encoder) + self._target_af = make_target_network(self.af) + if self.uses_dueling: + self._target_vf = make_target_network(self.vf) + + @override(TargetNetworkAPI) + def get_target_network_pairs(self) -> List[Tuple[NetworkType, NetworkType]]: + return [(self.encoder, self._target_encoder), (self.af, self._target_af)] + ( + # If we have a dueling architecture we need to update the value stream + # target, too. + [ + (self.vf, self._target_vf), + ] + if self.uses_dueling + else [] + ) + + @override(TargetNetworkAPI) + def forward_target(self, batch: Dict[str, Any]) -> Dict[str, Any]: + """Computes Q-values from the target network. + + Note, these can be accompanied by logits and probabilities + in case of distributional Q-learning, i.e. `self.num_atoms > 1`. + + Args: + batch: The batch received in the forward pass. + + Results: + A dictionary containing the target Q-value predictions ("qf_preds") + and in case of distributional Q-learning in addition to the target + Q-value predictions ("qf_preds") the support atoms ("atoms"), the target + Q-logits ("qf_logits"), and the probabilities ("qf_probs"). + """ + # If we have a dueling architecture we have to add the value stream. + return self._qf_forward_helper( + batch, + self._target_encoder, + ( + {"af": self._target_af, "vf": self._target_vf} + if self.uses_dueling + else self._target_af + ), + ) + + @override(QNetAPI) + def compute_q_values(self, batch: Dict[str, TensorType]) -> Dict[str, TensorType]: + """Computes Q-values, given encoder, q-net and (optionally), advantage net. + + Note, these can be accompanied by logits and probabilities + in case of distributional Q-learning, i.e. `self.num_atoms > 1`. + + Args: + batch: The batch received in the forward pass. + + Results: + A dictionary containing the Q-value predictions ("qf_preds") + and in case of distributional Q-learning - in addition to the Q-value + predictions ("qf_preds") - the support atoms ("atoms"), the Q-logits + ("qf_logits"), and the probabilities ("qf_probs"). + """ + # If we have a dueling architecture we have to add the value stream. + return self._qf_forward_helper( + batch, + self.encoder, + {"af": self.af, "vf": self.vf} if self.uses_dueling else self.af, + ) + + @override(RLModule) + def get_initial_state(self) -> dict: + if hasattr(self.encoder, "get_initial_state"): + return self.encoder.get_initial_state() + else: + return {} + + @abc.abstractmethod + @OverrideToImplementCustomLogic + def _qf_forward_helper( + self, + batch: Dict[str, TensorType], + encoder: Encoder, + head: Union[Model, Dict[str, Model]], + ) -> Dict[str, TensorType]: + """Computes Q-values. + + This is a helper function that takes care of all different cases, + i.e. if we use a dueling architecture or not and if we use distributional + Q-learning or not. + + Args: + batch: The batch received in the forward pass. + encoder: The encoder network to use. Here we have a single encoder + for all heads (Q or advantages and value in case of a dueling + architecture). + head: Either a head model or a dictionary of head model (dueling + architecture) containing advantage and value stream heads. + + Returns: + In case of expectation learning the Q-value predictions ("qf_preds") + and in case of distributional Q-learning in addition to the predictions + the atoms ("atoms"), the Q-value predictions ("qf_preds"), the Q-logits + ("qf_logits") and the probabilities for the support atoms ("qf_probs"). + """ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/distributional_q_tf_model.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/distributional_q_tf_model.py new file mode 100644 index 0000000000000000000000000000000000000000..421f5716d2b775e655a8c13d8a2252a9dbf2efae --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/distributional_q_tf_model.py @@ -0,0 +1,191 @@ +"""Tensorflow model for DQN""" + +from typing import List + +import gymnasium as gym + +from ray.rllib.models.tf.layers import NoisyLayer +from ray.rllib.models.tf.tf_modelv2 import TFModelV2 +from ray.rllib.utils.annotations import OldAPIStack +from ray.rllib.utils.framework import try_import_tf +from ray.rllib.utils.typing import ModelConfigDict, TensorType + +tf1, tf, tfv = try_import_tf() + + +@OldAPIStack +class DistributionalQTFModel(TFModelV2): + """Extension of standard TFModel to provide distributional Q values. + + It also supports options for noisy nets and parameter space noise. + + Data flow: + obs -> forward() -> model_out + model_out -> get_q_value_distributions() -> Q(s, a) atoms + model_out -> get_state_value() -> V(s) + + Note that this class by itself is not a valid model unless you + implement forward() in a subclass.""" + + def __init__( + self, + obs_space: gym.spaces.Space, + action_space: gym.spaces.Space, + num_outputs: int, + model_config: ModelConfigDict, + name: str, + q_hiddens=(256,), + dueling: bool = False, + num_atoms: int = 1, + use_noisy: bool = False, + v_min: float = -10.0, + v_max: float = 10.0, + sigma0: float = 0.5, + # TODO(sven): Move `add_layer_norm` into ModelCatalog as + # generic option, then error if we use ParameterNoise as + # Exploration type and do not have any LayerNorm layers in + # the net. + add_layer_norm: bool = False, + ): + """Initialize variables of this model. + + Extra model kwargs: + q_hiddens (List[int]): List of layer-sizes after(!) the + Advantages(A)/Value(V)-split. Hence, each of the A- and V- + branches will have this structure of Dense layers. To define + the NN before this A/V-split, use - as always - + config["model"]["fcnet_hiddens"]. + dueling: Whether to build the advantage(A)/value(V) heads + for DDQN. If True, Q-values are calculated as: + Q = (A - mean[A]) + V. If False, raw NN output is interpreted + as Q-values. + num_atoms: If >1, enables distributional DQN. + use_noisy: Use noisy nets. + v_min: Min value support for distributional DQN. + v_max: Max value support for distributional DQN. + sigma0 (float): Initial value of noisy layers. + add_layer_norm: Enable layer norm (for param noise). + + Note that the core layers for forward() are not defined here, this + only defines the layers for the Q head. Those layers for forward() + should be defined in subclasses of DistributionalQModel. + """ + super(DistributionalQTFModel, self).__init__( + obs_space, action_space, num_outputs, model_config, name + ) + + # setup the Q head output (i.e., model for get_q_values) + self.model_out = tf.keras.layers.Input(shape=(num_outputs,), name="model_out") + + def build_action_value(prefix: str, model_out: TensorType) -> List[TensorType]: + if q_hiddens: + action_out = model_out + for i in range(len(q_hiddens)): + if use_noisy: + action_out = NoisyLayer( + "{}hidden_{}".format(prefix, i), q_hiddens[i], sigma0 + )(action_out) + elif add_layer_norm: + action_out = tf.keras.layers.Dense( + units=q_hiddens[i], activation=tf.nn.relu + )(action_out) + action_out = tf.keras.layers.LayerNormalization()(action_out) + else: + action_out = tf.keras.layers.Dense( + units=q_hiddens[i], + activation=tf.nn.relu, + name="hidden_%d" % i, + )(action_out) + else: + # Avoid postprocessing the outputs. This enables custom models + # to be used for parametric action DQN. + action_out = model_out + + if use_noisy: + action_scores = NoisyLayer( + "{}output".format(prefix), + self.action_space.n * num_atoms, + sigma0, + activation=None, + )(action_out) + elif q_hiddens: + action_scores = tf.keras.layers.Dense( + units=self.action_space.n * num_atoms, activation=None + )(action_out) + else: + action_scores = model_out + + if num_atoms > 1: + # Distributional Q-learning uses a discrete support z + # to represent the action value distribution + z = tf.range(num_atoms, dtype=tf.float32) + z = v_min + z * (v_max - v_min) / float(num_atoms - 1) + + def _layer(x): + support_logits_per_action = tf.reshape( + tensor=x, shape=(-1, self.action_space.n, num_atoms) + ) + support_prob_per_action = tf.nn.softmax( + logits=support_logits_per_action + ) + x = tf.reduce_sum(input_tensor=z * support_prob_per_action, axis=-1) + logits = support_logits_per_action + dist = support_prob_per_action + return [x, z, support_logits_per_action, logits, dist] + + return tf.keras.layers.Lambda(_layer)(action_scores) + else: + logits = tf.expand_dims(tf.ones_like(action_scores), -1) + dist = tf.expand_dims(tf.ones_like(action_scores), -1) + return [action_scores, logits, dist] + + def build_state_score(prefix: str, model_out: TensorType) -> TensorType: + state_out = model_out + for i in range(len(q_hiddens)): + if use_noisy: + state_out = NoisyLayer( + "{}dueling_hidden_{}".format(prefix, i), q_hiddens[i], sigma0 + )(state_out) + else: + state_out = tf.keras.layers.Dense( + units=q_hiddens[i], activation=tf.nn.relu + )(state_out) + if add_layer_norm: + state_out = tf.keras.layers.LayerNormalization()(state_out) + if use_noisy: + state_score = NoisyLayer( + "{}dueling_output".format(prefix), + num_atoms, + sigma0, + activation=None, + )(state_out) + else: + state_score = tf.keras.layers.Dense(units=num_atoms, activation=None)( + state_out + ) + return state_score + + q_out = build_action_value(name + "/action_value/", self.model_out) + self.q_value_head = tf.keras.Model(self.model_out, q_out) + + if dueling: + state_out = build_state_score(name + "/state_value/", self.model_out) + self.state_value_head = tf.keras.Model(self.model_out, state_out) + + def get_q_value_distributions(self, model_out: TensorType) -> List[TensorType]: + """Returns distributional values for Q(s, a) given a state embedding. + + Override this in your custom model to customize the Q output head. + + Args: + model_out: embedding from the model layers + + Returns: + (action_scores, logits, dist) if num_atoms == 1, otherwise + (action_scores, z, support_logits_per_action, logits, dist) + """ + return self.q_value_head(model_out) + + def get_state_value(self, model_out: TensorType) -> TensorType: + """Returns the state value prediction for the given state embedding.""" + return self.state_value_head(model_out) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn.py new file mode 100644 index 0000000000000000000000000000000000000000..a5ca9a754d68510e3acacfec47767297deea8191 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn.py @@ -0,0 +1,859 @@ +""" +Deep Q-Networks (DQN, Rainbow, Parametric DQN) +============================================== + +This file defines the distributed Algorithm class for the Deep Q-Networks +algorithm. See `dqn_[tf|torch]_policy.py` for the definition of the policies. + +Detailed documentation: +https://docs.ray.io/en/master/rllib-algorithms.html#deep-q-networks-dqn-rainbow-parametric-dqn +""" # noqa: E501 + +import logging +from collections import defaultdict +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union + +import numpy as np +from typing_extensions import Self + +from ray._common.deprecation import DEPRECATED_VALUE +from ray.rllib.algorithms.algorithm import Algorithm +from ray.rllib.algorithms.algorithm_config import AlgorithmConfig, NotProvided +from ray.rllib.algorithms.dqn.dqn_tf_policy import DQNTFPolicy +from ray.rllib.algorithms.dqn.dqn_torch_policy import DQNTorchPolicy +from ray.rllib.core.learner import Learner +from ray.rllib.core.rl_module.rl_module import RLModuleSpec +from ray.rllib.execution.rollout_ops import ( + synchronous_parallel_sample, +) +from ray.rllib.execution.train_ops import ( + multi_gpu_train_one_step, + train_one_step, +) +from ray.rllib.policy.policy import Policy +from ray.rllib.policy.sample_batch import MultiAgentBatch +from ray.rllib.utils import deep_update +from ray.rllib.utils.annotations import override +from ray.rllib.utils.metrics import ( + ALL_MODULES, + ENV_RUNNER_RESULTS, + ENV_RUNNER_SAMPLING_TIMER, + LAST_TARGET_UPDATE_TS, + LEARNER_RESULTS, + LEARNER_UPDATE_TIMER, + NUM_AGENT_STEPS_SAMPLED, + NUM_AGENT_STEPS_SAMPLED_LIFETIME, + NUM_ENV_STEPS_SAMPLED, + NUM_ENV_STEPS_SAMPLED_LIFETIME, + NUM_TARGET_UPDATES, + REPLAY_BUFFER_ADD_DATA_TIMER, + REPLAY_BUFFER_RESULTS, + REPLAY_BUFFER_SAMPLE_TIMER, + REPLAY_BUFFER_UPDATE_PRIOS_TIMER, + SAMPLE_TIMER, + SYNCH_WORKER_WEIGHTS_TIMER, + TD_ERROR_KEY, + TIMERS, +) +from ray.rllib.utils.numpy import convert_to_numpy +from ray.rllib.utils.replay_buffers.utils import ( + sample_min_n_steps_from_buffer, + update_priorities_in_episode_replay_buffer, + update_priorities_in_replay_buffer, + validate_buffer_config, +) +from ray.rllib.utils.typing import ( + LearningRateOrSchedule, + ResultDict, + RLModuleSpecType, + SampleBatchType, +) + +logger = logging.getLogger(__name__) + + +class DQNConfig(AlgorithmConfig): + r"""Defines a configuration class from which a DQN Algorithm can be built. + + .. testcode:: + + from ray.rllib.algorithms.dqn.dqn import DQNConfig + + config = ( + DQNConfig() + .environment("CartPole-v1") + .training(replay_buffer_config={ + "type": "PrioritizedEpisodeReplayBuffer", + "capacity": 60000, + "alpha": 0.5, + "beta": 0.5, + }) + .env_runners(num_env_runners=1) + ) + algo = config.build() + algo.train() + algo.stop() + + .. testcode:: + + from ray.rllib.algorithms.dqn.dqn import DQNConfig + from ray import tune + + config = ( + DQNConfig() + .environment("CartPole-v1") + .training( + num_atoms=tune.grid_search([1,]) + ) + ) + tune.Tuner( + "DQN", + run_config=tune.RunConfig(stop={"training_iteration":1}), + param_space=config, + ).fit() + + .. testoutput:: + :hide: + + ... + + + """ + + def __init__(self, algo_class=None): + """Initializes a DQNConfig instance.""" + self.exploration_config = { + "type": "EpsilonGreedy", + "initial_epsilon": 1.0, + "final_epsilon": 0.02, + "epsilon_timesteps": 10000, + } + + super().__init__(algo_class=algo_class or DQN) + + # Overrides of AlgorithmConfig defaults + # `env_runners()` + # Set to `self.n_step`, if 'auto'. + self.rollout_fragment_length: Union[int, str] = "auto" + # New stack uses `epsilon` as either a constant value or a scheduler + # defined like this. + # TODO (simon): Ensure that users can understand how to provide epsilon. + # (sven): Should we add this to `self.env_runners(epsilon=..)`? + self.epsilon = [(0, 1.0), (10000, 0.05)] + + # `training()` + self.grad_clip = 40.0 + # Note: Only when using enable_rl_module_and_learner=True can the clipping mode + # be configured by the user. On the old API stack, RLlib will always clip by + # global_norm, no matter the value of `grad_clip_by`. + self.grad_clip_by = "global_norm" + self.lr = 5e-4 + self.train_batch_size = 32 + + # `evaluation()` + self.evaluation(evaluation_config=AlgorithmConfig.overrides(explore=False)) + + # `reporting()` + self.min_time_s_per_iteration = None + self.min_sample_timesteps_per_iteration = 1000 + + # DQN specific config settings. + # fmt: off + # __sphinx_doc_begin__ + self.target_network_update_freq = 500 + self.num_steps_sampled_before_learning_starts = 1000 + self.store_buffer_in_checkpoints = False + self.adam_epsilon = 1e-8 + + self.tau = 1.0 + + self.num_atoms = 1 + self.v_min = -10.0 + self.v_max = 10.0 + self.noisy = False + self.sigma0 = 0.5 + self.dueling = True + self.hiddens = [256] + self.double_q = True + self.n_step = 1 + self.before_learn_on_batch = None + self.training_intensity = None + self.td_error_loss_fn = "huber" + self.categorical_distribution_temperature = 1.0 + # The burn-in for stateful `RLModule`s. + self.burn_in_len = 0 + + # Replay buffer configuration. + self.replay_buffer_config = { + "type": "PrioritizedEpisodeReplayBuffer", + # Size of the replay buffer. Note that if async_updates is set, + # then each worker will have a replay buffer of this size. + "capacity": 50000, + "alpha": 0.6, + # Beta parameter for sampling from prioritized replay buffer. + "beta": 0.4, + } + # fmt: on + # __sphinx_doc_end__ + + self.lr_schedule = None # @OldAPIStack + + # Deprecated + self.buffer_size = DEPRECATED_VALUE + self.prioritized_replay = DEPRECATED_VALUE + self.learning_starts = DEPRECATED_VALUE + self.replay_batch_size = DEPRECATED_VALUE + # Can not use DEPRECATED_VALUE here because -1 is a common config value + self.replay_sequence_length = None + self.prioritized_replay_alpha = DEPRECATED_VALUE + self.prioritized_replay_beta = DEPRECATED_VALUE + self.prioritized_replay_eps = DEPRECATED_VALUE + + @override(AlgorithmConfig) + def training( + self, + *, + target_network_update_freq: Optional[int] = NotProvided, + replay_buffer_config: Optional[dict] = NotProvided, + store_buffer_in_checkpoints: Optional[bool] = NotProvided, + lr_schedule: Optional[List[List[Union[int, float]]]] = NotProvided, + epsilon: Optional[LearningRateOrSchedule] = NotProvided, + adam_epsilon: Optional[float] = NotProvided, + grad_clip: Optional[int] = NotProvided, + num_steps_sampled_before_learning_starts: Optional[int] = NotProvided, + tau: Optional[float] = NotProvided, + num_atoms: Optional[int] = NotProvided, + v_min: Optional[float] = NotProvided, + v_max: Optional[float] = NotProvided, + noisy: Optional[bool] = NotProvided, + sigma0: Optional[float] = NotProvided, + dueling: Optional[bool] = NotProvided, + hiddens: Optional[int] = NotProvided, + double_q: Optional[bool] = NotProvided, + n_step: Optional[Union[int, Tuple[int, int]]] = NotProvided, + before_learn_on_batch: Callable[ + [Type[MultiAgentBatch], List[Type[Policy]], Type[int]], + Type[MultiAgentBatch], + ] = NotProvided, + training_intensity: Optional[float] = NotProvided, + td_error_loss_fn: Optional[str] = NotProvided, + categorical_distribution_temperature: Optional[float] = NotProvided, + burn_in_len: Optional[int] = NotProvided, + **kwargs, + ) -> Self: + """Sets the training related configuration. + + Args: + target_network_update_freq: Update the target network every + `target_network_update_freq` sample steps. + replay_buffer_config: Replay buffer config. + Examples: + { + "_enable_replay_buffer_api": True, + "type": "MultiAgentReplayBuffer", + "capacity": 50000, + "replay_sequence_length": 1, + } + - OR - + { + "_enable_replay_buffer_api": True, + "type": "MultiAgentPrioritizedReplayBuffer", + "capacity": 50000, + "prioritized_replay_alpha": 0.6, + "prioritized_replay_beta": 0.4, + "prioritized_replay_eps": 1e-6, + "replay_sequence_length": 1, + } + - Where - + prioritized_replay_alpha: Alpha parameter controls the degree of + prioritization in the buffer. In other words, when a buffer sample has + a higher temporal-difference error, with how much more probability + should it drawn to use to update the parametrized Q-network. 0.0 + corresponds to uniform probability. Setting much above 1.0 may quickly + result as the sampling distribution could become heavily “pointy” with + low entropy. + prioritized_replay_beta: Beta parameter controls the degree of + importance sampling which suppresses the influence of gradient updates + from samples that have higher probability of being sampled via alpha + parameter and the temporal-difference error. + prioritized_replay_eps: Epsilon parameter sets the baseline probability + for sampling so that when the temporal-difference error of a sample is + zero, there is still a chance of drawing the sample. + store_buffer_in_checkpoints: Set this to True, if you want the contents of + your buffer(s) to be stored in any saved checkpoints as well. + Warnings will be created if: + - This is True AND restoring from a checkpoint that contains no buffer + data. + - This is False AND restoring from a checkpoint that does contain + buffer data. + epsilon: Epsilon exploration schedule. In the format of [[timestep, value], + [timestep, value], ...]. A schedule must start from + timestep 0. + adam_epsilon: Adam optimizer's epsilon hyper parameter. + grad_clip: If not None, clip gradients during optimization at this value. + num_steps_sampled_before_learning_starts: Number of timesteps to collect + from rollout workers before we start sampling from replay buffers for + learning. Whether we count this in agent steps or environment steps + depends on config.multi_agent(count_steps_by=..). + tau: Update the target by \tau * policy + (1-\tau) * target_policy. + num_atoms: Number of atoms for representing the distribution of return. + When this is greater than 1, distributional Q-learning is used. + v_min: Minimum value estimation + v_max: Maximum value estimation + noisy: Whether to use noisy network to aid exploration. This adds parametric + noise to the model weights. + sigma0: Control the initial parameter noise for noisy nets. + dueling: Whether to use dueling DQN. + hiddens: Dense-layer setup for each the advantage branch and the value + branch + double_q: Whether to use double DQN. + n_step: N-step target updates. If >1, sars' tuples in trajectories will be + postprocessed to become sa[discounted sum of R][s t+n] tuples. An + integer will be interpreted as a fixed n-step value. If a tuple of 2 + ints is provided here, the n-step value will be drawn for each sample(!) + in the train batch from a uniform distribution over the closed interval + defined by `[n_step[0], n_step[1]]`. + before_learn_on_batch: Callback to run before learning on a multi-agent + batch of experiences. + training_intensity: The intensity with which to update the model (vs + collecting samples from the env). + If None, uses "natural" values of: + `train_batch_size` / (`rollout_fragment_length` x `num_env_runners` x + `num_envs_per_env_runner`). + If not None, will make sure that the ratio between timesteps inserted + into and sampled from the buffer matches the given values. + Example: + training_intensity=1000.0 + train_batch_size=250 + rollout_fragment_length=1 + num_env_runners=1 (or 0) + num_envs_per_env_runner=1 + -> natural value = 250 / 1 = 250.0 + -> will make sure that replay+train op will be executed 4x asoften as + rollout+insert op (4 * 250 = 1000). + See: rllib/algorithms/dqn/dqn.py::calculate_rr_weights for further + details. + td_error_loss_fn: "huber" or "mse". loss function for calculating TD error + when num_atoms is 1. Note that if num_atoms is > 1, this parameter + is simply ignored, and softmax cross entropy loss will be used. + categorical_distribution_temperature: Set the temperature parameter used + by Categorical action distribution. A valid temperature is in the range + of [0, 1]. Note that this mostly affects evaluation since TD error uses + argmax for return calculation. + burn_in_len: The burn-in period for a stateful RLModule. It allows the + Learner to utilize the initial `burn_in_len` steps in a replay sequence + solely for unrolling the network and establishing a typical starting + state. The network is then updated on the remaining steps of the + sequence. This process helps mitigate issues stemming from a poor + initial state - zero or an outdated recorded state. Consider setting + this parameter to a positive integer if your stateful RLModule faces + convergence challenges or exhibits signs of catastrophic forgetting. + + Returns: + This updated AlgorithmConfig object. + """ + # Pass kwargs onto super's `training()` method. + super().training(**kwargs) + + if target_network_update_freq is not NotProvided: + self.target_network_update_freq = target_network_update_freq + if replay_buffer_config is not NotProvided: + # Override entire `replay_buffer_config` if `type` key changes. + # Update, if `type` key remains the same or is not specified. + new_replay_buffer_config = deep_update( + {"replay_buffer_config": self.replay_buffer_config}, + {"replay_buffer_config": replay_buffer_config}, + False, + ["replay_buffer_config"], + ["replay_buffer_config"], + ) + self.replay_buffer_config = new_replay_buffer_config["replay_buffer_config"] + if store_buffer_in_checkpoints is not NotProvided: + self.store_buffer_in_checkpoints = store_buffer_in_checkpoints + if lr_schedule is not NotProvided: + self.lr_schedule = lr_schedule + if epsilon is not NotProvided: + self.epsilon = epsilon + if adam_epsilon is not NotProvided: + self.adam_epsilon = adam_epsilon + if grad_clip is not NotProvided: + self.grad_clip = grad_clip + if num_steps_sampled_before_learning_starts is not NotProvided: + self.num_steps_sampled_before_learning_starts = ( + num_steps_sampled_before_learning_starts + ) + if tau is not NotProvided: + self.tau = tau + if num_atoms is not NotProvided: + self.num_atoms = num_atoms + if v_min is not NotProvided: + self.v_min = v_min + if v_max is not NotProvided: + self.v_max = v_max + if noisy is not NotProvided: + self.noisy = noisy + if sigma0 is not NotProvided: + self.sigma0 = sigma0 + if dueling is not NotProvided: + self.dueling = dueling + if hiddens is not NotProvided: + self.hiddens = hiddens + if double_q is not NotProvided: + self.double_q = double_q + if n_step is not NotProvided: + self.n_step = n_step + if before_learn_on_batch is not NotProvided: + self.before_learn_on_batch = before_learn_on_batch + if training_intensity is not NotProvided: + self.training_intensity = training_intensity + if td_error_loss_fn is not NotProvided: + self.td_error_loss_fn = td_error_loss_fn + if categorical_distribution_temperature is not NotProvided: + self.categorical_distribution_temperature = ( + categorical_distribution_temperature + ) + if burn_in_len is not NotProvided: + self.burn_in_len = burn_in_len + + return self + + @override(AlgorithmConfig) + def validate(self) -> None: + # Call super's validation method. + super().validate() + + if self.enable_rl_module_and_learner: + # `lr_schedule` checking. + if self.lr_schedule is not None: + self._value_error( + "`lr_schedule` is deprecated and must be None! Use the " + "`lr` setting to setup a schedule." + ) + else: + if not self.in_evaluation: + validate_buffer_config(self) + + # TODO (simon): Find a clean solution to deal with configuration configs + # when using the new API stack. + if self.exploration_config["type"] == "ParameterNoise": + if self.batch_mode != "complete_episodes": + self._value_error( + "ParameterNoise Exploration requires `batch_mode` to be " + "'complete_episodes'. Try setting `config.env_runners(" + "batch_mode='complete_episodes')`." + ) + if self.noisy: + self._value_error( + "ParameterNoise Exploration and `noisy` network cannot be" + " used at the same time!" + ) + + if self.td_error_loss_fn not in ["huber", "mse"]: + self._value_error("`td_error_loss_fn` must be 'huber' or 'mse'!") + + # Check rollout_fragment_length to be compatible with n_step. + if ( + not self.in_evaluation + and self.rollout_fragment_length != "auto" + and self.rollout_fragment_length < self.n_step + ): + self._value_error( + f"Your `rollout_fragment_length` ({self.rollout_fragment_length}) is " + f"smaller than `n_step` ({self.n_step})! " + "Try setting config.env_runners(rollout_fragment_length=" + f"{self.n_step})." + ) + + # Check, if the `max_seq_len` is longer then the burn-in. + if ( + "max_seq_len" in self.model_config + and 0 < self.model_config["max_seq_len"] <= self.burn_in_len + ): + raise ValueError( + f"Your defined `burn_in_len`={self.burn_in_len} is larger or equal " + f"`max_seq_len`={self.model_config['max_seq_len']}! Either decrease " + "the `burn_in_len` or increase your `max_seq_len`." + ) + + # Validate that we use the corresponding `EpisodeReplayBuffer` when using + # episodes. + # TODO (sven, simon): Implement the multi-agent case for replay buffers. + from ray.rllib.utils.replay_buffers.episode_replay_buffer import ( + EpisodeReplayBuffer, + ) + + if ( + self.enable_env_runner_and_connector_v2 + and not isinstance(self.replay_buffer_config["type"], str) + and not issubclass(self.replay_buffer_config["type"], EpisodeReplayBuffer) + ): + self._value_error( + "When using the new `EnvRunner API` the replay buffer must be of type " + "`EpisodeReplayBuffer`." + ) + elif not self.enable_env_runner_and_connector_v2 and ( + ( + isinstance(self.replay_buffer_config["type"], str) + and "Episode" in self.replay_buffer_config["type"] + ) + or issubclass(self.replay_buffer_config["type"], EpisodeReplayBuffer) + ): + self._value_error( + "When using the old API stack the replay buffer must not be of type " + "`EpisodeReplayBuffer`! We suggest you use the following config to run " + "DQN on the old API stack: `config.training(replay_buffer_config={" + "'type': 'MultiAgentPrioritizedReplayBuffer', " + "'prioritized_replay_alpha': [alpha], " + "'prioritized_replay_beta': [beta], " + "'prioritized_replay_eps': [eps], " + "})`." + ) + + @override(AlgorithmConfig) + def get_rollout_fragment_length(self, worker_index: int = 0) -> int: + if self.rollout_fragment_length == "auto": + return ( + self.n_step[1] + if isinstance(self.n_step, (tuple, list)) + else self.n_step + ) + else: + return self.rollout_fragment_length + + @override(AlgorithmConfig) + def get_default_rl_module_spec(self) -> RLModuleSpecType: + if self.framework_str == "torch": + from ray.rllib.algorithms.dqn.torch.default_dqn_torch_rl_module import ( + DefaultDQNTorchRLModule, + ) + + return RLModuleSpec( + module_class=DefaultDQNTorchRLModule, + model_config=self.model_config, + ) + else: + raise ValueError( + f"The framework {self.framework_str} is not supported! " + "Use `config.framework('torch')` instead." + ) + + @property + @override(AlgorithmConfig) + def _model_config_auto_includes(self) -> Dict[str, Any]: + return super()._model_config_auto_includes | { + "double_q": self.double_q, + "dueling": self.dueling, + "epsilon": self.epsilon, + "num_atoms": self.num_atoms, + "std_init": self.sigma0, + "v_max": self.v_max, + "v_min": self.v_min, + } + + @override(AlgorithmConfig) + def get_default_learner_class(self) -> Union[Type["Learner"], str]: + if self.framework_str == "torch": + from ray.rllib.algorithms.dqn.torch.dqn_torch_learner import ( + DQNTorchLearner, + ) + + return DQNTorchLearner + else: + raise ValueError( + f"The framework {self.framework_str} is not supported! " + "Use `config.framework('torch')` instead." + ) + + +def calculate_rr_weights(config: AlgorithmConfig) -> List[float]: + """Calculate the round robin weights for the rollout and train steps""" + if not config.training_intensity: + return [1, 1] + + # Calculate the "native ratio" as: + # [train-batch-size] / [size of env-rolled-out sampled data] + # This is to set freshly rollout-collected data in relation to + # the data we pull from the replay buffer (which also contains old + # samples). + native_ratio = config.total_train_batch_size / ( + config.get_rollout_fragment_length() + * config.num_envs_per_env_runner + # Add one to workers because the local + # worker usually collects experiences as well, and we avoid division by zero. + * max(config.num_env_runners + 1, 1) + ) + + # Training intensity is specified in terms of + # (steps_replayed / steps_sampled), so adjust for the native ratio. + sample_and_train_weight = config.training_intensity / native_ratio + if sample_and_train_weight < 1: + return [int(np.round(1 / sample_and_train_weight)), 1] + else: + return [1, int(np.round(sample_and_train_weight))] + + +class DQN(Algorithm): + @classmethod + @override(Algorithm) + def get_default_config(cls) -> DQNConfig: + return DQNConfig() + + @classmethod + @override(Algorithm) + def get_default_policy_class( + cls, config: AlgorithmConfig + ) -> Optional[Type[Policy]]: + if config["framework"] == "torch": + return DQNTorchPolicy + else: + return DQNTFPolicy + + @override(Algorithm) + def setup(self, config: AlgorithmConfig) -> None: + super().setup(config) + + if self.config.enable_env_runner_and_connector_v2 and self.env_runner_group: + if self.env_runner is None: + self._module_is_stateful = self.env_runner_group.foreach_env_runner( + lambda er: er.module.is_stateful(), + remote_worker_ids=[1], + local_env_runner=False, + )[0] + else: + self._module_is_stateful = self.env_runner.module.is_stateful() + + @override(Algorithm) + def training_step(self) -> None: + """DQN training iteration function. + + Each training iteration, we: + - Sample (MultiAgentBatch) from workers. + - Store new samples in replay buffer. + - Sample training batch (MultiAgentBatch) from replay buffer. + - Learn on training batch. + - Update remote workers' new policy weights. + - Update target network every `target_network_update_freq` sample steps. + - Return all collected metrics for the iteration. + + Returns: + The results dict from executing the training iteration. + """ + # Old API stack (Policy, RolloutWorker, Connector). + if not self.config.enable_env_runner_and_connector_v2: + return self._training_step_old_api_stack() + + # New API stack (RLModule, Learner, EnvRunner, ConnectorV2). + return self._training_step_new_api_stack() + + def _training_step_new_api_stack(self): + # Alternate between storing and sampling and training. + store_weight, sample_and_train_weight = calculate_rr_weights(self.config) + + # Run multiple sampling + storing to buffer iterations. + for _ in range(store_weight): + with self.metrics.log_time((TIMERS, ENV_RUNNER_SAMPLING_TIMER)): + # Sample in parallel from workers. + episodes, env_runner_results = synchronous_parallel_sample( + worker_set=self.env_runner_group, + concat=True, + sample_timeout_s=self.config.sample_timeout_s, + _uses_new_env_runners=True, + _return_metrics=True, + ) + # Reduce EnvRunner metrics over the n EnvRunners. + self.metrics.aggregate(env_runner_results, key=ENV_RUNNER_RESULTS) + + # Add the sampled experiences to the replay buffer. + with self.metrics.log_time((TIMERS, REPLAY_BUFFER_ADD_DATA_TIMER)): + self.local_replay_buffer.add(episodes) + + if self.config.count_steps_by == "agent_steps": + current_ts = sum( + self.metrics.peek( + (ENV_RUNNER_RESULTS, NUM_AGENT_STEPS_SAMPLED_LIFETIME), default={} + ).values() + ) + else: + current_ts = self.metrics.peek( + (ENV_RUNNER_RESULTS, NUM_ENV_STEPS_SAMPLED_LIFETIME), default=0 + ) + + # If enough experiences have been sampled start training. + if current_ts >= self.config.num_steps_sampled_before_learning_starts: + # Run multiple sample-from-buffer and update iterations. + for _ in range(sample_and_train_weight): + # Sample a list of episodes used for learning from the replay buffer. + with self.metrics.log_time((TIMERS, REPLAY_BUFFER_SAMPLE_TIMER)): + + episodes = self.local_replay_buffer.sample( + num_items=self.config.total_train_batch_size, + n_step=self.config.n_step, + # In case an `EpisodeReplayBuffer` is used we need to provide + # the sequence length. + batch_length_T=( + self._module_is_stateful + * self.config.model_config.get("max_seq_len", 0) + ), + lookback=int(self._module_is_stateful), + # TODO (simon): Implement `burn_in_len` in SAC and remove this + # if-else clause. + min_batch_length_T=self.config.burn_in_len + if hasattr(self.config, "burn_in_len") + else 0, + gamma=self.config.gamma, + beta=self.config.replay_buffer_config.get("beta"), + sample_episodes=True, + ) + + # Get the replay buffer metrics. + replay_buffer_results = self.local_replay_buffer.get_metrics() + self.metrics.aggregate( + [replay_buffer_results], key=REPLAY_BUFFER_RESULTS + ) + + # Perform an update on the buffer-sampled train batch. + with self.metrics.log_time((TIMERS, LEARNER_UPDATE_TIMER)): + learner_results = self.learner_group.update( + episodes=episodes, + timesteps={ + NUM_ENV_STEPS_SAMPLED_LIFETIME: ( + self.metrics.peek( + (ENV_RUNNER_RESULTS, NUM_ENV_STEPS_SAMPLED_LIFETIME) + ) + ), + NUM_AGENT_STEPS_SAMPLED_LIFETIME: ( + self.metrics.peek( + ( + ENV_RUNNER_RESULTS, + NUM_AGENT_STEPS_SAMPLED_LIFETIME, + ) + ) + ), + }, + ) + # Isolate TD-errors from result dicts (we should not log these to + # disk or WandB, they might be very large). + td_errors = defaultdict(list) + for res in learner_results: + for module_id, module_results in res.items(): + if TD_ERROR_KEY in module_results: + td_errors[module_id].extend( + convert_to_numpy( + module_results.pop(TD_ERROR_KEY).peek() + ) + ) + td_errors = { + module_id: {TD_ERROR_KEY: np.concatenate(s, axis=0)} + for module_id, s in td_errors.items() + } + self.metrics.aggregate(learner_results, key=LEARNER_RESULTS) + + # Update replay buffer priorities. + with self.metrics.log_time((TIMERS, REPLAY_BUFFER_UPDATE_PRIOS_TIMER)): + update_priorities_in_episode_replay_buffer( + replay_buffer=self.local_replay_buffer, + td_errors=td_errors, + ) + + # Update weights and global_vars - after learning on the local worker - + # on all remote workers. + with self.metrics.log_time((TIMERS, SYNCH_WORKER_WEIGHTS_TIMER)): + modules_to_update = set(learner_results[0].keys()) - {ALL_MODULES} + # NOTE: the new API stack does not use global vars. + self.env_runner_group.sync_weights( + from_worker_or_learner_group=self.learner_group, + policies=modules_to_update, + global_vars=None, + inference_only=True, + ) + + def _training_step_old_api_stack(self) -> ResultDict: + """Training step for the old API stack. + + More specifically this training step relies on `RolloutWorker`. + """ + train_results = {} + + # We alternate between storing new samples and sampling and training + store_weight, sample_and_train_weight = calculate_rr_weights(self.config) + + for _ in range(store_weight): + # Sample (MultiAgentBatch) from workers. + with self._timers[SAMPLE_TIMER]: + new_sample_batch: SampleBatchType = synchronous_parallel_sample( + worker_set=self.env_runner_group, + concat=True, + sample_timeout_s=self.config.sample_timeout_s, + ) + + # Return early if all our workers failed. + if not new_sample_batch: + return {} + + # Update counters + self._counters[NUM_AGENT_STEPS_SAMPLED] += new_sample_batch.agent_steps() + self._counters[NUM_ENV_STEPS_SAMPLED] += new_sample_batch.env_steps() + + # Store new samples in replay buffer. + self.local_replay_buffer.add(new_sample_batch) + + global_vars = { + "timestep": self._counters[NUM_ENV_STEPS_SAMPLED], + } + + # Update target network every `target_network_update_freq` sample steps. + cur_ts = self._counters[ + ( + NUM_AGENT_STEPS_SAMPLED + if self.config.count_steps_by == "agent_steps" + else NUM_ENV_STEPS_SAMPLED + ) + ] + + if cur_ts > self.config.num_steps_sampled_before_learning_starts: + for _ in range(sample_and_train_weight): + # Sample training batch (MultiAgentBatch) from replay buffer. + train_batch = sample_min_n_steps_from_buffer( + self.local_replay_buffer, + self.config.total_train_batch_size, + count_by_agent_steps=self.config.count_steps_by == "agent_steps", + ) + + # Postprocess batch before we learn on it + post_fn = self.config.get("before_learn_on_batch") or (lambda b, *a: b) + train_batch = post_fn(train_batch, self.env_runner_group, self.config) + + # Learn on training batch. + # Use simple optimizer (only for multi-agent or tf-eager; all other + # cases should use the multi-GPU optimizer, even if only using 1 GPU) + if self.config.get("simple_optimizer") is True: + train_results = train_one_step(self, train_batch) + else: + train_results = multi_gpu_train_one_step(self, train_batch) + + # Update replay buffer priorities. + update_priorities_in_replay_buffer( + self.local_replay_buffer, + self.config, + train_batch, + train_results, + ) + + last_update = self._counters[LAST_TARGET_UPDATE_TS] + if cur_ts - last_update >= self.config.target_network_update_freq: + to_update = self.env_runner.get_policies_to_train() + self.env_runner.foreach_policy_to_train( + lambda p, pid, to_update=to_update: ( + pid in to_update and p.update_target() + ) + ) + self._counters[NUM_TARGET_UPDATES] += 1 + self._counters[LAST_TARGET_UPDATE_TS] = cur_ts + + # Update weights and global_vars - after learning on the local worker - + # on all remote workers. + with self._timers[SYNCH_WORKER_WEIGHTS_TIMER]: + self.env_runner_group.sync_weights(global_vars=global_vars) + + # Return all collected metrics for the iteration. + return train_results diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_catalog.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_catalog.py new file mode 100644 index 0000000000000000000000000000000000000000..32c7cf1c063fca11ffe9bf72fab543e63aa9b6bc --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_catalog.py @@ -0,0 +1,179 @@ +import gymnasium as gym + +from ray.rllib.core.distribution.torch.torch_distribution import TorchCategorical +from ray.rllib.core.models.base import Model +from ray.rllib.core.models.catalog import Catalog +from ray.rllib.core.models.configs import MLPHeadConfig +from ray.rllib.utils.annotations import ( + ExperimentalAPI, + OverrideToImplementCustomLogic, + override, +) + + +@ExperimentalAPI +class DQNCatalog(Catalog): + """The catalog class used to build models for DQN Rainbow. + + `DQNCatalog` provides the following models: + - Encoder: The encoder used to encode the observations. + - Target_Encoder: The encoder used to encode the observations + for the target network. + - Af Head: Either the head of the advantage stream, if a dueling + architecture is used or the head of the Q-function. This is + a multi-node head with `action_space.n` many nodes in case + of expectation learning and `action_space.n` times the number + of atoms (`num_atoms`) in case of distributional Q-learning. + - Vf Head (optional): The head of the value function in case a + dueling architecture is chosen. This is a single node head. + If no dueling architecture is used, this head does not exist. + + Any custom head can be built by overridng the `build_af_head()` and + `build_vf_head()`. Alternatively, the `AfHeadConfig` or `VfHeadConfig` + can be overridden to build custom logic during `RLModule` runtime. + + All heads can optionally use distributional learning. In this case the + number of output neurons corresponds to the number of actions times the + number of support atoms of the discrete distribution. + + Any module built for exploration or inference is built with the flag + `ìnference_only=True` and does not contain any target networks. This flag can + be set in a `SingleAgentModuleSpec` through the `inference_only` boolean flag. + """ + + @override(Catalog) + def __init__( + self, + observation_space: gym.Space, + action_space: gym.Space, + model_config_dict: dict, + view_requirements: dict = None, + ): + """Initializes the DQNCatalog. + + Args: + observation_space: The observation space of the Encoder. + action_space: The action space for the Af Head. + model_config_dict: The model config to use. + """ + assert view_requirements is None, ( + "Instead, use the new ConnectorV2 API to pick whatever information " + "you need from the running episodes" + ) + + super().__init__( + observation_space=observation_space, + action_space=action_space, + model_config_dict=model_config_dict, + ) + + # The number of atoms to be used for distributional Q-learning. + self.num_atoms: bool = self._model_config_dict["num_atoms"] + + # Advantage and value streams have MLP heads. Note, the advantage + # stream will has an output dimension that is the product of the + # action space dimension and the number of atoms to approximate the + # return distribution in distributional reinforcement learning. + self.af_head_config = self._get_head_config( + output_layer_dim=int(self.action_space.n * self.num_atoms) + ) + self.vf_head_config = self._get_head_config(output_layer_dim=1) + + @OverrideToImplementCustomLogic + def build_af_head(self, framework: str) -> Model: + """Build the A/Q-function head. + + Note, if no dueling architecture is chosen, this will + be the Q-function head. + + The default behavior is to build the head from the `af_head_config`. + This can be overridden to build a custom policy head as a means to + configure the behavior of a `DQNRLModule` implementation. + + Args: + framework: The framework to use. Either "torch" or "tf2". + + Returns: + The advantage head in case a dueling architecutre is chosen or + the Q-function head in the other case. + """ + return self.af_head_config.build(framework=framework) + + @OverrideToImplementCustomLogic + def build_vf_head(self, framework: str) -> Model: + """Build the value function head. + + Note, this function is only called in case of a dueling architecture. + + The default behavior is to build the head from the `vf_head_config`. + This can be overridden to build a custom policy head as a means to + configure the behavior of a `DQNRLModule` implementation. + + Args: + framework: The framework to use. Either "torch" or "tf2". + + Returns: + The value function head. + """ + + return self.vf_head_config.build(framework=framework) + + @override(Catalog) + def get_action_dist_cls(self, framework: str) -> "TorchCategorical": + # We only implement DQN Rainbow for Torch. + if framework != "torch": + raise ValueError("DQN Rainbow is only supported for framework `torch`.") + else: + return TorchCategorical + + def _get_head_config(self, output_layer_dim: int): + """Returns a head config. + + Args: + output_layer_dim: Integer defining the output layer dimension. + This is 1 for the Vf-head and `action_space.n * num_atoms` + for the Af(Qf)-head. + + Returns: + A `MLPHeadConfig`. + """ + # Return the appropriate config. + return MLPHeadConfig( + input_dims=self.latent_dims, + hidden_layer_dims=self._model_config_dict["head_fcnet_hiddens"], + # Note, `"post_fcnet_activation"` is `"relu"` by definition. + hidden_layer_activation=self._model_config_dict["head_fcnet_activation"], + # TODO (simon): Not yet available. + # hidden_layer_use_layernorm=self._model_config_dict[ + # "hidden_layer_use_layernorm" + # ], + # hidden_layer_use_bias=self._model_config_dict["hidden_layer_use_bias"], + hidden_layer_weights_initializer=self._model_config_dict[ + "head_fcnet_kernel_initializer" + ], + hidden_layer_weights_initializer_config=self._model_config_dict[ + "head_fcnet_kernel_initializer_kwargs" + ], + hidden_layer_bias_initializer=self._model_config_dict[ + "head_fcnet_bias_initializer" + ], + hidden_layer_bias_initializer_config=self._model_config_dict[ + "head_fcnet_bias_initializer_kwargs" + ], + output_layer_activation="linear", + output_layer_dim=output_layer_dim, + # TODO (simon): Not yet available. + # output_layer_use_bias=self._model_config_dict["output_layer_use_bias"], + output_layer_weights_initializer=self._model_config_dict[ + "head_fcnet_kernel_initializer" + ], + output_layer_weights_initializer_config=self._model_config_dict[ + "head_fcnet_kernel_initializer_kwargs" + ], + output_layer_bias_initializer=self._model_config_dict[ + "head_fcnet_bias_initializer" + ], + output_layer_bias_initializer_config=self._model_config_dict[ + "head_fcnet_bias_initializer_kwargs" + ], + ) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_learner.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_learner.py new file mode 100644 index 0000000000000000000000000000000000000000..64bc51969a75e345e9251f557ca54d7d3c18ab53 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_learner.py @@ -0,0 +1,119 @@ +from typing import Any, Dict, Optional + +from ray.rllib.connectors.common.add_observations_from_episodes_to_batch import ( + AddObservationsFromEpisodesToBatch, +) +from ray.rllib.connectors.learner.add_next_observations_from_episodes_to_train_batch import ( # noqa + AddNextObservationsFromEpisodesToTrainBatch, +) +from ray.rllib.core.learner.learner import Learner +from ray.rllib.core.learner.utils import update_target_network +from ray.rllib.core.rl_module.apis import QNetAPI, TargetNetworkAPI +from ray.rllib.core.rl_module.multi_rl_module import MultiRLModuleSpec +from ray.rllib.core.rl_module.rl_module import RLModuleSpec +from ray.rllib.utils.annotations import ( + OverrideToImplementCustomLogic_CallToSuperRecommended, + override, +) +from ray.rllib.utils.metrics import ( + LAST_TARGET_UPDATE_TS, + NUM_ENV_STEPS_SAMPLED_LIFETIME, + NUM_TARGET_UPDATES, +) +from ray.rllib.utils.typing import ModuleID, ShouldModuleBeUpdatedFn + +# Now, this is double defined: In `SACRLModule` and here. I would keep it here +# or push it into the `Learner` as these are recurring keys in RL. +ATOMS = "atoms" +QF_LOSS_KEY = "qf_loss" +QF_LOGITS = "qf_logits" +QF_MEAN_KEY = "qf_mean" +QF_MAX_KEY = "qf_max" +QF_MIN_KEY = "qf_min" +QF_NEXT_PREDS = "qf_next_preds" +QF_TARGET_NEXT_PREDS = "qf_target_next_preds" +QF_TARGET_NEXT_PROBS = "qf_target_next_probs" +QF_PREDS = "qf_preds" +QF_PROBS = "qf_probs" +TD_ERROR_MEAN_KEY = "td_error_mean" + + +class DQNLearner(Learner): + @OverrideToImplementCustomLogic_CallToSuperRecommended + @override(Learner) + def build(self) -> None: + super().build() + + # Make target networks. + self.module.foreach_module( + lambda mid, mod: ( + mod.make_target_networks() + if isinstance(mod, TargetNetworkAPI) + else None + ) + ) + + # Prepend the "add-NEXT_OBS-from-episodes-to-train-batch" connector piece (right + # after the corresponding "add-OBS-..." default piece). + self._learner_connector.insert_after( + AddObservationsFromEpisodesToBatch, + AddNextObservationsFromEpisodesToTrainBatch(), + ) + + @override(Learner) + def add_module( + self, + *, + module_id: ModuleID, + module_spec: RLModuleSpec, + config_overrides: Optional[Dict] = None, + new_should_module_be_updated: Optional[ShouldModuleBeUpdatedFn] = None, + ) -> MultiRLModuleSpec: + marl_spec = super().add_module( + module_id=module_id, + module_spec=module_spec, + config_overrides=config_overrides, + new_should_module_be_updated=new_should_module_be_updated, + ) + # Create target networks for added Module, if applicable. + if isinstance(self.module[module_id].unwrapped(), TargetNetworkAPI): + self.module[module_id].unwrapped().make_target_networks() + return marl_spec + + @override(Learner) + def after_gradient_based_update(self, *, timesteps: Dict[str, Any]) -> None: + """Updates the target Q Networks.""" + super().after_gradient_based_update(timesteps=timesteps) + + timestep = timesteps.get(NUM_ENV_STEPS_SAMPLED_LIFETIME, 0) + + # TODO (sven): Maybe we should have a `after_gradient_based_update` + # method per module? + for module_id, module in self.module._rl_modules.items(): + config = self.config.get_config_for_module(module_id) + last_update_ts_key = (module_id, LAST_TARGET_UPDATE_TS) + if timestep - self.metrics.peek( + last_update_ts_key, default=0 + ) >= config.target_network_update_freq and isinstance( + module.unwrapped(), TargetNetworkAPI + ): + for ( + main_net, + target_net, + ) in module.unwrapped().get_target_network_pairs(): + update_target_network( + main_net=main_net, + target_net=target_net, + tau=config.tau, + ) + # Increase lifetime target network update counter by one. + self.metrics.log_value((module_id, NUM_TARGET_UPDATES), 1, reduce="sum") + # Update the (single-value -> window=1) last updated timestep metric. + self.metrics.log_value(last_update_ts_key, timestep, window=1) + + @classmethod + @override(Learner) + def rl_module_required_apis(cls) -> list[type]: + # In order for a PPOLearner to update an RLModule, it must implement the + # following APIs: + return [QNetAPI, TargetNetworkAPI] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_tf_policy.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_tf_policy.py new file mode 100644 index 0000000000000000000000000000000000000000..4affad4b0043c50f45ffc3ae0afec10163f49cd5 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_tf_policy.py @@ -0,0 +1,511 @@ +from typing import Dict + +import gymnasium as gym +import numpy as np + +import ray +from ray.rllib.algorithms.dqn.distributional_q_tf_model import DistributionalQTFModel +from ray.rllib.evaluation.postprocessing import adjust_nstep +from ray.rllib.models import ModelCatalog +from ray.rllib.models.modelv2 import ModelV2 +from ray.rllib.models.tf.tf_action_dist import get_categorical_class_with_temperature +from ray.rllib.policy.policy import Policy +from ray.rllib.policy.sample_batch import SampleBatch +from ray.rllib.policy.tf_mixins import LearningRateSchedule, TargetNetworkMixin +from ray.rllib.policy.tf_policy_template import build_tf_policy +from ray.rllib.utils.annotations import OldAPIStack +from ray.rllib.utils.error import UnsupportedSpaceException +from ray.rllib.utils.exploration import ParameterNoise +from ray.rllib.utils.framework import try_import_tf +from ray.rllib.utils.numpy import convert_to_numpy +from ray.rllib.utils.tf_utils import ( + huber_loss, + l2_loss, + make_tf_callable, + minimize_and_clip, + reduce_mean_ignore_inf, +) +from ray.rllib.utils.typing import AlgorithmConfigDict, ModelGradients, TensorType + +tf1, tf, tfv = try_import_tf() + +# Importance sampling weights for prioritized replay +PRIO_WEIGHTS = "weights" +Q_SCOPE = "q_func" +Q_TARGET_SCOPE = "target_q_func" + + +@OldAPIStack +class QLoss: + def __init__( + self, + q_t_selected: TensorType, + q_logits_t_selected: TensorType, + q_tp1_best: TensorType, + q_dist_tp1_best: TensorType, + importance_weights: TensorType, + rewards: TensorType, + done_mask: TensorType, + gamma: float = 0.99, + n_step: int = 1, + num_atoms: int = 1, + v_min: float = -10.0, + v_max: float = 10.0, + loss_fn=huber_loss, + ): + + if num_atoms > 1: + # Distributional Q-learning which corresponds to an entropy loss + + z = tf.range(num_atoms, dtype=tf.float32) + z = v_min + z * (v_max - v_min) / float(num_atoms - 1) + + # (batch_size, 1) * (1, num_atoms) = (batch_size, num_atoms) + r_tau = tf.expand_dims(rewards, -1) + gamma**n_step * tf.expand_dims( + 1.0 - done_mask, -1 + ) * tf.expand_dims(z, 0) + r_tau = tf.clip_by_value(r_tau, v_min, v_max) + b = (r_tau - v_min) / ((v_max - v_min) / float(num_atoms - 1)) + lb = tf.floor(b) + ub = tf.math.ceil(b) + # indispensable judgement which is missed in most implementations + # when b happens to be an integer, lb == ub, so pr_j(s', a*) will + # be discarded because (ub-b) == (b-lb) == 0 + floor_equal_ceil = tf.cast(tf.less(ub - lb, 0.5), tf.float32) + + l_project = tf.one_hot( + tf.cast(lb, dtype=tf.int32), num_atoms + ) # (batch_size, num_atoms, num_atoms) + u_project = tf.one_hot( + tf.cast(ub, dtype=tf.int32), num_atoms + ) # (batch_size, num_atoms, num_atoms) + ml_delta = q_dist_tp1_best * (ub - b + floor_equal_ceil) + mu_delta = q_dist_tp1_best * (b - lb) + ml_delta = tf.reduce_sum(l_project * tf.expand_dims(ml_delta, -1), axis=1) + mu_delta = tf.reduce_sum(u_project * tf.expand_dims(mu_delta, -1), axis=1) + m = ml_delta + mu_delta + + # Rainbow paper claims that using this cross entropy loss for + # priority is robust and insensitive to `prioritized_replay_alpha` + self.td_error = tf.nn.softmax_cross_entropy_with_logits( + labels=m, logits=q_logits_t_selected + ) + self.loss = tf.reduce_mean( + self.td_error * tf.cast(importance_weights, tf.float32) + ) + self.stats = { + # TODO: better Q stats for dist dqn + "mean_td_error": tf.reduce_mean(self.td_error), + } + else: + q_tp1_best_masked = (1.0 - done_mask) * q_tp1_best + + # compute RHS of bellman equation + q_t_selected_target = rewards + gamma**n_step * q_tp1_best_masked + + # compute the error (potentially clipped) + self.td_error = q_t_selected - tf.stop_gradient(q_t_selected_target) + self.loss = tf.reduce_mean( + tf.cast(importance_weights, tf.float32) * loss_fn(self.td_error) + ) + self.stats = { + "mean_q": tf.reduce_mean(q_t_selected), + "min_q": tf.reduce_min(q_t_selected), + "max_q": tf.reduce_max(q_t_selected), + "mean_td_error": tf.reduce_mean(self.td_error), + } + + +@OldAPIStack +class ComputeTDErrorMixin: + """Assign the `compute_td_error` method to the DQNTFPolicy + + This allows us to prioritize on the worker side. + """ + + def __init__(self): + @make_tf_callable(self.get_session(), dynamic_shape=True) + def compute_td_error( + obs_t, act_t, rew_t, obs_tp1, terminateds_mask, importance_weights + ): + # Do forward pass on loss to update td error attribute + build_q_losses( + self, + self.model, + None, + { + SampleBatch.CUR_OBS: tf.convert_to_tensor(obs_t), + SampleBatch.ACTIONS: tf.convert_to_tensor(act_t), + SampleBatch.REWARDS: tf.convert_to_tensor(rew_t), + SampleBatch.NEXT_OBS: tf.convert_to_tensor(obs_tp1), + SampleBatch.TERMINATEDS: tf.convert_to_tensor(terminateds_mask), + PRIO_WEIGHTS: tf.convert_to_tensor(importance_weights), + }, + ) + + return self.q_loss.td_error + + self.compute_td_error = compute_td_error + + +@OldAPIStack +def build_q_model( + policy: Policy, + obs_space: gym.spaces.Space, + action_space: gym.spaces.Space, + config: AlgorithmConfigDict, +) -> ModelV2: + """Build q_model and target_model for DQN + + Args: + policy: The Policy, which will use the model for optimization. + obs_space (gym.spaces.Space): The policy's observation space. + action_space (gym.spaces.Space): The policy's action space. + config (AlgorithmConfigDict): + + Returns: + ModelV2: The Model for the Policy to use. + Note: The target q model will not be returned, just assigned to + `policy.target_model`. + """ + if not isinstance(action_space, gym.spaces.Discrete): + raise UnsupportedSpaceException( + "Action space {} is not supported for DQN.".format(action_space) + ) + + if config["hiddens"]: + # try to infer the last layer size, otherwise fall back to 256 + num_outputs = ([256] + list(config["model"]["fcnet_hiddens"]))[-1] + config["model"]["no_final_linear"] = True + else: + num_outputs = action_space.n + + q_model = ModelCatalog.get_model_v2( + obs_space=obs_space, + action_space=action_space, + num_outputs=num_outputs, + model_config=config["model"], + framework="tf", + model_interface=DistributionalQTFModel, + name=Q_SCOPE, + num_atoms=config["num_atoms"], + dueling=config["dueling"], + q_hiddens=config["hiddens"], + use_noisy=config["noisy"], + v_min=config["v_min"], + v_max=config["v_max"], + sigma0=config["sigma0"], + # TODO(sven): Move option to add LayerNorm after each Dense + # generically into ModelCatalog. + add_layer_norm=isinstance(getattr(policy, "exploration", None), ParameterNoise) + or config["exploration_config"]["type"] == "ParameterNoise", + ) + + policy.target_model = ModelCatalog.get_model_v2( + obs_space=obs_space, + action_space=action_space, + num_outputs=num_outputs, + model_config=config["model"], + framework="tf", + model_interface=DistributionalQTFModel, + name=Q_TARGET_SCOPE, + num_atoms=config["num_atoms"], + dueling=config["dueling"], + q_hiddens=config["hiddens"], + use_noisy=config["noisy"], + v_min=config["v_min"], + v_max=config["v_max"], + sigma0=config["sigma0"], + # TODO(sven): Move option to add LayerNorm after each Dense + # generically into ModelCatalog. + add_layer_norm=isinstance(getattr(policy, "exploration", None), ParameterNoise) + or config["exploration_config"]["type"] == "ParameterNoise", + ) + + return q_model + + +@OldAPIStack +def get_distribution_inputs_and_class( + policy: Policy, model: ModelV2, input_dict: SampleBatch, *, explore=True, **kwargs +): + q_vals = compute_q_values( + policy, model, input_dict, state_batches=None, explore=explore + ) + q_vals = q_vals[0] if isinstance(q_vals, tuple) else q_vals + + policy.q_values = q_vals + + # Return a Torch TorchCategorical distribution where the temperature + # parameter is partially binded to the configured value. + temperature = policy.config["categorical_distribution_temperature"] + + return ( + policy.q_values, + get_categorical_class_with_temperature(temperature), + [], + ) # state-out + + +@OldAPIStack +def build_q_losses(policy: Policy, model, _, train_batch: SampleBatch) -> TensorType: + """Constructs the loss for DQNTFPolicy. + + Args: + policy: The Policy to calculate the loss for. + model (ModelV2): The Model to calculate the loss for. + train_batch: The training data. + + Returns: + TensorType: A single loss tensor. + """ + config = policy.config + # q network evaluation + q_t, q_logits_t, q_dist_t, _ = compute_q_values( + policy, + model, + SampleBatch({"obs": train_batch[SampleBatch.CUR_OBS]}), + state_batches=None, + explore=False, + ) + + # target q network evalution + q_tp1, q_logits_tp1, q_dist_tp1, _ = compute_q_values( + policy, + policy.target_model, + SampleBatch({"obs": train_batch[SampleBatch.NEXT_OBS]}), + state_batches=None, + explore=False, + ) + if not hasattr(policy, "target_q_func_vars"): + policy.target_q_func_vars = policy.target_model.variables() + + # q scores for actions which we know were selected in the given state. + one_hot_selection = tf.one_hot( + tf.cast(train_batch[SampleBatch.ACTIONS], tf.int32), policy.action_space.n + ) + q_t_selected = tf.reduce_sum(q_t * one_hot_selection, 1) + q_logits_t_selected = tf.reduce_sum( + q_logits_t * tf.expand_dims(one_hot_selection, -1), 1 + ) + + # compute estimate of best possible value starting from state at t + 1 + if config["double_q"]: + ( + q_tp1_using_online_net, + q_logits_tp1_using_online_net, + q_dist_tp1_using_online_net, + _, + ) = compute_q_values( + policy, + model, + SampleBatch({"obs": train_batch[SampleBatch.NEXT_OBS]}), + state_batches=None, + explore=False, + ) + q_tp1_best_using_online_net = tf.argmax(q_tp1_using_online_net, 1) + q_tp1_best_one_hot_selection = tf.one_hot( + q_tp1_best_using_online_net, policy.action_space.n + ) + q_tp1_best = tf.reduce_sum(q_tp1 * q_tp1_best_one_hot_selection, 1) + q_dist_tp1_best = tf.reduce_sum( + q_dist_tp1 * tf.expand_dims(q_tp1_best_one_hot_selection, -1), 1 + ) + else: + q_tp1_best_one_hot_selection = tf.one_hot( + tf.argmax(q_tp1, 1), policy.action_space.n + ) + q_tp1_best = tf.reduce_sum(q_tp1 * q_tp1_best_one_hot_selection, 1) + q_dist_tp1_best = tf.reduce_sum( + q_dist_tp1 * tf.expand_dims(q_tp1_best_one_hot_selection, -1), 1 + ) + + loss_fn = huber_loss if policy.config["td_error_loss_fn"] == "huber" else l2_loss + + policy.q_loss = QLoss( + q_t_selected, + q_logits_t_selected, + q_tp1_best, + q_dist_tp1_best, + train_batch[PRIO_WEIGHTS], + tf.cast(train_batch[SampleBatch.REWARDS], tf.float32), + tf.cast(train_batch[SampleBatch.TERMINATEDS], tf.float32), + config["gamma"], + config["n_step"], + config["num_atoms"], + config["v_min"], + config["v_max"], + loss_fn, + ) + + return policy.q_loss.loss + + +@OldAPIStack +def adam_optimizer( + policy: Policy, config: AlgorithmConfigDict +) -> "tf.keras.optimizers.Optimizer": + if policy.config["framework"] == "tf2": + return tf.keras.optimizers.Adam( + learning_rate=policy.cur_lr, epsilon=config["adam_epsilon"] + ) + else: + return tf1.train.AdamOptimizer( + learning_rate=policy.cur_lr, epsilon=config["adam_epsilon"] + ) + + +@OldAPIStack +def clip_gradients( + policy: Policy, optimizer: "tf.keras.optimizers.Optimizer", loss: TensorType +) -> ModelGradients: + if not hasattr(policy, "q_func_vars"): + policy.q_func_vars = policy.model.variables() + + return minimize_and_clip( + optimizer, + loss, + var_list=policy.q_func_vars, + clip_val=policy.config["grad_clip"], + ) + + +@OldAPIStack +def build_q_stats(policy: Policy, batch) -> Dict[str, TensorType]: + return dict( + { + "cur_lr": tf.cast(policy.cur_lr, tf.float64), + }, + **policy.q_loss.stats + ) + + +@OldAPIStack +def setup_mid_mixins(policy: Policy, obs_space, action_space, config) -> None: + LearningRateSchedule.__init__(policy, config["lr"], config["lr_schedule"]) + ComputeTDErrorMixin.__init__(policy) + + +@OldAPIStack +def setup_late_mixins( + policy: Policy, + obs_space: gym.spaces.Space, + action_space: gym.spaces.Space, + config: AlgorithmConfigDict, +) -> None: + TargetNetworkMixin.__init__(policy) + + +@OldAPIStack +def compute_q_values( + policy: Policy, + model: ModelV2, + input_batch: SampleBatch, + state_batches=None, + seq_lens=None, + explore=None, + is_training: bool = False, +): + + config = policy.config + + model_out, state = model(input_batch, state_batches or [], seq_lens) + + if config["num_atoms"] > 1: + ( + action_scores, + z, + support_logits_per_action, + logits, + dist, + ) = model.get_q_value_distributions(model_out) + else: + (action_scores, logits, dist) = model.get_q_value_distributions(model_out) + + if config["dueling"]: + state_score = model.get_state_value(model_out) + if config["num_atoms"] > 1: + support_logits_per_action_mean = tf.reduce_mean( + support_logits_per_action, 1 + ) + support_logits_per_action_centered = ( + support_logits_per_action + - tf.expand_dims(support_logits_per_action_mean, 1) + ) + support_logits_per_action = ( + tf.expand_dims(state_score, 1) + support_logits_per_action_centered + ) + support_prob_per_action = tf.nn.softmax(logits=support_logits_per_action) + value = tf.reduce_sum(input_tensor=z * support_prob_per_action, axis=-1) + logits = support_logits_per_action + dist = support_prob_per_action + else: + action_scores_mean = reduce_mean_ignore_inf(action_scores, 1) + action_scores_centered = action_scores - tf.expand_dims( + action_scores_mean, 1 + ) + value = state_score + action_scores_centered + else: + value = action_scores + + return value, logits, dist, state + + +@OldAPIStack +def postprocess_nstep_and_prio( + policy: Policy, batch: SampleBatch, other_agent=None, episode=None +) -> SampleBatch: + # N-step Q adjustments. + if policy.config["n_step"] > 1: + adjust_nstep(policy.config["n_step"], policy.config["gamma"], batch) + + # Create dummy prio-weights (1.0) in case we don't have any in + # the batch. + if PRIO_WEIGHTS not in batch: + batch[PRIO_WEIGHTS] = np.ones_like(batch[SampleBatch.REWARDS]) + + # Prioritize on the worker side. + if batch.count > 0 and policy.config["replay_buffer_config"].get( + "worker_side_prioritization", False + ): + td_errors = policy.compute_td_error( + batch[SampleBatch.OBS], + batch[SampleBatch.ACTIONS], + batch[SampleBatch.REWARDS], + batch[SampleBatch.NEXT_OBS], + batch[SampleBatch.TERMINATEDS], + batch[PRIO_WEIGHTS], + ) + # Retain compatibility with old-style Replay args + epsilon = policy.config.get("replay_buffer_config", {}).get( + "prioritized_replay_eps" + ) or policy.config.get("prioritized_replay_eps") + if epsilon is None: + raise ValueError("prioritized_replay_eps not defined in config.") + + new_priorities = np.abs(convert_to_numpy(td_errors)) + epsilon + batch[PRIO_WEIGHTS] = new_priorities + + return batch + + +DQNTFPolicy = build_tf_policy( + name="DQNTFPolicy", + get_default_config=lambda: ray.rllib.algorithms.dqn.dqn.DQNConfig(), + make_model=build_q_model, + action_distribution_fn=get_distribution_inputs_and_class, + loss_fn=build_q_losses, + stats_fn=build_q_stats, + postprocess_fn=postprocess_nstep_and_prio, + optimizer_fn=adam_optimizer, + compute_gradients_fn=clip_gradients, + extra_action_out_fn=lambda policy: {"q_values": policy.q_values}, + extra_learn_fetches_fn=lambda policy: {"td_error": policy.q_loss.td_error}, + before_loss_init=setup_mid_mixins, + after_init=setup_late_mixins, + mixins=[ + TargetNetworkMixin, + ComputeTDErrorMixin, + LearningRateSchedule, + ], +) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_torch_model.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_torch_model.py new file mode 100644 index 0000000000000000000000000000000000000000..4cb93bb63967c7193783765f528d0c0128543947 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_torch_model.py @@ -0,0 +1,177 @@ +"""PyTorch model for DQN""" + +from typing import Sequence + +import gymnasium as gym + +from ray.rllib.models.torch.misc import SlimFC +from ray.rllib.models.torch.modules.noisy_layer import NoisyLayer +from ray.rllib.models.torch.torch_modelv2 import TorchModelV2 +from ray.rllib.utils.annotations import OldAPIStack +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.typing import ModelConfigDict + +torch, nn = try_import_torch() + + +@OldAPIStack +class DQNTorchModel(TorchModelV2, nn.Module): + """Extension of standard TorchModelV2 to provide dueling-Q functionality.""" + + def __init__( + self, + obs_space: gym.spaces.Space, + action_space: gym.spaces.Space, + num_outputs: int, + model_config: ModelConfigDict, + name: str, + *, + q_hiddens: Sequence[int] = (256,), + dueling: bool = False, + dueling_activation: str = "relu", + num_atoms: int = 1, + use_noisy: bool = False, + v_min: float = -10.0, + v_max: float = 10.0, + sigma0: float = 0.5, + # TODO(sven): Move `add_layer_norm` into ModelCatalog as + # generic option, then error if we use ParameterNoise as + # Exploration type and do not have any LayerNorm layers in + # the net. + add_layer_norm: bool = False + ): + """Initialize variables of this model. + + Extra model kwargs: + q_hiddens (Sequence[int]): List of layer-sizes after(!) the + Advantages(A)/Value(V)-split. Hence, each of the A- and V- + branches will have this structure of Dense layers. To define + the NN before this A/V-split, use - as always - + config["model"]["fcnet_hiddens"]. + dueling: Whether to build the advantage(A)/value(V) heads + for DDQN. If True, Q-values are calculated as: + Q = (A - mean[A]) + V. If False, raw NN output is interpreted + as Q-values. + dueling_activation: The activation to use for all dueling + layers (A- and V-branch). One of "relu", "tanh", "linear". + num_atoms: If >1, enables distributional DQN. + use_noisy: Use noisy layers. + v_min: Min value support for distributional DQN. + v_max: Max value support for distributional DQN. + sigma0 (float): Initial value of noisy layers. + add_layer_norm: Enable layer norm (for param noise). + """ + nn.Module.__init__(self) + super(DQNTorchModel, self).__init__( + obs_space, action_space, num_outputs, model_config, name + ) + + self.dueling = dueling + self.num_atoms = num_atoms + self.v_min = v_min + self.v_max = v_max + self.sigma0 = sigma0 + ins = num_outputs + + advantage_module = nn.Sequential() + value_module = nn.Sequential() + + # Dueling case: Build the shared (advantages and value) fc-network. + for i, n in enumerate(q_hiddens): + if use_noisy: + advantage_module.add_module( + "dueling_A_{}".format(i), + NoisyLayer( + ins, n, sigma0=self.sigma0, activation=dueling_activation + ), + ) + value_module.add_module( + "dueling_V_{}".format(i), + NoisyLayer( + ins, n, sigma0=self.sigma0, activation=dueling_activation + ), + ) + else: + advantage_module.add_module( + "dueling_A_{}".format(i), + SlimFC(ins, n, activation_fn=dueling_activation), + ) + value_module.add_module( + "dueling_V_{}".format(i), + SlimFC(ins, n, activation_fn=dueling_activation), + ) + # Add LayerNorm after each Dense. + if add_layer_norm: + advantage_module.add_module( + "LayerNorm_A_{}".format(i), nn.LayerNorm(n) + ) + value_module.add_module("LayerNorm_V_{}".format(i), nn.LayerNorm(n)) + ins = n + + # Actual Advantages layer (nodes=num-actions). + if use_noisy: + advantage_module.add_module( + "A", + NoisyLayer( + ins, self.action_space.n * self.num_atoms, sigma0, activation=None + ), + ) + elif q_hiddens: + advantage_module.add_module( + "A", SlimFC(ins, action_space.n * self.num_atoms, activation_fn=None) + ) + + self.advantage_module = advantage_module + + # Value layer (nodes=1). + if self.dueling: + if use_noisy: + value_module.add_module( + "V", NoisyLayer(ins, self.num_atoms, sigma0, activation=None) + ) + elif q_hiddens: + value_module.add_module( + "V", SlimFC(ins, self.num_atoms, activation_fn=None) + ) + self.value_module = value_module + + def get_q_value_distributions(self, model_out): + """Returns distributional values for Q(s, a) given a state embedding. + + Override this in your custom model to customize the Q output head. + + Args: + model_out: Embedding from the model layers. + + Returns: + (action_scores, logits, dist) if num_atoms == 1, otherwise + (action_scores, z, support_logits_per_action, logits, dist) + """ + action_scores = self.advantage_module(model_out) + + if self.num_atoms > 1: + # Distributional Q-learning uses a discrete support z + # to represent the action value distribution + z = torch.arange(0.0, self.num_atoms, dtype=torch.float32).to( + action_scores.device + ) + z = self.v_min + z * (self.v_max - self.v_min) / float(self.num_atoms - 1) + + support_logits_per_action = torch.reshape( + action_scores, shape=(-1, self.action_space.n, self.num_atoms) + ) + support_prob_per_action = nn.functional.softmax( + support_logits_per_action, dim=-1 + ) + action_scores = torch.sum(z * support_prob_per_action, dim=-1) + logits = support_logits_per_action + probs = support_prob_per_action + return action_scores, z, support_logits_per_action, logits, probs + else: + logits = torch.unsqueeze(torch.ones_like(action_scores), -1) + return action_scores, logits, logits + + def get_state_value(self, model_out): + """Returns the state value prediction for the given state embedding.""" + + return self.value_module(model_out) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_torch_policy.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_torch_policy.py new file mode 100644 index 0000000000000000000000000000000000000000..fead64a5bc11e5a40b41ff055a90d066afa9dc2c --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/dqn_torch_policy.py @@ -0,0 +1,519 @@ +"""PyTorch policy class used for DQN""" + +from typing import Dict, List, Tuple + +import gymnasium as gym + +import ray +from ray.rllib.algorithms.dqn.dqn_tf_policy import ( + PRIO_WEIGHTS, + Q_SCOPE, + Q_TARGET_SCOPE, + postprocess_nstep_and_prio, +) +from ray.rllib.algorithms.dqn.dqn_torch_model import DQNTorchModel +from ray.rllib.models.catalog import ModelCatalog +from ray.rllib.models.modelv2 import ModelV2 +from ray.rllib.models.torch.torch_action_dist import ( + TorchDistributionWrapper, + get_torch_categorical_class_with_temperature, +) +from ray.rllib.policy.policy import Policy +from ray.rllib.policy.policy_template import build_policy_class +from ray.rllib.policy.sample_batch import SampleBatch +from ray.rllib.policy.torch_mixins import ( + LearningRateSchedule, + TargetNetworkMixin, +) +from ray.rllib.utils.annotations import OldAPIStack +from ray.rllib.utils.error import UnsupportedSpaceException +from ray.rllib.utils.exploration.parameter_noise import ParameterNoise +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.torch_utils import ( + FLOAT_MIN, + apply_grad_clipping, + concat_multi_gpu_td_errors, + huber_loss, + l2_loss, + reduce_mean_ignore_inf, + softmax_cross_entropy_with_logits, +) +from ray.rllib.utils.typing import AlgorithmConfigDict, TensorType + +torch, nn = try_import_torch() +F = None +if nn: + F = nn.functional + + +@OldAPIStack +class QLoss: + def __init__( + self, + q_t_selected: TensorType, + q_logits_t_selected: TensorType, + q_tp1_best: TensorType, + q_probs_tp1_best: TensorType, + importance_weights: TensorType, + rewards: TensorType, + done_mask: TensorType, + gamma=0.99, + n_step=1, + num_atoms=1, + v_min=-10.0, + v_max=10.0, + loss_fn=huber_loss, + ): + + if num_atoms > 1: + # Distributional Q-learning which corresponds to an entropy loss + z = torch.arange(0.0, num_atoms, dtype=torch.float32).to(rewards.device) + z = v_min + z * (v_max - v_min) / float(num_atoms - 1) + + # (batch_size, 1) * (1, num_atoms) = (batch_size, num_atoms) + r_tau = torch.unsqueeze(rewards, -1) + gamma**n_step * torch.unsqueeze( + 1.0 - done_mask, -1 + ) * torch.unsqueeze(z, 0) + r_tau = torch.clamp(r_tau, v_min, v_max) + b = (r_tau - v_min) / ((v_max - v_min) / float(num_atoms - 1)) + lb = torch.floor(b) + ub = torch.ceil(b) + + # Indispensable judgement which is missed in most implementations + # when b happens to be an integer, lb == ub, so pr_j(s', a*) will + # be discarded because (ub-b) == (b-lb) == 0. + floor_equal_ceil = ((ub - lb) < 0.5).float() + + # (batch_size, num_atoms, num_atoms) + l_project = F.one_hot(lb.long(), num_atoms) + # (batch_size, num_atoms, num_atoms) + u_project = F.one_hot(ub.long(), num_atoms) + ml_delta = q_probs_tp1_best * (ub - b + floor_equal_ceil) + mu_delta = q_probs_tp1_best * (b - lb) + ml_delta = torch.sum(l_project * torch.unsqueeze(ml_delta, -1), dim=1) + mu_delta = torch.sum(u_project * torch.unsqueeze(mu_delta, -1), dim=1) + m = ml_delta + mu_delta + + # Rainbow paper claims that using this cross entropy loss for + # priority is robust and insensitive to `prioritized_replay_alpha` + self.td_error = softmax_cross_entropy_with_logits( + logits=q_logits_t_selected, labels=m.detach() + ) + self.loss = torch.mean(self.td_error * importance_weights) + self.stats = { + # TODO: better Q stats for dist dqn + } + else: + q_tp1_best_masked = (1.0 - done_mask) * q_tp1_best + + # compute RHS of bellman equation + q_t_selected_target = rewards + gamma**n_step * q_tp1_best_masked + + # compute the error (potentially clipped) + self.td_error = q_t_selected - q_t_selected_target.detach() + self.loss = torch.mean(importance_weights.float() * loss_fn(self.td_error)) + self.stats = { + "mean_q": torch.mean(q_t_selected), + "min_q": torch.min(q_t_selected), + "max_q": torch.max(q_t_selected), + } + + +@OldAPIStack +class ComputeTDErrorMixin: + """Assign the `compute_td_error` method to the DQNTorchPolicy + + This allows us to prioritize on the worker side. + """ + + def __init__(self): + def compute_td_error( + obs_t, act_t, rew_t, obs_tp1, terminateds_mask, importance_weights + ): + input_dict = self._lazy_tensor_dict({SampleBatch.CUR_OBS: obs_t}) + input_dict[SampleBatch.ACTIONS] = act_t + input_dict[SampleBatch.REWARDS] = rew_t + input_dict[SampleBatch.NEXT_OBS] = obs_tp1 + input_dict[SampleBatch.TERMINATEDS] = terminateds_mask + input_dict[PRIO_WEIGHTS] = importance_weights + + # Do forward pass on loss to update td error attribute + build_q_losses(self, self.model, None, input_dict) + + return self.model.tower_stats["q_loss"].td_error + + self.compute_td_error = compute_td_error + + +@OldAPIStack +def build_q_model_and_distribution( + policy: Policy, + obs_space: gym.spaces.Space, + action_space: gym.spaces.Space, + config: AlgorithmConfigDict, +) -> Tuple[ModelV2, TorchDistributionWrapper]: + """Build q_model and target_model for DQN + + Args: + policy: The policy, which will use the model for optimization. + obs_space (gym.spaces.Space): The policy's observation space. + action_space (gym.spaces.Space): The policy's action space. + config (AlgorithmConfigDict): + + Returns: + (q_model, TorchCategorical) + Note: The target q model will not be returned, just assigned to + `policy.target_model`. + """ + if not isinstance(action_space, gym.spaces.Discrete): + raise UnsupportedSpaceException( + "Action space {} is not supported for DQN.".format(action_space) + ) + + if config["hiddens"]: + # try to infer the last layer size, otherwise fall back to 256 + num_outputs = ([256] + list(config["model"]["fcnet_hiddens"]))[-1] + config["model"]["no_final_linear"] = True + else: + num_outputs = action_space.n + + # TODO(sven): Move option to add LayerNorm after each Dense + # generically into ModelCatalog. + add_layer_norm = ( + isinstance(getattr(policy, "exploration", None), ParameterNoise) + or config["exploration_config"]["type"] == "ParameterNoise" + ) + + model = ModelCatalog.get_model_v2( + obs_space=obs_space, + action_space=action_space, + num_outputs=num_outputs, + model_config=config["model"], + framework="torch", + model_interface=DQNTorchModel, + name=Q_SCOPE, + q_hiddens=config["hiddens"], + dueling=config["dueling"], + num_atoms=config["num_atoms"], + use_noisy=config["noisy"], + v_min=config["v_min"], + v_max=config["v_max"], + sigma0=config["sigma0"], + # TODO(sven): Move option to add LayerNorm after each Dense + # generically into ModelCatalog. + add_layer_norm=add_layer_norm, + ) + + policy.target_model = ModelCatalog.get_model_v2( + obs_space=obs_space, + action_space=action_space, + num_outputs=num_outputs, + model_config=config["model"], + framework="torch", + model_interface=DQNTorchModel, + name=Q_TARGET_SCOPE, + q_hiddens=config["hiddens"], + dueling=config["dueling"], + num_atoms=config["num_atoms"], + use_noisy=config["noisy"], + v_min=config["v_min"], + v_max=config["v_max"], + sigma0=config["sigma0"], + # TODO(sven): Move option to add LayerNorm after each Dense + # generically into ModelCatalog. + add_layer_norm=add_layer_norm, + ) + + # Return a Torch TorchCategorical distribution where the temperature + # parameter is partially binded to the configured value. + temperature = config["categorical_distribution_temperature"] + + return model, get_torch_categorical_class_with_temperature(temperature) + + +@OldAPIStack +def get_distribution_inputs_and_class( + policy: Policy, + model: ModelV2, + input_dict: SampleBatch, + *, + explore: bool = True, + is_training: bool = False, + **kwargs +) -> Tuple[TensorType, type, List[TensorType]]: + q_vals = compute_q_values( + policy, model, input_dict, explore=explore, is_training=is_training + ) + q_vals = q_vals[0] if isinstance(q_vals, tuple) else q_vals + + model.tower_stats["q_values"] = q_vals + + # Return a Torch TorchCategorical distribution where the temperature + # parameter is partially binded to the configured value. + temperature = policy.config["categorical_distribution_temperature"] + + return ( + q_vals, + get_torch_categorical_class_with_temperature(temperature), + [], # state-out + ) + + +@OldAPIStack +def build_q_losses(policy: Policy, model, _, train_batch: SampleBatch) -> TensorType: + """Constructs the loss for DQNTorchPolicy. + + Args: + policy: The Policy to calculate the loss for. + model (ModelV2): The Model to calculate the loss for. + train_batch: The training data. + + Returns: + TensorType: A single loss tensor. + """ + + config = policy.config + # Q-network evaluation. + q_t, q_logits_t, q_probs_t, _ = compute_q_values( + policy, + model, + {"obs": train_batch[SampleBatch.CUR_OBS]}, + explore=False, + is_training=True, + ) + + # Target Q-network evaluation. + q_tp1, q_logits_tp1, q_probs_tp1, _ = compute_q_values( + policy, + policy.target_models[model], + {"obs": train_batch[SampleBatch.NEXT_OBS]}, + explore=False, + is_training=True, + ) + + # Q scores for actions which we know were selected in the given state. + one_hot_selection = F.one_hot( + train_batch[SampleBatch.ACTIONS].long(), policy.action_space.n + ) + q_t_selected = torch.sum( + torch.where(q_t > FLOAT_MIN, q_t, torch.tensor(0.0, device=q_t.device)) + * one_hot_selection, + 1, + ) + q_logits_t_selected = torch.sum( + q_logits_t * torch.unsqueeze(one_hot_selection, -1), 1 + ) + + # compute estimate of best possible value starting from state at t + 1 + if config["double_q"]: + ( + q_tp1_using_online_net, + q_logits_tp1_using_online_net, + q_dist_tp1_using_online_net, + _, + ) = compute_q_values( + policy, + model, + {"obs": train_batch[SampleBatch.NEXT_OBS]}, + explore=False, + is_training=True, + ) + q_tp1_best_using_online_net = torch.argmax(q_tp1_using_online_net, 1) + q_tp1_best_one_hot_selection = F.one_hot( + q_tp1_best_using_online_net, policy.action_space.n + ) + q_tp1_best = torch.sum( + torch.where( + q_tp1 > FLOAT_MIN, q_tp1, torch.tensor(0.0, device=q_tp1.device) + ) + * q_tp1_best_one_hot_selection, + 1, + ) + q_probs_tp1_best = torch.sum( + q_probs_tp1 * torch.unsqueeze(q_tp1_best_one_hot_selection, -1), 1 + ) + else: + q_tp1_best_one_hot_selection = F.one_hot( + torch.argmax(q_tp1, 1), policy.action_space.n + ) + q_tp1_best = torch.sum( + torch.where( + q_tp1 > FLOAT_MIN, q_tp1, torch.tensor(0.0, device=q_tp1.device) + ) + * q_tp1_best_one_hot_selection, + 1, + ) + q_probs_tp1_best = torch.sum( + q_probs_tp1 * torch.unsqueeze(q_tp1_best_one_hot_selection, -1), 1 + ) + + loss_fn = huber_loss if policy.config["td_error_loss_fn"] == "huber" else l2_loss + + q_loss = QLoss( + q_t_selected, + q_logits_t_selected, + q_tp1_best, + q_probs_tp1_best, + train_batch[PRIO_WEIGHTS], + train_batch[SampleBatch.REWARDS], + train_batch[SampleBatch.TERMINATEDS].float(), + config["gamma"], + config["n_step"], + config["num_atoms"], + config["v_min"], + config["v_max"], + loss_fn, + ) + + # Store values for stats function in model (tower), such that for + # multi-GPU, we do not override them during the parallel loss phase. + model.tower_stats["td_error"] = q_loss.td_error + # TD-error tensor in final stats + # will be concatenated and retrieved for each individual batch item. + model.tower_stats["q_loss"] = q_loss + + return q_loss.loss + + +@OldAPIStack +def adam_optimizer( + policy: Policy, config: AlgorithmConfigDict +) -> "torch.optim.Optimizer": + + # By this time, the models have been moved to the GPU - if any - and we + # can define our optimizers using the correct CUDA variables. + if not hasattr(policy, "q_func_vars"): + policy.q_func_vars = policy.model.variables() + + return torch.optim.Adam( + policy.q_func_vars, lr=policy.cur_lr, eps=config["adam_epsilon"] + ) + + +@OldAPIStack +def build_q_stats(policy: Policy, batch) -> Dict[str, TensorType]: + stats = {} + for stats_key in policy.model_gpu_towers[0].tower_stats["q_loss"].stats.keys(): + stats[stats_key] = torch.mean( + torch.stack( + [ + t.tower_stats["q_loss"].stats[stats_key].to(policy.device) + for t in policy.model_gpu_towers + if "q_loss" in t.tower_stats + ] + ) + ) + stats["cur_lr"] = policy.cur_lr + return stats + + +@OldAPIStack +def setup_early_mixins( + policy: Policy, obs_space, action_space, config: AlgorithmConfigDict +) -> None: + LearningRateSchedule.__init__(policy, config["lr"], config["lr_schedule"]) + + +@OldAPIStack +def before_loss_init( + policy: Policy, + obs_space: gym.spaces.Space, + action_space: gym.spaces.Space, + config: AlgorithmConfigDict, +) -> None: + ComputeTDErrorMixin.__init__(policy) + TargetNetworkMixin.__init__(policy) + + +@OldAPIStack +def compute_q_values( + policy: Policy, + model: ModelV2, + input_dict, + state_batches=None, + seq_lens=None, + explore=None, + is_training: bool = False, +): + config = policy.config + + model_out, state = model(input_dict, state_batches or [], seq_lens) + + if config["num_atoms"] > 1: + ( + action_scores, + z, + support_logits_per_action, + logits, + probs_or_logits, + ) = model.get_q_value_distributions(model_out) + else: + (action_scores, logits, probs_or_logits) = model.get_q_value_distributions( + model_out + ) + + if config["dueling"]: + state_score = model.get_state_value(model_out) + if policy.config["num_atoms"] > 1: + support_logits_per_action_mean = torch.mean( + support_logits_per_action, dim=1 + ) + support_logits_per_action_centered = ( + support_logits_per_action + - torch.unsqueeze(support_logits_per_action_mean, dim=1) + ) + support_logits_per_action = ( + torch.unsqueeze(state_score, dim=1) + support_logits_per_action_centered + ) + support_prob_per_action = nn.functional.softmax( + support_logits_per_action, dim=-1 + ) + value = torch.sum(z * support_prob_per_action, dim=-1) + logits = support_logits_per_action + probs_or_logits = support_prob_per_action + else: + advantages_mean = reduce_mean_ignore_inf(action_scores, 1) + advantages_centered = action_scores - torch.unsqueeze(advantages_mean, 1) + value = state_score + advantages_centered + else: + value = action_scores + + return value, logits, probs_or_logits, state + + +@OldAPIStack +def grad_process_and_td_error_fn( + policy: Policy, optimizer: "torch.optim.Optimizer", loss: TensorType +) -> Dict[str, TensorType]: + # Clip grads if configured. + return apply_grad_clipping(policy, optimizer, loss) + + +@OldAPIStack +def extra_action_out_fn( + policy: Policy, input_dict, state_batches, model, action_dist +) -> Dict[str, TensorType]: + return {"q_values": model.tower_stats["q_values"]} + + +DQNTorchPolicy = build_policy_class( + name="DQNTorchPolicy", + framework="torch", + loss_fn=build_q_losses, + get_default_config=lambda: ray.rllib.algorithms.dqn.dqn.DQNConfig(), + make_model_and_action_dist=build_q_model_and_distribution, + action_distribution_fn=get_distribution_inputs_and_class, + stats_fn=build_q_stats, + postprocess_fn=postprocess_nstep_and_prio, + optimizer_fn=adam_optimizer, + extra_grad_process_fn=grad_process_and_td_error_fn, + extra_learn_fetches_fn=concat_multi_gpu_td_errors, + extra_action_out_fn=extra_action_out_fn, + before_init=setup_early_mixins, + before_loss_init=before_loss_init, + mixins=[ + TargetNetworkMixin, + ComputeTDErrorMixin, + LearningRateSchedule, + ], +) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3981d60cba24172faa14b5377e53c9867ab67230 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__pycache__/default_dqn_torch_rl_module.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__pycache__/default_dqn_torch_rl_module.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e52ecd9b09a71b2c2f5f2103577b0c4052d94700 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__pycache__/default_dqn_torch_rl_module.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__pycache__/dqn_torch_learner.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__pycache__/dqn_torch_learner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fec8b0e6573ae7d2a0c3039cc2cd604a05d3b56 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/__pycache__/dqn_torch_learner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/default_dqn_torch_rl_module.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/default_dqn_torch_rl_module.py new file mode 100644 index 0000000000000000000000000000000000000000..b1a07226d5c783b2330f4559b55053f8631df8da --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/default_dqn_torch_rl_module.py @@ -0,0 +1,328 @@ +from typing import Dict, Union + +import tree + +from ray.rllib.algorithms.dqn.default_dqn_rl_module import ( + ATOMS, + QF_LOGITS, + QF_NEXT_PREDS, + QF_PREDS, + QF_PROBS, + QF_TARGET_NEXT_PREDS, + QF_TARGET_NEXT_PROBS, + DefaultDQNRLModule, +) +from ray.rllib.algorithms.dqn.dqn_catalog import DQNCatalog +from ray.rllib.core.columns import Columns +from ray.rllib.core.models.base import ENCODER_OUT, Encoder, Model +from ray.rllib.core.rl_module.apis.q_net_api import QNetAPI +from ray.rllib.core.rl_module.rl_module import RLModule +from ray.rllib.core.rl_module.torch.torch_rl_module import TorchRLModule +from ray.rllib.utils.annotations import override +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.typing import TensorStructType, TensorType +from ray.util.annotations import DeveloperAPI + +torch, nn = try_import_torch() + + +@DeveloperAPI +class DefaultDQNTorchRLModule(TorchRLModule, DefaultDQNRLModule): + framework: str = "torch" + + def __init__(self, *args, **kwargs): + catalog_class = kwargs.pop("catalog_class", None) + if catalog_class is None: + catalog_class = DQNCatalog + super().__init__(*args, **kwargs, catalog_class=catalog_class) + + @override(RLModule) + def _forward_inference(self, batch: Dict[str, TensorType]) -> Dict[str, TensorType]: + # Q-network forward pass. + qf_outs = self.compute_q_values(batch) + + # Get action distribution. + action_dist_cls = self.get_exploration_action_dist_cls() + action_dist = action_dist_cls.from_logits(qf_outs[QF_PREDS]) + # Note, the deterministic version of the categorical distribution + # outputs directly the `argmax` of the logits. + exploit_actions = action_dist.to_deterministic().sample() + + output = {Columns.ACTIONS: exploit_actions} + if Columns.STATE_OUT in qf_outs: + output[Columns.STATE_OUT] = qf_outs[Columns.STATE_OUT] + + # In inference, we only need the exploitation actions. + return output + + @override(RLModule) + def _forward_exploration( + self, batch: Dict[str, TensorType], t: int + ) -> Dict[str, TensorType]: + # Define the return dictionary. + output = {} + + # Q-network forward pass. + qf_outs = self.compute_q_values(batch) + + # Get action distribution. + action_dist_cls = self.get_exploration_action_dist_cls() + action_dist = action_dist_cls.from_logits(qf_outs[QF_PREDS]) + # Note, the deterministic version of the categorical distribution + # outputs directly the `argmax` of the logits. + exploit_actions = action_dist.to_deterministic().sample() + + # We need epsilon greedy to support exploration. + # TODO (simon): Implement sampling for nested spaces. + # Update scheduler. + self.epsilon_schedule.update(t) + # Get the actual epsilon, + epsilon = self.epsilon_schedule.get_current_value() + # Apply epsilon-greedy exploration. + B = qf_outs[QF_PREDS].shape[0] + random_actions = torch.squeeze( + torch.multinomial( + ( + torch.nan_to_num( + qf_outs[QF_PREDS].reshape(-1, qf_outs[QF_PREDS].size(-1)), + neginf=0.0, + ) + != 0.0 + ).float(), + num_samples=1, + ), + dim=1, + ) + + actions = torch.where( + torch.rand((B,)) < epsilon, + random_actions, + exploit_actions, + ) + + # Add the actions to the return dictionary. + output[Columns.ACTIONS] = actions + + # If this is a stateful module, add output states. + if Columns.STATE_OUT in qf_outs: + output[Columns.STATE_OUT] = qf_outs[Columns.STATE_OUT] + + return output + + @override(RLModule) + def _forward_train( + self, batch: Dict[str, TensorType] + ) -> Dict[str, TensorStructType]: + if self.inference_only: + raise RuntimeError( + "Trying to train a module that is not a learner module. Set the " + "flag `inference_only=False` when building the module." + ) + output = {} + + # If we use a double-Q setup. + if self.uses_double_q: + # Then we need to make a single forward pass with both, + # current and next observations. + batch_base = { + Columns.OBS: torch.concat( + [batch[Columns.OBS], batch[Columns.NEXT_OBS]], dim=0 + ), + } + # If this is a stateful module add the input states. + if Columns.STATE_IN in batch: + # Add both, the input state for the actual observation and + # the one for the next observation. + batch_base.update( + { + Columns.STATE_IN: tree.map_structure( + lambda t1, t2: torch.cat([t1, t2], dim=0), + batch[Columns.STATE_IN], + batch[Columns.NEXT_STATE_IN], + ) + } + ) + # Otherwise we can just use the current observations. + else: + batch_base = {Columns.OBS: batch[Columns.OBS]} + # If this is a stateful module add the input state. + if Columns.STATE_IN in batch: + batch_base.update({Columns.STATE_IN: batch[Columns.STATE_IN]}) + + batch_target = {Columns.OBS: batch[Columns.NEXT_OBS]} + + # If we have a stateful encoder, add the states for the target forward + # pass. + if Columns.NEXT_STATE_IN in batch: + batch_target.update({Columns.STATE_IN: batch[Columns.NEXT_STATE_IN]}) + + # Q-network forward passes. + qf_outs = self.compute_q_values(batch_base) + if self.uses_double_q: + output[QF_PREDS], output[QF_NEXT_PREDS] = torch.chunk( + qf_outs[QF_PREDS], chunks=2, dim=0 + ) + else: + output[QF_PREDS] = qf_outs[QF_PREDS] + # The target Q-values for the next observations. + qf_target_next_outs = self.forward_target(batch_target) + output[QF_TARGET_NEXT_PREDS] = qf_target_next_outs[QF_PREDS] + # We are learning a Q-value distribution. + if self.num_atoms > 1: + # Add distribution artefacts to the output. + # Distribution support. + output[ATOMS] = qf_target_next_outs[ATOMS] + # Original logits from the Q-head. + output[QF_LOGITS] = qf_outs[QF_LOGITS] + # Probabilities of the Q-value distribution of the current state. + output[QF_PROBS] = qf_outs[QF_PROBS] + # Probabilities of the target Q-value distribution of the next state. + output[QF_TARGET_NEXT_PROBS] = qf_target_next_outs[QF_PROBS] + + # Add the states to the output, if the module is stateful. + if Columns.STATE_OUT in qf_outs: + output[Columns.STATE_OUT] = qf_outs[Columns.STATE_OUT] + # For correctness, also add the output states from the target forward pass. + # Note, we do not backpropagate through this state. + if Columns.STATE_OUT in qf_target_next_outs: + output[Columns.NEXT_STATE_OUT] = qf_target_next_outs[Columns.STATE_OUT] + + return output + + @override(QNetAPI) + def compute_advantage_distribution( + self, + batch: Dict[str, TensorType], + ) -> Dict[str, TensorType]: + output = {} + # Distributional Q-learning uses a discrete support `z` + # to represent the action value distribution. + # TODO (simon): Check, if we still need here the device for torch. + z = torch.arange(0.0, self.num_atoms, dtype=torch.float32).to( + batch.device, + ) + # Rescale the support. + z = self.v_min + z * (self.v_max - self.v_min) / float(self.num_atoms - 1) + # Reshape the action values. + # NOTE: Handcrafted action shape. + logits_per_action_per_atom = torch.reshape( + batch, shape=(*batch.shape[:-1], self.action_space.n, self.num_atoms) + ) + # Calculate the probability for each action value atom. Note, + # the sum along action value atoms of a single action value + # must sum to one. + prob_per_action_per_atom = nn.functional.softmax( + logits_per_action_per_atom, + dim=-1, + ) + # Compute expected action value by weighted sum. + output[ATOMS] = z + output["logits"] = logits_per_action_per_atom + output["probs"] = prob_per_action_per_atom + + return output + + # TODO (simon): Test, if providing the function with a `return_probs` + # improves performance significantly. + @override(DefaultDQNRLModule) + def _qf_forward_helper( + self, + batch: Dict[str, TensorType], + encoder: Encoder, + head: Union[Model, Dict[str, Model]], + ) -> Dict[str, TensorType]: + """Computes Q-values. + + This is a helper function that takes care of all different cases, + i.e. if we use a dueling architecture or not and if we use distributional + Q-learning or not. + + Args: + batch: The batch received in the forward pass. + encoder: The encoder network to use. Here we have a single encoder + for all heads (Q or advantages and value in case of a dueling + architecture). + head: Either a head model or a dictionary of head model (dueling + architecture) containing advantage and value stream heads. + + Returns: + In case of expectation learning the Q-value predictions ("qf_preds") + and in case of distributional Q-learning in addition to the predictions + the atoms ("atoms"), the Q-value predictions ("qf_preds"), the Q-logits + ("qf_logits") and the probabilities for the support atoms ("qf_probs"). + """ + output = {} + + # Encoder forward pass. + encoder_outs = encoder(batch) + + # Do we have a dueling architecture. + if self.uses_dueling: + # Head forward passes for advantage and value stream. + qf_outs = head["af"](encoder_outs[ENCODER_OUT]) + vf_outs = head["vf"](encoder_outs[ENCODER_OUT]) + # We learn a Q-value distribution. + if self.num_atoms > 1: + # Compute the advantage stream distribution. + af_dist_output = self.compute_advantage_distribution(qf_outs) + # Center the advantage stream distribution. + centered_af_logits = af_dist_output["logits"] - af_dist_output[ + "logits" + ].mean(dim=-1, keepdim=True) + # Calculate the Q-value distribution by adding advantage and + # value stream. + qf_logits = centered_af_logits + vf_outs.view( + -1, *((1,) * (centered_af_logits.dim() - 1)) + ) + # Calculate probabilites for the Q-value distribution along + # the support given by the atoms. + qf_probs = nn.functional.softmax(qf_logits, dim=-1) + # Return also the support as we need it in the learner. + output[ATOMS] = af_dist_output[ATOMS] + # Calculate the Q-values by the weighted sum over the atoms. + output[QF_PREDS] = torch.sum(af_dist_output[ATOMS] * qf_probs, dim=-1) + output[QF_LOGITS] = qf_logits + output[QF_PROBS] = qf_probs + # Otherwise we learn an expectation. + else: + # Center advantages. Note, we cannot do an in-place operation here + # b/c we backpropagate through these values. See for a discussion + # https://discuss.pytorch.org/t/gradient-computation-issue-due-to- + # inplace-operation-unsure-how-to-debug-for-custom-model/170133 + # Has to be a mean for each batch element. + af_outs_mean = torch.nan_to_num(qf_outs, neginf=torch.nan).nanmean( + dim=-1, keepdim=True + ) + qf_outs = qf_outs - af_outs_mean + # Add advantage and value stream. Note, we broadcast here. + output[QF_PREDS] = qf_outs + vf_outs + # No dueling architecture. + else: + # Note, in this case the advantage network is the Q-network. + # Forward pass through Q-head. + qf_outs = head(encoder_outs[ENCODER_OUT]) + # We learn a Q-value distribution. + if self.num_atoms > 1: + # Note in a non-dueling architecture the advantage distribution is + # the Q-value distribution. + # Get the Q-value distribution. + qf_dist_outs = self.compute_advantage_distribution(qf_outs) + # Get the support of the Q-value distribution. + output[ATOMS] = qf_dist_outs[ATOMS] + # Calculate the Q-values by the weighted sum over the atoms. + output[QF_PREDS] = torch.sum( + qf_dist_outs[ATOMS] * qf_dist_outs["probs"], dim=-1 + ) + output[QF_LOGITS] = qf_dist_outs["logits"] + output[QF_PROBS] = qf_dist_outs["probs"] + # Otherwise we learn an expectation. + else: + # In this case we have a Q-head of dimension (1, action_space.n). + output[QF_PREDS] = qf_outs + + # If we have a stateful encoder add the output states to the return + # dictionary. + if Columns.STATE_OUT in encoder_outs: + output[Columns.STATE_OUT] = encoder_outs[Columns.STATE_OUT] + + return output diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/dqn_torch_learner.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/dqn_torch_learner.py new file mode 100644 index 0000000000000000000000000000000000000000..3e77529bc130f589a64bc15ffbdacf2123b36a26 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dqn/torch/dqn_torch_learner.py @@ -0,0 +1,294 @@ +from typing import Dict + +from ray.rllib.algorithms.dqn.dqn import DQNConfig +from ray.rllib.algorithms.dqn.dqn_learner import ( + ATOMS, + QF_LOGITS, + QF_LOSS_KEY, + QF_MAX_KEY, + QF_MEAN_KEY, + QF_MIN_KEY, + QF_NEXT_PREDS, + QF_PREDS, + QF_PROBS, + QF_TARGET_NEXT_PREDS, + QF_TARGET_NEXT_PROBS, + TD_ERROR_MEAN_KEY, + DQNLearner, +) +from ray.rllib.core.columns import Columns +from ray.rllib.core.learner.torch.torch_learner import TorchLearner +from ray.rllib.utils.annotations import override +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.metrics import TD_ERROR_KEY +from ray.rllib.utils.typing import ModuleID, TensorType + +torch, nn = try_import_torch() + + +class DQNTorchLearner(DQNLearner, TorchLearner): + """Implements `torch`-specific DQN Rainbow loss logic on top of `DQNLearner` + + This ' Learner' class implements the loss in its + `self.compute_loss_for_module()` method. + """ + + @override(TorchLearner) + def compute_loss_for_module( + self, + *, + module_id: ModuleID, + config: DQNConfig, + batch: Dict, + fwd_out: Dict[str, TensorType] + ) -> TensorType: + + # Possibly apply masking to some sub loss terms and to the total loss term + # at the end. Masking could be used for RNN-based model (zero padded `batch`) + # and for PPO's batched value function (and bootstrap value) computations, + # for which we add an (artificial) timestep to each episode to + # simplify the actual computation. + if Columns.LOSS_MASK in batch: + mask = batch[Columns.LOSS_MASK].clone() + # Check, if a burn-in should be used to recover from a poor state. + if self.config.burn_in_len > 0: + # Train only on the timesteps after the burn-in period. + mask[:, : self.config.burn_in_len] = False + num_valid = torch.sum(mask) + + def possibly_masked_mean(data_): + return torch.sum(data_[mask]) / num_valid + + def possibly_masked_min(data_): + # Prevent minimum over empty tensors, which can happened + # when all elements in the mask are `False`. + return ( + torch.tensor(float("nan")) + if data_[mask].numel() == 0 + else torch.min(data_[mask]) + ) + + def possibly_masked_max(data_): + # Prevent maximum over empty tensors, which can happened + # when all elements in the mask are `False`. + return ( + torch.tensor(float("nan")) + if data_[mask].numel() == 0 + else torch.max(data_[mask]) + ) + + else: + possibly_masked_mean = torch.mean + possibly_masked_min = torch.min + possibly_masked_max = torch.max + + q_curr = fwd_out[QF_PREDS] + q_target_next = fwd_out[QF_TARGET_NEXT_PREDS] + + # Get the Q-values for the selected actions in the rollout. + # TODO (simon, sven): Check, if we can use `gather` with a complex action + # space - we might need the one_hot_selection. Also test performance. + q_selected = torch.nan_to_num( + torch.gather( + q_curr, + dim=-1, + index=batch[Columns.ACTIONS] + .view(*batch[Columns.ACTIONS].shape, 1) + .long(), + ), + neginf=0.0, + ).squeeze(dim=-1) + + # Use double Q learning. + if config.double_q: + # Then we evaluate the target Q-function at the best action (greedy action) + # over the online Q-function. + # Mark the best online Q-value of the next state. + q_next_best_idx = ( + torch.argmax(fwd_out[QF_NEXT_PREDS], dim=-1).unsqueeze(dim=-1).long() + ) + # Get the Q-value of the target network at maximum of the online network + # (bootstrap action). + q_next_best = torch.nan_to_num( + torch.gather(q_target_next, dim=-1, index=q_next_best_idx), + neginf=0.0, + ).squeeze() + else: + # Mark the maximum Q-value(s). + q_next_best_idx = ( + torch.argmax(q_target_next, dim=-1).unsqueeze(dim=-1).long() + ) + # Get the maximum Q-value(s). + q_next_best = torch.nan_to_num( + torch.gather(q_target_next, dim=-1, index=q_next_best_idx), + neginf=0.0, + ).squeeze() + + # If we learn a Q-distribution. + if config.num_atoms > 1: + # Extract the Q-logits evaluated at the selected actions. + # (Note, `torch.gather` should be faster than multiplication + # with a one-hot tensor.) + # (32, 2, 10) -> (32, 10) + q_logits_selected = torch.gather( + fwd_out[QF_LOGITS], + dim=1, + # Note, the Q-logits are of shape (B, action_space.n, num_atoms) + # while the actions have shape (B, 1). We reshape actions to + # (B, 1, num_atoms). + index=batch[Columns.ACTIONS] + .view(-1, 1, 1) + .expand(-1, 1, config.num_atoms) + .long(), + ).squeeze(dim=1) + # Get the probabilies for the maximum Q-value(s). + q_probs_next_best = torch.gather( + fwd_out[QF_TARGET_NEXT_PROBS], + dim=1, + # Change the view and then expand to get to the dimensions + # of the probabilities (dims 0 and 2, 1 should be reduced + # from 2 -> 1). + index=q_next_best_idx.view(-1, 1, 1).expand(-1, 1, config.num_atoms), + ).squeeze(dim=1) + + # For distributional Q-learning we use an entropy loss. + + # Extract the support grid for the Q distribution. + z = fwd_out[ATOMS] + # TODO (simon): Enable computing on GPU. + # (batch_size, 1) * (1, num_atoms) = (batch_size, num_atoms)s + r_tau = torch.clamp( + batch[Columns.REWARDS].unsqueeze(dim=-1) + + ( + config.gamma ** batch["n_step"] + * (1.0 - batch[Columns.TERMINATEDS].float()) + ).unsqueeze(dim=-1) + * z, + config.v_min, + config.v_max, + ).squeeze(dim=1) + # (32, 10) + b = (r_tau - config.v_min) / ( + (config.v_max - config.v_min) / float(config.num_atoms - 1.0) + ) + lower_bound = torch.floor(b) + upper_bound = torch.ceil(b) + + floor_equal_ceil = ((upper_bound - lower_bound) < 0.5).float() + + # (B, num_atoms, num_atoms). + lower_projection = nn.functional.one_hot( + lower_bound.long(), config.num_atoms + ) + upper_projection = nn.functional.one_hot( + upper_bound.long(), config.num_atoms + ) + # (32, 10) + ml_delta = q_probs_next_best * (upper_bound - b + floor_equal_ceil) + mu_delta = q_probs_next_best * (b - lower_bound) + # (32, 10) + ml_delta = torch.sum(lower_projection * ml_delta.unsqueeze(dim=-1), dim=1) + mu_delta = torch.sum(upper_projection * mu_delta.unsqueeze(dim=-1), dim=1) + # We do not want to propagate through the distributional targets. + # (32, 10) + m = (ml_delta + mu_delta).detach() + + # The Rainbow paper claims to use the KL-divergence loss. This is identical + # to using the cross-entropy (differs only by entropy which is constant) + # when optimizing by the gradient (the gradient is identical). + td_error = nn.CrossEntropyLoss(reduction="none")(q_logits_selected, m) + # Compute the weighted loss (importance sampling weights). + total_loss = torch.mean(batch["weights"] * td_error) + else: + # Masked all Q-values with terminated next states in the targets. + q_next_best_masked = ( + 1.0 - batch[Columns.TERMINATEDS].float() + ) * q_next_best + + # Compute the RHS of the Bellman equation. + # Detach this node from the computation graph as we do not want to + # backpropagate through the target network when optimizing the Q loss. + q_selected_target = ( + batch[Columns.REWARDS] + + (config.gamma ** batch["n_step"]) * q_next_best_masked + ).detach() + + # Choose the requested loss function. Note, in case of the Huber loss + # we fall back to the default of `delta=1.0`. + loss_fn = nn.HuberLoss if config.td_error_loss_fn == "huber" else nn.MSELoss + # Compute the TD error. + td_error = torch.abs(q_selected - q_selected_target) + # Compute the weighted loss (importance sampling weights). + total_loss = possibly_masked_mean( + batch["weights"] + * loss_fn(reduction="none")(q_selected, q_selected_target) + ) + + # Log the TD-error with reduce=None, such that - in case we have n parallel + # Learners - we will re-concatenate the produced TD-error tensors to yield + # a 1:1 representation of the original batch. + self.metrics.log_value( + key=(module_id, TD_ERROR_KEY), + value=td_error, + reduce=None, + clear_on_reduce=True, + ) + # Log other important loss stats (reduce=mean (default), but with window=1 + # in order to keep them history free). + self.metrics.log_dict( + { + QF_LOSS_KEY: total_loss, + QF_MEAN_KEY: possibly_masked_mean(q_selected), + QF_MAX_KEY: possibly_masked_max(q_selected), + QF_MIN_KEY: possibly_masked_min(q_selected), + TD_ERROR_MEAN_KEY: possibly_masked_mean(td_error), + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + # If we learn a Q-value distribution log the support and average + # probabilities. + if config.num_atoms > 1: + # Log important loss stats. + self.metrics.log_dict( + { + ATOMS: torch.mean(z), + # The absolute difference in expectation between the actions + # should (at least mildly) rise. + "expectations_abs_diff": torch.mean( + torch.abs( + torch.diff( + torch.sum(fwd_out[QF_PROBS].mean(dim=0) * z, dim=1) + ).mean(dim=0) + ) + ), + # The total variation distance should measure the distance between + # return distributions of different actions. This should (at least + # mildly) increase during training when the agent differentiates + # more between actions. + "dist_total_variation_dist": torch.diff( + fwd_out[QF_PROBS].mean(dim=0), dim=0 + ) + .abs() + .sum() + * 0.5, + # The maximum distance between the action distributions. This metric + # should increase over the course of training. + "dist_max_abs_distance": torch.max( + torch.diff(fwd_out[QF_PROBS].mean(dim=0), dim=0).abs() + ), + # Mean shannon entropy of action distributions. This should decrease + # over the course of training. + "action_dist_mean_entropy": torch.mean( + ( + fwd_out[QF_PROBS].mean(dim=0) + * torch.log(fwd_out[QF_PROBS].mean(dim=0)) + ).sum(dim=1), + dim=0, + ), + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + + return total_loss diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d4b2adb0d57ed1d44ced601c57fa78c91bc4bede --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__init__.py @@ -0,0 +1,15 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf + +[2] Mastering Atari with Discrete World Models - 2021 +D. Hafner, T. Lillicrap, M. Norouzi, J. Ba +https://arxiv.org/pdf/2010.02193.pdf +""" +from ray.rllib.algorithms.dreamerv3.dreamerv3 import DreamerV3, DreamerV3Config + +__all__ = [ + "DreamerV3", + "DreamerV3Config", +] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71b27c46c777316b1406babed78d3a9181cb3938 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d36a109c420cf6879dd08943f6d50af8b71257d Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3_catalog.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3_catalog.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a24c3fd38b89fcee7112c99d6ab3224b11943984 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3_catalog.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3_learner.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3_learner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..152e4fc39c0384a4b8dbe7eef0f0b33b48d38128 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3_learner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3_rl_module.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3_rl_module.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..638d2124d9440fa15558fbec69a46a95de6aff8b Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/__pycache__/dreamerv3_rl_module.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3.py new file mode 100644 index 0000000000000000000000000000000000000000..935f7a53a738659a1650c74621a14c3479c675eb --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3.py @@ -0,0 +1,731 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf + +[2] Mastering Atari with Discrete World Models - 2021 +D. Hafner, T. Lillicrap, M. Norouzi, J. Ba +https://arxiv.org/pdf/2010.02193.pdf +""" + +import logging +from typing import Any, Dict, Optional, Union + +import gymnasium as gym +from typing_extensions import Self + +from ray.rllib.algorithms.algorithm import Algorithm +from ray.rllib.algorithms.algorithm_config import AlgorithmConfig, NotProvided +from ray.rllib.algorithms.dreamerv3.dreamerv3_catalog import DreamerV3Catalog +from ray.rllib.algorithms.dreamerv3.utils import do_symlog_obs +from ray.rllib.algorithms.dreamerv3.utils.add_is_firsts_to_batch import ( + AddIsFirstsToBatch, +) +from ray.rllib.algorithms.dreamerv3.utils.summaries import ( + report_dreamed_eval_trajectory_vs_samples, + report_predicted_vs_sampled_obs, + report_sampling_and_replay_buffer, +) +from ray.rllib.connectors.common import AddStatesFromEpisodesToBatch +from ray.rllib.core import DEFAULT_MODULE_ID +from ray.rllib.core.columns import Columns +from ray.rllib.core.rl_module.rl_module import RLModuleSpec +from ray.rllib.execution.rollout_ops import synchronous_parallel_sample +from ray.rllib.policy.sample_batch import SampleBatch +from ray.rllib.utils import deep_update +from ray.rllib.utils.annotations import PublicAPI, override +from ray.rllib.utils.metrics import ( + ENV_RUNNER_RESULTS, + LEARN_ON_BATCH_TIMER, + LEARNER_RESULTS, + NUM_ENV_STEPS_SAMPLED_LIFETIME, + NUM_ENV_STEPS_TRAINED_LIFETIME, + NUM_GRAD_UPDATES_LIFETIME, + NUM_SYNCH_WORKER_WEIGHTS, + REPLAY_BUFFER_RESULTS, + SAMPLE_TIMER, + SYNCH_WORKER_WEIGHTS_TIMER, + TIMERS, +) +from ray.rllib.utils.numpy import one_hot +from ray.rllib.utils.replay_buffers.episode_replay_buffer import EpisodeReplayBuffer +from ray.rllib.utils.typing import LearningRateOrSchedule + +logger = logging.getLogger(__name__) + + +class DreamerV3Config(AlgorithmConfig): + """Defines a configuration class from which a DreamerV3 can be built. + + .. testcode:: + + from ray.rllib.algorithms.dreamerv3 import DreamerV3Config + config = ( + DreamerV3Config() + .environment("CartPole-v1") + .training( + model_size="XS", + training_ratio=1, + # TODO + model={ + "batch_size_B": 1, + "batch_length_T": 1, + "horizon_H": 1, + "gamma": 0.997, + "model_size": "XS", + }, + ) + ) + + config = config.learners(num_learners=0) + # Build a Algorithm object from the config and run 1 training iteration. + algo = config.build() + # algo.train() + del algo + + .. testoutput:: + :hide: + + ... + """ + + def __init__(self, algo_class=None): + """Initializes a DreamerV3Config instance.""" + super().__init__(algo_class=algo_class or DreamerV3) + + # fmt: off + # __sphinx_doc_begin__ + + # DreamerV3 specific settings: + self.model_size = "XS" + self.training_ratio = 1024 + + self.replay_buffer_config = { + "type": "EpisodeReplayBuffer", + "capacity": int(1e6), + } + self.world_model_lr = 1e-4 + self.actor_lr = 3e-5 + self.critic_lr = 3e-5 + self.batch_size_B = 16 + self.batch_length_T = 64 + self.horizon_H = 15 + self.gae_lambda = 0.95 # [1] eq. 7. + self.entropy_scale = 3e-4 # [1] eq. 11. + self.return_normalization_decay = 0.99 # [1] eq. 11 and 12. + self.train_critic = True + self.train_actor = True + self.intrinsic_rewards_scale = 0.1 + self.world_model_grad_clip_by_global_norm = 1000.0 + self.critic_grad_clip_by_global_norm = 100.0 + self.actor_grad_clip_by_global_norm = 100.0 + self.symlog_obs = "auto" + self.use_float16 = False + self.use_curiosity = False + + # Reporting. + # DreamerV3 is super sample efficient and only needs very few episodes + # (normally) to learn. Leaving this at its default value would gravely + # underestimate the learning performance over the course of an experiment. + self.metrics_num_episodes_for_smoothing = 1 + self.report_individual_batch_item_stats = False + self.report_dream_data = False + self.report_images_and_videos = False + + # Override some of AlgorithmConfig's default values with DreamerV3-specific + # values. + self.lr = None + self.gamma = 0.997 # [1] eq. 7. + # Do not use! Set `batch_size_B` and `batch_length_T` instead. + self.train_batch_size = None + self.num_env_runners = 0 + self.rollout_fragment_length = 1 + # Dreamer only runs on the new API stack. + self.enable_rl_module_and_learner = True + self.enable_env_runner_and_connector_v2 = True + # TODO (sven): DreamerV3 still uses its own EnvRunner class. This env-runner + # does not use connectors. We therefore should not attempt to merge/broadcast + # the connector states between EnvRunners (if >0). Note that this is only + # relevant if num_env_runners > 0, which is normally not the case when using + # this algo. + self.use_worker_filter_stats = False + # __sphinx_doc_end__ + # fmt: on + + @override(AlgorithmConfig) + def build_env_to_module_connector(self, env, spaces, device): + connector = super().build_env_to_module_connector(env, spaces, device) + + # Prepend the "is_first" connector such that the RSSM knows, when to insert + # its (learned) internal state into the batch. + # We have to do this before the `AddStatesFromEpisodesToBatch` piece + # such that the column is properly batched/time-ranked. + if self.add_default_connectors_to_learner_pipeline: + connector.insert_before( + AddStatesFromEpisodesToBatch, + AddIsFirstsToBatch(), + ) + return connector + + @property + def batch_size_B_per_learner(self): + """Returns the batch_size_B per Learner worker. + + Needed by some of the DreamerV3 loss math.""" + return self.batch_size_B // (self.num_learners or 1) + + @override(AlgorithmConfig) + def training( + self, + *, + model_size: Optional[str] = NotProvided, + training_ratio: Optional[float] = NotProvided, + batch_size_B: Optional[int] = NotProvided, + batch_length_T: Optional[int] = NotProvided, + horizon_H: Optional[int] = NotProvided, + gae_lambda: Optional[float] = NotProvided, + entropy_scale: Optional[float] = NotProvided, + return_normalization_decay: Optional[float] = NotProvided, + train_critic: Optional[bool] = NotProvided, + train_actor: Optional[bool] = NotProvided, + intrinsic_rewards_scale: Optional[float] = NotProvided, + world_model_lr: Optional[LearningRateOrSchedule] = NotProvided, + actor_lr: Optional[LearningRateOrSchedule] = NotProvided, + critic_lr: Optional[LearningRateOrSchedule] = NotProvided, + world_model_grad_clip_by_global_norm: Optional[float] = NotProvided, + critic_grad_clip_by_global_norm: Optional[float] = NotProvided, + actor_grad_clip_by_global_norm: Optional[float] = NotProvided, + symlog_obs: Optional[Union[bool, str]] = NotProvided, + use_float16: Optional[bool] = NotProvided, + replay_buffer_config: Optional[dict] = NotProvided, + use_curiosity: Optional[bool] = NotProvided, + **kwargs, + ) -> Self: + """Sets the training related configuration. + + Args: + model_size: The main switch for adjusting the overall model size. See [1] + (table B) for more information on the effects of this setting on the + model architecture. + Supported values are "XS", "S", "M", "L", "XL" (as per the paper), as + well as, "nano", "micro", "mini", and "XXS" (for RLlib's + implementation). See ray.rllib.algorithms.dreamerv3.utils. + __init__.py for the details on what exactly each size does to the layer + sizes, number of layers, etc.. + training_ratio: The ratio of total steps trained (sum of the sizes of all + batches ever sampled from the replay buffer) over the total env steps + taken (in the actual environment, not the dreamed one). For example, + if the training_ratio is 1024 and the batch size is 1024, we would take + 1 env step for every training update: 1024 / 1. If the training ratio + is 512 and the batch size is 1024, we would take 2 env steps and then + perform a single training update (on a 1024 batch): 1024 / 2. + batch_size_B: The batch size (B) interpreted as number of rows (each of + length `batch_length_T`) to sample from the replay buffer in each + iteration. + batch_length_T: The batch length (T) interpreted as the length of each row + sampled from the replay buffer in each iteration. Note that + `batch_size_B` rows will be sampled in each iteration. Rows normally + contain consecutive data (consecutive timesteps from the same episode), + but there might be episode boundaries in a row as well. + horizon_H: The horizon (in timesteps) used to create dreamed data from the + world model, which in turn is used to train/update both actor- and + critic networks. + gae_lambda: The lambda parameter used for computing the GAE-style + value targets for the actor- and critic losses. + entropy_scale: The factor with which to multiply the entropy loss term + inside the actor loss. + return_normalization_decay: The decay value to use when computing the + running EMA values for return normalization (used in the actor loss). + train_critic: Whether to train the critic network. If False, `train_actor` + must also be False (cannot train actor w/o training the critic). + train_actor: Whether to train the actor network. If True, `train_critic` + must also be True (cannot train actor w/o training the critic). + intrinsic_rewards_scale: The factor to multiply intrinsic rewards with + before adding them to the extrinsic (environment) rewards. + world_model_lr: The learning rate or schedule for the world model optimizer. + actor_lr: The learning rate or schedule for the actor optimizer. + critic_lr: The learning rate or schedule for the critic optimizer. + world_model_grad_clip_by_global_norm: World model grad clipping value + (by global norm). + critic_grad_clip_by_global_norm: Critic grad clipping value + (by global norm). + actor_grad_clip_by_global_norm: Actor grad clipping value (by global norm). + symlog_obs: Whether to symlog observations or not. If set to "auto" + (default), will check for the environment's observation space and then + only symlog if not an image space. + use_float16: Whether to train with mixed float16 precision. In this mode, + model parameters are stored as float32, but all computations are + performed in float16 space (except for losses and distribution params + and outputs). + replay_buffer_config: Replay buffer config. + Only serves in DreamerV3 to set the capacity of the replay buffer. + Note though that in the paper ([1]) a size of 1M is used for all + benchmarks and there doesn't seem to be a good reason to change this + parameter. + Examples: + { + "type": "EpisodeReplayBuffer", + "capacity": 100000, + } + + Returns: + This updated AlgorithmConfig object. + """ + # Not fully supported/tested yet. + if use_curiosity is not NotProvided: + raise ValueError( + "`DreamerV3Config.curiosity` is not fully supported and tested yet! " + "It thus remains disabled for now." + ) + + # Pass kwargs onto super's `training()` method. + super().training(**kwargs) + + if model_size is not NotProvided: + self.model_size = model_size + if training_ratio is not NotProvided: + self.training_ratio = training_ratio + if batch_size_B is not NotProvided: + self.batch_size_B = batch_size_B + if batch_length_T is not NotProvided: + self.batch_length_T = batch_length_T + if horizon_H is not NotProvided: + self.horizon_H = horizon_H + if gae_lambda is not NotProvided: + self.gae_lambda = gae_lambda + if entropy_scale is not NotProvided: + self.entropy_scale = entropy_scale + if return_normalization_decay is not NotProvided: + self.return_normalization_decay = return_normalization_decay + if train_critic is not NotProvided: + self.train_critic = train_critic + if train_actor is not NotProvided: + self.train_actor = train_actor + if intrinsic_rewards_scale is not NotProvided: + self.intrinsic_rewards_scale = intrinsic_rewards_scale + if world_model_lr is not NotProvided: + self.world_model_lr = world_model_lr + if actor_lr is not NotProvided: + self.actor_lr = actor_lr + if critic_lr is not NotProvided: + self.critic_lr = critic_lr + if world_model_grad_clip_by_global_norm is not NotProvided: + self.world_model_grad_clip_by_global_norm = ( + world_model_grad_clip_by_global_norm + ) + if critic_grad_clip_by_global_norm is not NotProvided: + self.critic_grad_clip_by_global_norm = critic_grad_clip_by_global_norm + if actor_grad_clip_by_global_norm is not NotProvided: + self.actor_grad_clip_by_global_norm = actor_grad_clip_by_global_norm + if symlog_obs is not NotProvided: + self.symlog_obs = symlog_obs + if use_float16 is not NotProvided: + self.use_float16 = use_float16 + if replay_buffer_config is not NotProvided: + # Override entire `replay_buffer_config` if `type` key changes. + # Update, if `type` key remains the same or is not specified. + new_replay_buffer_config = deep_update( + {"replay_buffer_config": self.replay_buffer_config}, + {"replay_buffer_config": replay_buffer_config}, + False, + ["replay_buffer_config"], + ["replay_buffer_config"], + ) + self.replay_buffer_config = new_replay_buffer_config["replay_buffer_config"] + + return self + + @override(AlgorithmConfig) + def reporting( + self, + *, + report_individual_batch_item_stats: Optional[bool] = NotProvided, + report_dream_data: Optional[bool] = NotProvided, + report_images_and_videos: Optional[bool] = NotProvided, + **kwargs, + ): + """Sets the reporting related configuration. + + Args: + report_individual_batch_item_stats: Whether to include loss and other stats + per individual timestep inside the training batch in the result dict + returned by `training_step()`. If True, besides the `CRITIC_L_total`, + the individual critic loss values per batch row and time axis step + in the train batch (CRITIC_L_total_B_T) will also be part of the + results. + report_dream_data: Whether to include the dreamed trajectory data in the + result dict returned by `training_step()`. If True, however, will + slice each reported item in the dream data down to the shape. + (H, B, t=0, ...), where H is the horizon and B is the batch size. The + original time axis will only be represented by the first timestep + to not make this data too large to handle. + report_images_and_videos: Whether to include any image/video data in the + result dict returned by `training_step()`. + **kwargs: + + Returns: + This updated AlgorithmConfig object. + """ + super().reporting(**kwargs) + + if report_individual_batch_item_stats is not NotProvided: + self.report_individual_batch_item_stats = report_individual_batch_item_stats + if report_dream_data is not NotProvided: + self.report_dream_data = report_dream_data + if report_images_and_videos is not NotProvided: + self.report_images_and_videos = report_images_and_videos + + return self + + @override(AlgorithmConfig) + def validate(self) -> None: + # Call the super class' validation method first. + super().validate() + + # Make sure, users are not using DreamerV3 yet for multi-agent: + if self.is_multi_agent: + self._value_error("DreamerV3 does NOT support multi-agent setups yet!") + + # Make sure, we are configure for the new API stack. + if not self.enable_rl_module_and_learner: + self._value_error( + "DreamerV3 must be run with `config.api_stack(" + "enable_rl_module_and_learner=True)`!" + ) + + # If run on several Learners, the provided batch_size_B must be a multiple + # of `num_learners`. + if self.num_learners > 1 and (self.batch_size_B % self.num_learners != 0): + self._value_error( + f"Your `batch_size_B` ({self.batch_size_B}) must be a multiple of " + f"`num_learners` ({self.num_learners}) in order for " + "DreamerV3 to be able to split batches evenly across your Learner " + "processes." + ) + + # Cannot train actor w/o critic. + if self.train_actor and not self.train_critic: + self._value_error( + "Cannot train actor network (`train_actor=True`) w/o training critic! " + "Make sure you either set `train_critic=True` or `train_actor=False`." + ) + # Use DreamerV3 specific batch size settings. + if self.train_batch_size is not None: + self._value_error( + "`train_batch_size` should NOT be set! Use `batch_size_B` and " + "`batch_length_T` instead." + ) + # Must be run with `EpisodeReplayBuffer` type. + if self.replay_buffer_config.get("type") != "EpisodeReplayBuffer": + self._value_error( + "DreamerV3 must be run with the `EpisodeReplayBuffer` type! None " + "other supported." + ) + + @override(AlgorithmConfig) + def get_default_learner_class(self): + if self.framework_str == "torch": + from ray.rllib.algorithms.dreamerv3.torch.dreamerv3_torch_learner import ( + DreamerV3TorchLearner, + ) + + return DreamerV3TorchLearner + else: + raise ValueError(f"The framework {self.framework_str} is not supported.") + + @override(AlgorithmConfig) + def get_default_rl_module_spec(self) -> RLModuleSpec: + if self.framework_str == "torch": + from ray.rllib.algorithms.dreamerv3.torch.dreamerv3_torch_rl_module import ( + DreamerV3TorchRLModule as module, + ) + + else: + raise ValueError(f"The framework {self.framework_str} is not supported.") + + return RLModuleSpec(module_class=module, catalog_class=DreamerV3Catalog) + + @property + @override(AlgorithmConfig) + def _model_config_auto_includes(self) -> Dict[str, Any]: + return super()._model_config_auto_includes | { + "gamma": self.gamma, + "horizon_H": self.horizon_H, + "model_size": self.model_size, + "symlog_obs": self.symlog_obs, + "use_float16": self.use_float16, + "batch_length_T": self.batch_length_T, + } + + +class DreamerV3(Algorithm): + """Implementation of the model-based DreamerV3 RL algorithm described in [1].""" + + # TODO (sven): Deprecate/do-over the Algorithm.compute_single_action() API. + @override(Algorithm) + def compute_single_action(self, *args, **kwargs): + raise NotImplementedError( + "DreamerV3 does not support the `compute_single_action()` API. Refer to the" + " README here (https://github.com/ray-project/ray/tree/master/rllib/" + "algorithms/dreamerv3) to find more information on how to run action " + "inference with this algorithm." + ) + + @classmethod + @override(Algorithm) + def get_default_config(cls) -> DreamerV3Config: + return DreamerV3Config() + + @override(Algorithm) + def setup(self, config: AlgorithmConfig): + super().setup(config) + + # Share RLModule between EnvRunner and single (local) Learner instance. + # To avoid possibly expensive weight synching step. + # if self.config.share_module_between_env_runner_and_learner: + # assert self.env_runner.module is None + # self.env_runner.module = self.learner_group._learner.module[ + # DEFAULT_MODULE_ID + # ] + + # Create a replay buffer for storing actual env samples. + self.replay_buffer = EpisodeReplayBuffer( + capacity=self.config.replay_buffer_config["capacity"], + batch_size_B=self.config.batch_size_B, + batch_length_T=self.config.batch_length_T, + ) + + @override(Algorithm) + def training_step(self) -> None: + # Push enough samples into buffer initially before we start training. + if self.training_iteration == 0: + logger.info( + "Filling replay buffer so it contains at least " + f"{self.config.batch_size_B * self.config.batch_length_T} timesteps " + "(required for a single train batch)." + ) + + # Have we sampled yet in this `training_step()` call? + have_sampled = False + with self.metrics.log_time((TIMERS, SAMPLE_TIMER)): + # Continue sampling from the actual environment (and add collected samples + # to our replay buffer) as long as we: + while ( + # a) Don't have at least batch_size_B x batch_length_T timesteps stored + # in the buffer. This is the minimum needed to train. + self.replay_buffer.get_num_timesteps() + < (self.config.batch_size_B * self.config.batch_length_T) + # b) The computed `training_ratio` is >= the configured (desired) + # training ratio (meaning we should continue sampling). + or self.training_ratio >= self.config.training_ratio + # c) we have not sampled at all yet in this `training_step()` call. + or not have_sampled + ): + # Sample using the env runner's module. + episodes, env_runner_results = synchronous_parallel_sample( + worker_set=self.env_runner_group, + max_agent_steps=( + self.config.rollout_fragment_length + * self.config.num_envs_per_env_runner + ), + sample_timeout_s=self.config.sample_timeout_s, + _uses_new_env_runners=True, + _return_metrics=True, + ) + self.metrics.aggregate(env_runner_results, key=ENV_RUNNER_RESULTS) + # Add ongoing and finished episodes into buffer. The buffer will + # automatically take care of properly concatenating (by episode IDs) + # the different chunks of the same episodes, even if they come in via + # separate `add()` calls. + self.replay_buffer.add(episodes=episodes) + have_sampled = True + + # We took B x T env steps. + env_steps_last_regular_sample = sum(len(eps) for eps in episodes) + total_sampled = env_steps_last_regular_sample + + # If we have never sampled before (just started the algo and not + # recovered from a checkpoint), sample B random actions first. + if ( + self.metrics.peek( + (ENV_RUNNER_RESULTS, NUM_ENV_STEPS_SAMPLED_LIFETIME), + default=0, + ) + == 0 + ): + _episodes, _env_runner_results = synchronous_parallel_sample( + worker_set=self.env_runner_group, + max_agent_steps=( + self.config.batch_size_B * self.config.batch_length_T + - env_steps_last_regular_sample + ), + sample_timeout_s=self.config.sample_timeout_s, + random_actions=True, + _uses_new_env_runners=True, + _return_metrics=True, + ) + self.metrics.aggregate(_env_runner_results, key=ENV_RUNNER_RESULTS) + self.replay_buffer.add(episodes=_episodes) + total_sampled += sum(len(eps) for eps in _episodes) + + # Summarize environment interaction and buffer data. + report_sampling_and_replay_buffer( + metrics=self.metrics, replay_buffer=self.replay_buffer + ) + # Get the replay buffer metrics. + replay_buffer_results = self.local_replay_buffer.get_metrics() + self.metrics.aggregate([replay_buffer_results], key=REPLAY_BUFFER_RESULTS) + + # Continue sampling batch_size_B x batch_length_T sized batches from the buffer + # and using these to update our models (`LearnerGroup.update()`) + # until the computed `training_ratio` is larger than the configured one, meaning + # we should go back and collect more samples again from the actual environment. + # However, when calculating the `training_ratio` here, we use only the + # trained steps in this very `training_step()` call over the most recent sample + # amount (`env_steps_last_regular_sample`), not the global values. This is to + # avoid a heavy overtraining at the very beginning when we have just pre-filled + # the buffer with the minimum amount of samples. + replayed_steps_this_iter = sub_iter = 0 + while ( + replayed_steps_this_iter / env_steps_last_regular_sample + ) < self.config.training_ratio: + # Time individual batch updates. + with self.metrics.log_time((TIMERS, LEARN_ON_BATCH_TIMER)): + logger.info(f"\tSub-iteration {self.training_iteration}/{sub_iter})") + + # Draw a new sample from the replay buffer. + sample = self.replay_buffer.sample( + batch_size_B=self.config.batch_size_B, + batch_length_T=self.config.batch_length_T, + ) + replayed_steps = self.config.batch_size_B * self.config.batch_length_T + replayed_steps_this_iter += replayed_steps + + if isinstance( + self.env_runner.env.single_action_space, gym.spaces.Discrete + ): + sample["actions_ints"] = sample[Columns.ACTIONS] + sample[Columns.ACTIONS] = one_hot( + sample["actions_ints"], + depth=self.env_runner.env.single_action_space.n, + ) + + # Perform the actual update via our learner group. + learner_results = self.learner_group.update( + batch=SampleBatch(sample).as_multi_agent(), + # TODO(sven): Maybe we should do this broadcase of global timesteps + # at the end, like for EnvRunner global env step counts. Maybe when + # we request the state from the Learners, we can - at the same + # time - send the current globally summed/reduced-timesteps. + timesteps={ + NUM_ENV_STEPS_SAMPLED_LIFETIME: self.metrics.peek( + (ENV_RUNNER_RESULTS, NUM_ENV_STEPS_SAMPLED_LIFETIME), + default=0, + ) + }, + ) + self.metrics.aggregate(learner_results, key=LEARNER_RESULTS) + + sub_iter += 1 + self.metrics.log_value(NUM_GRAD_UPDATES_LIFETIME, 1, reduce="sum") + + # Log videos showing how the decoder produces observation predictions + # from the posterior states. + # Only every n iterations and only for the first sampled batch row + # (videos are `config.batch_length_T` frames long). + report_predicted_vs_sampled_obs( + # TODO (sven): DreamerV3 is single-agent only. + metrics=self.metrics, + sample=sample, + batch_size_B=self.config.batch_size_B, + batch_length_T=self.config.batch_length_T, + symlog_obs=do_symlog_obs( + self.env_runner.env.single_observation_space, + self.config.symlog_obs, + ), + do_report=( + self.config.report_images_and_videos + and self.training_iteration % 100 == 0 + ), + ) + + # Log videos showing some of the dreamed trajectories and compare them with the + # actual trajectories from the train batch. + # Only every n iterations and only for the first sampled batch row AND first ts. + # (videos are `config.horizon_H` frames long originating from the observation + # at B=0 and T=0 in the train batch). + report_dreamed_eval_trajectory_vs_samples( + metrics=self.metrics, + sample=sample, + burn_in_T=0, + dreamed_T=self.config.horizon_H + 1, + dreamer_model=self.env_runner.module.dreamer_model, + symlog_obs=do_symlog_obs( + self.env_runner.env.single_observation_space, + self.config.symlog_obs, + ), + do_report=( + self.config.report_dream_data and self.training_iteration % 100 == 0 + ), + framework=self.config.framework_str, + ) + + # Update weights - after learning on the LearnerGroup - on all EnvRunner + # workers. + with self.metrics.log_time((TIMERS, SYNCH_WORKER_WEIGHTS_TIMER)): + # Only necessary if RLModule is not shared between (local) EnvRunner and + # (local) Learner. + # if not self.config.share_module_between_env_runner_and_learner: + self.metrics.log_value(NUM_SYNCH_WORKER_WEIGHTS, 1, reduce="sum") + self.env_runner_group.sync_weights( + from_worker_or_learner_group=self.learner_group, + inference_only=True, + ) + + # Add train results and the actual training ratio to stats. The latter should + # be close to the configured `training_ratio`. + self.metrics.log_value("actual_training_ratio", self.training_ratio, window=1) + + @property + def training_ratio(self) -> float: + """Returns the actual training ratio of this Algorithm (not the configured one). + + The training ratio is copmuted by dividing the total number of steps + trained thus far (replayed from the buffer) over the total number of actual + env steps taken thus far. + """ + eps = 0.0001 + return self.metrics.peek(NUM_ENV_STEPS_TRAINED_LIFETIME, default=0) / ( + ( + self.metrics.peek( + (ENV_RUNNER_RESULTS, NUM_ENV_STEPS_SAMPLED_LIFETIME), + default=eps, + ) + or eps + ) + ) + + # TODO (sven): Remove this once DreamerV3 is on the new SingleAgentEnvRunner. + @PublicAPI + def __setstate__(self, state) -> None: + """Sts the algorithm to the provided state + + Args: + state: The state dictionary to restore this `DreamerV3` instance to. + `state` may have been returned by a call to an `Algorithm`'s + `__getstate__()` method. + """ + # Call the `Algorithm`'s `__setstate__()` method. + super().__setstate__(state=state) + + # Assign the module to the local `EnvRunner` if sharing is enabled. + # Note, in `Learner.restore_from_path()` the module is first deleted + # and then a new one is built - therefore the worker has no + # longer a copy of the learner. + if self.config.share_module_between_env_runner_and_learner: + assert id(self.env_runner.module) != id( + self.learner_group._learner.module[DEFAULT_MODULE_ID] + ) + self.env_runner.module = self.learner_group._learner.module[ + DEFAULT_MODULE_ID + ] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3_catalog.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3_catalog.py new file mode 100644 index 0000000000000000000000000000000000000000..ce16b747ec4d2251105591d94d604125c03eb494 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3_catalog.py @@ -0,0 +1,184 @@ +import gymnasium as gym +import numpy as np + +from ray.rllib.algorithms.dreamerv3.utils import ( + do_symlog_obs, + get_gru_units, + get_num_z_categoricals, + get_num_z_classes, +) +from ray.rllib.core.models.base import Encoder, Model +from ray.rllib.core.models.catalog import Catalog +from ray.rllib.utils import override + + +class DreamerV3Catalog(Catalog): + """The Catalog class used to build all the models needed for DreamerV3 training.""" + + def __init__( + self, + observation_space: gym.Space, + action_space: gym.Space, + model_config_dict: dict, + ): + """Initializes a DreamerV3Catalog instance. + + Args: + observation_space: The observation space of the environment. + action_space: The action space of the environment. + model_config_dict: The model config to use. + """ + super().__init__( + observation_space=observation_space, + action_space=action_space, + model_config_dict=model_config_dict, + ) + + self.model_size = self._model_config_dict["model_size"] + self.is_img_space = len(self.observation_space.shape) in [2, 3] + self.is_gray_scale = ( + self.is_img_space and len(self.observation_space.shape) == 2 + ) + # Compute the size of the vector coming out of the sequence model. + self.h_plus_z_flat = get_gru_units(self.model_size) + ( + get_num_z_categoricals(self.model_size) * get_num_z_classes(self.model_size) + ) + + # TODO (sven): We should work with sub-component configurations here, + # and even try replacing all current Dreamer model components with + # our default primitives. But for now, we'll construct the DreamerV3Model + # directly in our `build_...()` methods. + + @override(Catalog) + def build_encoder(self, framework: str) -> Encoder: + """Builds the World-Model's encoder network depending on the obs space.""" + if self.is_img_space: + if framework == "torch": + from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + cnn_atari, + ) + + return cnn_atari.CNNAtari( + gray_scaled=self.is_gray_scale, + model_size=self.model_size, + ) + else: + raise ValueError(f"`framework={framework}` not supported!") + + else: + if framework == "torch": + from ray.rllib.algorithms.dreamerv3.torch.models.components import mlp + + return mlp.MLP( + input_size=int(np.prod(self.observation_space.shape)), + model_size=self.model_size, + ) + else: + raise ValueError(f"`framework={framework}` not supported!") + + def build_decoder(self, framework: str) -> Model: + """Builds the World-Model's decoder network depending on the obs space.""" + + if self.is_img_space: + if framework == "torch": + from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + conv_transpose_atari, + ) + + return conv_transpose_atari.ConvTransposeAtari( + input_size=self.h_plus_z_flat, + gray_scaled=self.is_gray_scale, + model_size=self.model_size, + ) + else: + raise ValueError(f"`framework={framework}` not supported!") + + else: + if framework == "torch": + from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + vector_decoder, + ) + + return vector_decoder.VectorDecoder( + input_size=self.h_plus_z_flat, + model_size=self.model_size, + observation_space=self.observation_space, + ) + else: + raise ValueError(f"`framework={framework}` not supported!") + + def build_world_model(self, framework: str, *, encoder, decoder) -> Model: + symlog_obs = do_symlog_obs( + self.observation_space, + self._model_config_dict.get("symlog_obs", "auto"), + ) + + if framework == "torch": + from ray.rllib.algorithms.dreamerv3.torch.models.world_model import ( + WorldModel, + ) + else: + raise ValueError(f"`framework={framework}` not supported!") + + return WorldModel( + model_size=self.model_size, + observation_space=self.observation_space, + action_space=self.action_space, + batch_length_T=self._model_config_dict["batch_length_T"], + encoder=encoder, + decoder=decoder, + symlog_obs=symlog_obs, + ) + + def build_actor(self, framework: str) -> Model: + if framework == "torch": + from ray.rllib.algorithms.dreamerv3.torch.models.actor_network import ( + ActorNetwork, + ) + + return ActorNetwork( + input_size=self.h_plus_z_flat, + action_space=self.action_space, + model_size=self.model_size, + ) + else: + raise ValueError(f"`framework={framework}` not supported!") + + def build_critic(self, framework: str) -> Model: + if framework == "torch": + from ray.rllib.algorithms.dreamerv3.torch.models.critic_network import ( + CriticNetwork, + ) + + return CriticNetwork( + input_size=self.h_plus_z_flat, + model_size=self.model_size, + ) + else: + raise ValueError(f"`framework={framework}` not supported!") + + def build_dreamer_model( + self, framework: str, *, world_model, actor, critic, horizon=None, gamma=None + ) -> Model: + if framework == "torch": + from ray.rllib.algorithms.dreamerv3.torch.models.dreamer_model import ( + DreamerModel, + ) + else: + raise ValueError(f"`framework={framework}` not supported!") + + return DreamerModel( + model_size=self.model_size, + action_space=self.action_space, + world_model=world_model, + actor=actor, + critic=critic, + **( + {} + if framework == "torch" + else { + "horizon": horizon, + "gamma": gamma, + } + ), + ) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3_learner.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3_learner.py new file mode 100644 index 0000000000000000000000000000000000000000..b2c0cf27cb220a936b2fe58422b32523e65da182 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3_learner.py @@ -0,0 +1,31 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf + +[2] Mastering Atari with Discrete World Models - 2021 +D. Hafner, T. Lillicrap, M. Norouzi, J. Ba +https://arxiv.org/pdf/2010.02193.pdf +""" +from ray.rllib.core.learner.learner import Learner +from ray.rllib.utils.annotations import ( + OverrideToImplementCustomLogic_CallToSuperRecommended, + override, +) + + +class DreamerV3Learner(Learner): + """DreamerV3 specific Learner class. + + Only implements the `after_gradient_based_update()` method to define the logic + for updating the critic EMA-copy after each training step. + """ + + @OverrideToImplementCustomLogic_CallToSuperRecommended + @override(Learner) + def after_gradient_based_update(self, *, timesteps): + super().after_gradient_based_update(timesteps=timesteps) + + # Update EMA weights of the critic. + for module_id, module in self.module._rl_modules.items(): + module.unwrapped().critic.update_ema() diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3_rl_module.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3_rl_module.py new file mode 100644 index 0000000000000000000000000000000000000000..5cf8f4884a97fbb9e1db6c674f9b856e5b55160e --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/dreamerv3_rl_module.py @@ -0,0 +1,85 @@ +""" +This file holds framework-agnostic components for DreamerV3's RLModule. +""" + +import abc +from typing import Dict + +from ray.rllib.algorithms.dreamerv3.torch.models.actor_network import ActorNetwork +from ray.rllib.algorithms.dreamerv3.torch.models.critic_network import CriticNetwork +from ray.rllib.algorithms.dreamerv3.torch.models.dreamer_model import DreamerModel +from ray.rllib.algorithms.dreamerv3.torch.models.world_model import WorldModel +from ray.rllib.algorithms.dreamerv3.utils import ( + do_symlog_obs, + get_gru_units, + get_num_z_categoricals, + get_num_z_classes, +) +from ray.rllib.core.rl_module.rl_module import RLModule +from ray.rllib.utils.annotations import override +from ray.util.annotations import DeveloperAPI + +ACTIONS_ONE_HOT = "actions_one_hot" + + +@DeveloperAPI(stability="alpha") +class DreamerV3RLModule(RLModule, abc.ABC): + @override(RLModule) + def setup(self): + super().setup() + + # Gather model-relevant settings. + T = self.model_config["batch_length_T"] + symlog_obs = do_symlog_obs( + self.observation_space, + self.model_config.get("symlog_obs", "auto"), + ) + model_size = self.model_config["model_size"] + + # Build encoder and decoder from catalog. + self.encoder = self.catalog.build_encoder(framework=self.framework) + self.decoder = self.catalog.build_decoder(framework=self.framework) + + # Build the world model (containing encoder and decoder). + self.world_model = WorldModel( + model_size=model_size, + observation_space=self.observation_space, + action_space=self.action_space, + batch_length_T=T, + encoder=self.encoder, + decoder=self.decoder, + symlog_obs=symlog_obs, + ) + input_size = get_gru_units(model_size) + get_num_z_classes( + model_size + ) * get_num_z_categoricals(model_size) + self.actor = ActorNetwork( + input_size=input_size, + action_space=self.action_space, + model_size=model_size, + ) + self.critic = CriticNetwork( + input_size=input_size, + model_size=model_size, + ) + # Build the final dreamer model (containing the world model). + self.dreamer_model = DreamerModel( + model_size=self.model_config["model_size"], + action_space=self.action_space, + world_model=self.world_model, + actor=self.actor, + critic=self.critic, + # horizon=horizon_H, + # gamma=gamma, + ) + self.action_dist_cls = self.catalog.get_action_dist_cls( + framework=self.framework + ) + + # Initialize the critic EMA net: + self.critic.init_ema() + + @override(RLModule) + def get_initial_state(self) -> Dict: + # Use `DreamerModel`'s `get_initial_state` method. + return self.dreamer_model.get_initial_state() diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30d34d58989f2243277883340b0ea8efbfe886d7 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__pycache__/dreamerv3_torch_learner.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__pycache__/dreamerv3_torch_learner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b8ee81b34be915fbb25ee29a39ecb08286d636a Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__pycache__/dreamerv3_torch_learner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__pycache__/dreamerv3_torch_rl_module.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__pycache__/dreamerv3_torch_rl_module.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..067b6d04be560ca29c56ca28a30aa98316ce9cc4 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/__pycache__/dreamerv3_torch_rl_module.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/dreamerv3_torch_learner.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/dreamerv3_torch_learner.py new file mode 100644 index 0000000000000000000000000000000000000000..8d9c0ec4ea04c71ac5985966b808aff68c7b45d8 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/dreamerv3_torch_learner.py @@ -0,0 +1,925 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf + +[2] Mastering Atari with Discrete World Models - 2021 +D. Hafner, T. Lillicrap, M. Norouzi, J. Ba +https://arxiv.org/pdf/2010.02193.pdf +""" +from typing import Any, Dict, Tuple + +import gymnasium as gym + +from ray.rllib.algorithms.dreamerv3.dreamerv3 import DreamerV3Config +from ray.rllib.algorithms.dreamerv3.dreamerv3_learner import DreamerV3Learner +from ray.rllib.core import DEFAULT_MODULE_ID +from ray.rllib.core.columns import Columns +from ray.rllib.core.learner.learner import ParamDict +from ray.rllib.core.learner.torch.torch_learner import TorchLearner +from ray.rllib.utils.annotations import override +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.torch_utils import clip_gradients, symlog, two_hot +from ray.rllib.utils.typing import ModuleID, TensorType + +torch, nn = try_import_torch() + + +class DreamerV3TorchLearner(DreamerV3Learner, TorchLearner): + """Implements DreamerV3 losses and gradient-based update logic in PyTorch. + + The critic EMA-copy update step can be found in the `DreamerV3Learner` base class, + as it is framework independent. + + We define 3 local PyTorch optimizers for the sub components "world_model", + "actor", and "critic". Each of these optimizers might use a different learning rate, + epsilon parameter, and gradient clipping thresholds and procedures. + """ + + def build(self) -> None: + super().build() + + # Store loss tensors here temporarily inside the loss function for (exact) + # consumption later by the compute gradients function. + # Keys=(module_id, optimizer_name), values=loss tensors (in-graph). + self._temp_losses = {} + + @override(TorchLearner) + def configure_optimizers_for_module( + self, module_id: ModuleID, config: DreamerV3Config = None + ): + """Create the 3 optimizers for Dreamer learning: world_model, actor, critic. + + The learning rates used are described in [1] and the epsilon values used here + - albeit probably not that important - are used by the author's own + implementation. + """ + + dreamerv3_module = self._module[module_id] + + # World Model optimizer. + optim_world_model = torch.optim.Adam( + dreamerv3_module.world_model.parameters(), + eps=1e-8, + ) + self.register_optimizer( + module_id=module_id, + optimizer_name="world_model", + optimizer=optim_world_model, + params=list(dreamerv3_module.world_model.parameters()), + lr_or_lr_schedule=config.world_model_lr, + ) + + # Actor optimizer. + optim_actor = torch.optim.Adam(dreamerv3_module.actor.parameters(), eps=1e-5) + self.register_optimizer( + module_id=module_id, + optimizer_name="actor", + optimizer=optim_actor, + params=list(dreamerv3_module.actor.parameters()), + lr_or_lr_schedule=config.actor_lr, + ) + + # Critic optimizer. + optim_critic = torch.optim.Adam(dreamerv3_module.critic.parameters(), eps=1e-5) + self.register_optimizer( + module_id=module_id, + optimizer_name="critic", + optimizer=optim_critic, + params=list(dreamerv3_module.critic.parameters()), + lr_or_lr_schedule=config.critic_lr, + ) + + @override(TorchLearner) + def postprocess_gradients_for_module( + self, + *, + module_id: ModuleID, + config: DreamerV3Config, + module_gradients_dict: Dict[str, Any], + ) -> ParamDict: + """Performs gradient clipping on the 3 module components' computed grads. + + Note that different grad global-norm clip values are used for the 3 + module components: world model, actor, and critic. + """ + for optimizer_name, optimizer in self.get_optimizers_for_module( + module_id=module_id + ): + grads_sub_dict = self.filter_param_dict_for_optimizer( + module_gradients_dict, optimizer + ) + # Figure out which grad clip setting to use. + grad_clip = ( + config.world_model_grad_clip_by_global_norm + if optimizer_name == "world_model" + else config.actor_grad_clip_by_global_norm + if optimizer_name == "actor" + else config.critic_grad_clip_by_global_norm + ) + global_norm = clip_gradients( + grads_sub_dict, grad_clip=grad_clip, grad_clip_by="global_norm" + ) + module_gradients_dict.update(grads_sub_dict) + + # DreamerV3 stats have the format: [WORLD_MODEL|ACTOR|CRITIC]_[stats name]. + self.metrics.log_dict( + { + optimizer_name.upper() + + "_gradients_global_norm": (global_norm.item()), + optimizer_name.upper() + + "_gradients_maxabs_after_clipping": ( + torch.max( + torch.abs( + torch.cat( + [g.flatten() for g in grads_sub_dict.values()] + ) + ) + ).item() + ), + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + + return module_gradients_dict + + @override(TorchLearner) + def compute_gradients( + self, + loss_per_module, + **kwargs, + ): + """Override of the default gradient computation method. + + For DreamerV3, we need to compute gradients over the individual loss terms + as otherwise, the world model's parameters would have their gradients also + be influenced by the actor- and critic loss terms/gradient computations. + """ + grads = {} + + # Do actor and critic's grad computations first, such that after those two, + # we can zero out the gradients of the world model again (they will have values + # in them from the actor/critic backwards). + for component in ["actor", "critic", "world_model"]: + optim = self.get_optimizer(DEFAULT_MODULE_ID, component) + optim.zero_grad(set_to_none=True) + # Do the backward pass + loss = self._temp_losses.pop(component.upper()) + loss.backward(retain_graph=component in ["actor", "critic"]) + optim_grads = { + pid: p.grad + for pid, p in self.filter_param_dict_for_optimizer( + self._params, optim + ).items() + } + for ref, grad in optim_grads.items(): + assert ref not in grads + grads[ref] = grad + + return grads + + @override(TorchLearner) + def compute_loss_for_module( + self, + module_id: ModuleID, + config: DreamerV3Config, + batch: Dict[str, TensorType], + fwd_out: Dict[str, TensorType], + ) -> TensorType: + # World model losses. + prediction_losses = self._compute_world_model_prediction_losses( + config=config, + rewards_B_T=batch[Columns.REWARDS], + continues_B_T=(1.0 - batch["is_terminated"].float()), + fwd_out=fwd_out, + ) + + ( + L_dyn_B_T, + L_rep_B_T, + ) = self._compute_world_model_dynamics_and_representation_loss( + config=config, fwd_out=fwd_out + ) + L_dyn = torch.mean(L_dyn_B_T) + L_rep = torch.mean(L_rep_B_T) + # Make sure values for L_rep and L_dyn are the same (they only differ in their + # gradients). + assert torch.allclose(L_dyn, L_rep) + + # Compute the actual total loss using fixed weights described in [1] eq. 4. + L_world_model_total_B_T = ( + 1.0 * prediction_losses["L_prediction_B_T"] + + 0.5 * L_dyn_B_T + + 0.1 * L_rep_B_T + ) + + # In the paper, it says to sum up timesteps, and average over + # batch (see eq. 4 in [1]). But Danijar's implementation only does + # averaging (over B and T), so we'll do this here as well. This is generally + # true for all other loss terms as well (we'll always just average, no summing + # over T axis!). + L_world_model_total = torch.mean(L_world_model_total_B_T) + + # Log world model loss stats. + self.metrics.log_dict( + { + "WORLD_MODEL_learned_initial_h": self.module[module_id] + .unwrapped() + .world_model.initial_h.mean(), + # Prediction losses. + # Decoder (obs) loss. + "WORLD_MODEL_L_decoder": prediction_losses["L_decoder"], + # Reward loss. + "WORLD_MODEL_L_reward": prediction_losses["L_reward"], + # Continue loss. + "WORLD_MODEL_L_continue": prediction_losses["L_continue"], + # Total. + "WORLD_MODEL_L_prediction": prediction_losses["L_prediction"], + # Dynamics loss. + "WORLD_MODEL_L_dynamics": L_dyn, + # Representation loss. + "WORLD_MODEL_L_representation": L_rep, + # Total loss. + "WORLD_MODEL_L_total": L_world_model_total, + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + + # Add the predicted obs distributions for possible (video) summarization. + if config.report_images_and_videos: + self.metrics.log_value( + (module_id, "WORLD_MODEL_fwd_out_obs_distribution_means_b0xT"), + fwd_out["obs_distribution_means_BxT"][: self.config.batch_length_T], + reduce=None, # No reduction, we want the obs tensor to stay in-tact. + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + + if config.report_individual_batch_item_stats: + # Log important world-model loss stats. + self.metrics.log_dict( + { + "WORLD_MODEL_L_decoder_B_T": prediction_losses["L_decoder_B_T"], + "WORLD_MODEL_L_reward_B_T": prediction_losses["L_reward_B_T"], + "WORLD_MODEL_L_continue_B_T": prediction_losses["L_continue_B_T"], + "WORLD_MODEL_L_prediction_B_T": ( + prediction_losses["L_prediction_B_T"] + ), + "WORLD_MODEL_L_dynamics_B_T": L_dyn_B_T, + "WORLD_MODEL_L_representation_B_T": L_rep_B_T, + "WORLD_MODEL_L_total_B_T": L_world_model_total_B_T, + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + + # Dream trajectories starting in all internal states (h + z_posterior) that were + # computed during world model training. + # Everything goes in as BxT: We are starting a new dream trajectory at every + # actually encountered timestep in the batch, so we are creating B*T + # trajectories of len `horizon_H`. + dream_data = ( + self.module[module_id] + .unwrapped() + .dreamer_model.dream_trajectory( + start_states={ + "h": fwd_out["h_states_BxT"], + "z": fwd_out["z_posterior_states_BxT"], + }, + start_is_terminated=batch["is_terminated"].reshape(-1), # -> BxT + timesteps_H=config.horizon_H, + gamma=config.gamma, + ) + ) + if config.report_dream_data: + # To reduce this massive amount of data a little, slice out a T=1 piece + # from each stats that has the shape (H, BxT), meaning convert e.g. + # `rewards_dreamed_t0_to_H_BxT` into `rewards_dreamed_t0_to_H_Bx1`. + # This will reduce the amount of data to be transferred and reported + # by the factor of `batch_length_T`. + self.metrics.log_dict( + { + # Replace 'T' with '1'. + key[:-1] + "1": value[:, :: config.batch_length_T] + for key, value in dream_data.items() + if key.endswith("H_BxT") + }, + key=(module_id, "dream_data"), + reduce=None, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + + value_targets_t0_to_Hm1_BxT = self._compute_value_targets( + config=config, + # Learn critic in symlog'd space. + rewards_t0_to_H_BxT=dream_data["rewards_dreamed_t0_to_H_BxT"], + intrinsic_rewards_t1_to_H_BxT=( + dream_data["rewards_intrinsic_t1_to_H_B"] + if config.use_curiosity + else None + ), + continues_t0_to_H_BxT=dream_data["continues_dreamed_t0_to_H_BxT"], + value_predictions_t0_to_H_BxT=dream_data["values_dreamed_t0_to_H_BxT"], + ) + # self.metrics.log_value( + # key=(module_id, "VALUE_TARGETS_H_BxT"), + # value=value_targets_t0_to_Hm1_BxT, + # window=1, # <- single items (should not be mean/ema-reduced over time). + # ) + + CRITIC_L_total = self._compute_critic_loss( + module_id=module_id, + config=config, + dream_data=dream_data, + value_targets_t0_to_Hm1_BxT=value_targets_t0_to_Hm1_BxT, + ) + if config.train_actor: + ACTOR_L_total = self._compute_actor_loss( + module_id=module_id, + config=config, + dream_data=dream_data, + value_targets_t0_to_Hm1_BxT=value_targets_t0_to_Hm1_BxT, + ) + else: + ACTOR_L_total = 0.0 + + self._temp_losses["ACTOR"] = ACTOR_L_total + self._temp_losses["CRITIC"] = CRITIC_L_total + self._temp_losses["WORLD_MODEL"] = L_world_model_total + + # Return the total loss as a sum of all individual losses. + return L_world_model_total + CRITIC_L_total + ACTOR_L_total + + def _compute_world_model_prediction_losses( + self, + *, + config: DreamerV3Config, + rewards_B_T: TensorType, + continues_B_T: TensorType, + fwd_out: Dict[str, TensorType], + ) -> Dict[str, TensorType]: + """Helper method computing all world-model related prediction losses. + + Prediction losses are used to train the predictors of the world model, which + are: Reward predictor, continue predictor, and the decoder (which predicts + observations). + + Args: + config: The DreamerV3Config to use. + rewards_B_T: The rewards batch in the shape (B, T) and of type float32. + continues_B_T: The continues batch in the shape (B, T) and of type float32 + (1.0 -> continue; 0.0 -> end of episode). + fwd_out: The `forward_train` outputs of the DreamerV3RLModule. + """ + + # Learn to produce symlog'd observation predictions. + # If symlog is disabled (e.g. for uint8 image inputs), `obs_symlog_BxT` is the + # same as `obs_BxT`. + obs_BxT = fwd_out["sampled_obs_symlog_BxT"] + obs_distr_means = fwd_out["obs_distribution_means_BxT"] + + # Leave time dim folded (BxT) and flatten all other (e.g. image) dims. + obs_BxT = obs_BxT.reshape(obs_BxT.shape[0], -1) + + # Squared diff loss w/ sum(!) over all (already folded) obs dims. + # decoder_loss_BxT = SUM[ (obs_distr.loc - observations)^2 ] + # Note: This is described strangely in the paper (stating a neglogp loss here), + # but the author's own implementation actually uses simple MSE with the loc + # of the Gaussian. + decoder_loss_BxT = torch.sum(torch.square(obs_distr_means - obs_BxT), dim=-1) + + # Unfold time rank back in. + decoder_loss_B_T = decoder_loss_BxT.reshape( + config.batch_size_B_per_learner, config.batch_length_T + ) + L_decoder = torch.mean(decoder_loss_B_T) + + # The FiniteDiscrete reward bucket distribution computed by our reward + # predictor. + # [B x num_buckets]. + reward_logits_BxT = fwd_out["reward_logits_BxT"] + # Learn to produce symlog'd reward predictions. + rewards_symlog_B_T = symlog(rewards_B_T) + # Fold time dim. + rewards_symlog_BxT = rewards_symlog_B_T.reshape(-1) + + # Two-hot encode. + two_hot_rewards_symlog_BxT = two_hot(rewards_symlog_BxT, device=self._device) + # two_hot_rewards_symlog_BxT=[B*T, num_buckets] + reward_log_pred_BxT = reward_logits_BxT - torch.logsumexp( + reward_logits_BxT, dim=-1, keepdim=True + ) + # Multiply with two-hot targets and neg. + reward_loss_two_hot_BxT = -torch.sum( + reward_log_pred_BxT * two_hot_rewards_symlog_BxT, dim=-1 + ) + # Unfold time rank back in. + reward_loss_two_hot_B_T = reward_loss_two_hot_BxT.reshape( + config.batch_size_B_per_learner, config.batch_length_T + ) + L_reward_two_hot = torch.mean(reward_loss_two_hot_B_T) + + # Probabilities that episode continues, computed by our continue predictor. + # [B] + continue_distr = fwd_out["continue_distribution_BxT"] + # -log(p) loss + # Fold time dim. + continues_BxT = continues_B_T.reshape(-1) + continue_loss_BxT = -continue_distr.log_prob(continues_BxT) + # Unfold time rank back in. + continue_loss_B_T = continue_loss_BxT.reshape( + config.batch_size_B_per_learner, config.batch_length_T + ) + L_continue = torch.mean(continue_loss_B_T) + + # Sum all losses together as the "prediction" loss. + L_pred_B_T = decoder_loss_B_T + reward_loss_two_hot_B_T + continue_loss_B_T + L_pred = torch.mean(L_pred_B_T) + + return { + "L_decoder_B_T": decoder_loss_B_T, + "L_decoder": L_decoder, + "L_reward": L_reward_two_hot, + "L_reward_B_T": reward_loss_two_hot_B_T, + "L_continue": L_continue, + "L_continue_B_T": continue_loss_B_T, + "L_prediction": L_pred, + "L_prediction_B_T": L_pred_B_T, + } + + def _compute_world_model_dynamics_and_representation_loss( + self, *, config: DreamerV3Config, fwd_out: Dict[str, Any] + ) -> Tuple[TensorType, TensorType]: + """Helper method computing the world-model's dynamics and representation losses. + + Args: + config: The DreamerV3Config to use. + fwd_out: The `forward_train` outputs of the DreamerV3RLModule. + + Returns: + Tuple consisting of a) dynamics loss: Trains the prior network, predicting + z^ prior states from h-states and b) representation loss: Trains posterior + network, predicting z posterior states from h-states and (encoded) + observations. + """ + + # Actual distribution over stochastic internal states (z) produced by the + # encoder. + z_posterior_probs_BxT = fwd_out["z_posterior_probs_BxT"] + z_posterior_distr_BxT = torch.distributions.Independent( + torch.distributions.OneHotCategorical(probs=z_posterior_probs_BxT), + reinterpreted_batch_ndims=1, + ) + + # Actual distribution over stochastic internal states (z) produced by the + # dynamics network. + z_prior_probs_BxT = fwd_out["z_prior_probs_BxT"] + z_prior_distr_BxT = torch.distributions.Independent( + torch.distributions.OneHotCategorical(probs=z_prior_probs_BxT), + reinterpreted_batch_ndims=1, + ) + + # Stop gradient for encoder's z-outputs: + sg_z_posterior_distr_BxT = torch.distributions.Independent( + torch.distributions.OneHotCategorical(probs=z_posterior_probs_BxT.detach()), + reinterpreted_batch_ndims=1, + ) + # Stop gradient for dynamics model's z-outputs: + sg_z_prior_distr_BxT = torch.distributions.Independent( + torch.distributions.OneHotCategorical(probs=z_prior_probs_BxT.detach()), + reinterpreted_batch_ndims=1, + ) + + # Implement free bits. According to [1]: + # "To avoid a degenerate solution where the dynamics are trivial to predict but + # contain not enough information about the inputs, we employ free bits by + # clipping the dynamics and representation losses below the value of + # 1 nat ≈ 1.44 bits. This disables them while they are already minimized well to + # focus the world model on its prediction loss" + L_dyn_BxT = torch.clamp( + torch.distributions.kl.kl_divergence( + sg_z_posterior_distr_BxT, z_prior_distr_BxT + ), + min=1.0, + ) + # Unfold time rank back in. + L_dyn_B_T = L_dyn_BxT.reshape( + config.batch_size_B_per_learner, config.batch_length_T + ) + + L_rep_BxT = torch.clamp( + torch.distributions.kl.kl_divergence( + z_posterior_distr_BxT, sg_z_prior_distr_BxT + ), + min=1.0, + ) + # Unfold time rank back in. + L_rep_B_T = L_rep_BxT.reshape( + config.batch_size_B_per_learner, config.batch_length_T + ) + + return L_dyn_B_T, L_rep_B_T + + def _compute_actor_loss( + self, + *, + module_id: ModuleID, + config: DreamerV3Config, + dream_data: Dict[str, TensorType], + value_targets_t0_to_Hm1_BxT: TensorType, + ) -> TensorType: + """Helper method computing the actor's loss terms. + + Args: + module_id: The module_id for which to compute the actor loss. + config: The DreamerV3Config to use. + dream_data: The data generated by dreaming for H steps (horizon) starting + from any BxT state (sampled from the buffer for the train batch). + value_targets_t0_to_Hm1_BxT: The computed value function targets of the + shape (t0 to H-1, BxT). + + Returns: + The total actor loss tensor. + """ + actor = self.module[module_id].unwrapped().actor + + # Note: `scaled_value_targets_t0_to_Hm1_B` are NOT stop_gradient'd yet. + scaled_value_targets_t0_to_Hm1_B = self._compute_scaled_value_targets( + module_id=module_id, + config=config, + value_targets_t0_to_Hm1_BxT=value_targets_t0_to_Hm1_BxT, + value_predictions_t0_to_Hm1_BxT=dream_data["values_dreamed_t0_to_H_BxT"][ + :-1 + ], + ) + + # Actions actually taken in the dream. + actions_dreamed = dream_data["actions_dreamed_t0_to_H_BxT"][:-1].detach() + actions_dreamed_dist_params_t0_to_Hm1_B = dream_data[ + "actions_dreamed_dist_params_t0_to_H_BxT" + ][:-1] + + dist_t0_to_Hm1_B = actor.get_action_dist_object( + actions_dreamed_dist_params_t0_to_Hm1_B + ) + + # Compute log(p)s of all possible actions in the dream. + if isinstance( + self.module[module_id].unwrapped().actor.action_space, gym.spaces.Discrete + ): + # Note that when we create the Categorical action distributions, we compute + # unimix probs, then math.log these and provide these log(p) as "logits" to + # the Categorical. So here, we'll continue to work with log(p)s (not + # really "logits")! + logp_actions_t0_to_Hm1_B = actions_dreamed_dist_params_t0_to_Hm1_B + + # Log probs of actions actually taken in the dream. + logp_actions_dreamed_t0_to_Hm1_B = torch.sum( + actions_dreamed * logp_actions_t0_to_Hm1_B, + dim=-1, + ) + # First term of loss function. [1] eq. 11. + logp_loss_H_B = ( + logp_actions_dreamed_t0_to_Hm1_B + * scaled_value_targets_t0_to_Hm1_B.detach() + ) + # Box space. + else: + logp_actions_dreamed_t0_to_Hm1_B = dist_t0_to_Hm1_B.log_prob( + actions_dreamed + ) + # First term of loss function. [1] eq. 11. + logp_loss_H_B = scaled_value_targets_t0_to_Hm1_B + + assert logp_loss_H_B.ndim == 2 + + # Add entropy loss term (second term [1] eq. 11). + entropy_H_B = dist_t0_to_Hm1_B.entropy() + assert entropy_H_B.ndim == 2 + entropy = torch.mean(entropy_H_B) + + L_actor_reinforce_term_H_B = -logp_loss_H_B + L_actor_action_entropy_term_H_B = -config.entropy_scale * entropy_H_B + + L_actor_H_B = L_actor_reinforce_term_H_B + L_actor_action_entropy_term_H_B + # Mask out everything that goes beyond a predicted continue=False boundary. + L_actor_H_B *= dream_data["dream_loss_weights_t0_to_H_BxT"][:-1].detach() + L_actor = torch.mean(L_actor_H_B) + + # Log important actor loss stats. + self.metrics.log_dict( + { + "ACTOR_L_total": L_actor, + "ACTOR_value_targets_pct95_ema": actor.ema_value_target_pct95, + "ACTOR_value_targets_pct5_ema": actor.ema_value_target_pct5, + "ACTOR_action_entropy": entropy, + # Individual loss terms. + "ACTOR_L_neglogp_reinforce_term": torch.mean( + L_actor_reinforce_term_H_B + ), + "ACTOR_L_neg_entropy_term": torch.mean(L_actor_action_entropy_term_H_B), + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + if config.report_individual_batch_item_stats: + self.metrics.log_dict( + { + "ACTOR_L_total_H_BxT": L_actor_H_B, + "ACTOR_logp_actions_dreamed_H_BxT": ( + logp_actions_dreamed_t0_to_Hm1_B + ), + "ACTOR_scaled_value_targets_H_BxT": ( + scaled_value_targets_t0_to_Hm1_B + ), + "ACTOR_action_entropy_H_BxT": entropy_H_B, + # Individual loss terms. + "ACTOR_L_neglogp_reinforce_term_H_BxT": L_actor_reinforce_term_H_B, + "ACTOR_L_neg_entropy_term_H_BxT": L_actor_action_entropy_term_H_B, + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + + return L_actor + + def _compute_critic_loss( + self, + *, + module_id: ModuleID, + config: DreamerV3Config, + dream_data: Dict[str, TensorType], + value_targets_t0_to_Hm1_BxT: TensorType, + ) -> TensorType: + """Helper method computing the critic's loss terms. + + Args: + module_id: The ModuleID for which to compute the critic loss. + config: The DreamerV3Config to use. + dream_data: The data generated by dreaming for H steps (horizon) starting + from any BxT state (sampled from the buffer for the train batch). + value_targets_t0_to_Hm1_BxT: The computed value function targets of the + shape (t0 to H-1, BxT). + + Returns: + The total critic loss tensor. + """ + # B=BxT + H, B = dream_data["rewards_dreamed_t0_to_H_BxT"].shape[:2] + Hm1 = H - 1 + + # Note that value targets are NOT symlog'd and go from t0 to H-1, not H, like + # all the other dream data. + + # From here on: B=BxT + value_targets_t0_to_Hm1_B = value_targets_t0_to_Hm1_BxT.detach() + value_symlog_targets_t0_to_Hm1_B = symlog(value_targets_t0_to_Hm1_B) + # Fold time rank (for two_hot'ing). + value_symlog_targets_HxB = value_symlog_targets_t0_to_Hm1_B.view( + -1, + ) + value_symlog_targets_two_hot_HxB = two_hot( + value_symlog_targets_HxB, device=self._device + ) + # Unfold time rank. + value_symlog_targets_two_hot_t0_to_Hm1_B = ( + value_symlog_targets_two_hot_HxB.view( + [Hm1, B, value_symlog_targets_two_hot_HxB.shape[-1]] + ) + ) + + # Get (B x T x probs) tensor from return distributions. + # Use the value function outputs that don't graph-trace back through the + # world model. The other corresponding value function outputs + # which do trace back through the world model are only used for cont. actions + # for the actor loss (to compute the scaled value targets). + value_symlog_logits_HxB = dream_data[ + "values_symlog_dreamed_logits_t0_to_HxBxT_wm_detached" + ] + # Unfold time rank and cut last time index to match value targets. + value_symlog_logits_t0_to_Hm1_B = value_symlog_logits_HxB.view( + [H, B, value_symlog_logits_HxB.shape[-1]] + )[:-1] + + values_log_pred_Hm1_B = value_symlog_logits_t0_to_Hm1_B - torch.logsumexp( + value_symlog_logits_t0_to_Hm1_B, dim=-1, keepdim=True + ) + # Multiply with two-hot targets and neg. + value_loss_two_hot_H_B = -torch.sum( + values_log_pred_Hm1_B * value_symlog_targets_two_hot_t0_to_Hm1_B, dim=-1 + ) + + # Compute EMA regularization loss. + # Expected values (dreamed) from the EMA (slow critic) net. + value_symlog_ema_t0_to_Hm1_B = dream_data[ + "v_symlog_dreamed_ema_t0_to_H_BxT" + ].detach()[:-1] + # Fold time rank (for two_hot'ing). + value_symlog_ema_HxB = value_symlog_ema_t0_to_Hm1_B.view( + -1, + ) + value_symlog_ema_two_hot_HxB = two_hot( + value_symlog_ema_HxB, device=self._device + ) + # Unfold time rank. + value_symlog_ema_two_hot_t0_to_Hm1_B = value_symlog_ema_two_hot_HxB.view( + [Hm1, B, value_symlog_ema_two_hot_HxB.shape[-1]] + ) + + # Compute ema regularizer loss. + # In the paper, it is not described how exactly to form this regularizer term + # and how to weigh it. + # So we follow Danijar's repo here: + # `reg = -dist.log_prob(sg(self.slow(traj).mean()))` + # with a weight of 1.0, where dist is the bucket'ized distribution output by the + # fast critic. sg=stop gradient; mean() -> use the expected EMA values. + # Multiply with two-hot targets and neg. + ema_regularization_loss_H_B = -torch.sum( + values_log_pred_Hm1_B * value_symlog_ema_two_hot_t0_to_Hm1_B, dim=-1 + ) + + L_critic_H_B = value_loss_two_hot_H_B + ema_regularization_loss_H_B + + # Mask out everything that goes beyond a predicted continue=False boundary. + L_critic_H_B *= dream_data["dream_loss_weights_t0_to_H_BxT"].detach()[:-1] + + # Reduce over both H- (time) axis and B-axis (mean). + L_critic = L_critic_H_B.mean() + + # Log important critic loss stats. + self.metrics.log_dict( + { + "CRITIC_L_total": L_critic, + "CRITIC_L_neg_logp_of_value_targets": torch.mean( + value_loss_two_hot_H_B + ), + "CRITIC_L_slow_critic_regularization": torch.mean( + ema_regularization_loss_H_B + ), + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + if config.report_individual_batch_item_stats: + # Log important critic loss stats. + self.metrics.log_dict( + { + # Symlog'd value targets. Critic learns to predict symlog'd values. + "VALUE_TARGETS_symlog_H_BxT": value_symlog_targets_t0_to_Hm1_B, + # Critic loss terms. + "CRITIC_L_total_H_BxT": L_critic_H_B, + "CRITIC_L_neg_logp_of_value_targets_H_BxT": value_loss_two_hot_H_B, + "CRITIC_L_slow_critic_regularization_H_BxT": ( + ema_regularization_loss_H_B + ), + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + + return L_critic + + def _compute_value_targets( + self, + *, + config: DreamerV3Config, + rewards_t0_to_H_BxT: torch.Tensor, + intrinsic_rewards_t1_to_H_BxT: torch.Tensor, + continues_t0_to_H_BxT: torch.Tensor, + value_predictions_t0_to_H_BxT: torch.Tensor, + ) -> torch.Tensor: + """Helper method computing the value targets. + + All args are (H, BxT, ...) and in non-symlog'd (real) reward space. + Non-symlog is important b/c log(a+b) != log(a) + log(b). + See [1] eq. 8 and 10. + Thus, targets are always returned in real (non-symlog'd space). + They need to be re-symlog'd before computing the critic loss from them (b/c the + critic produces predictions in symlog space). + Note that the original B and T ranks together form the new batch dimension + (folded into BxT) and the new time rank is the dream horizon (hence: [H, BxT]). + + Variable names nomenclature: + `H`=1+horizon_H (start state + H steps dreamed), + `BxT`=batch_size * batch_length (meaning the original trajectory time rank has + been folded). + + Rewards, continues, and value predictions are all of shape [t0-H, BxT] + (time-major), whereas returned targets are [t0 to H-1, B] (last timestep missing + b/c the target value equals vf prediction in that location anyways. + + Args: + config: The DreamerV3Config to use. + rewards_t0_to_H_BxT: The reward predictor's predictions over the + dreamed trajectory t0 to H (and for the batch BxT). + intrinsic_rewards_t1_to_H_BxT: The predicted intrinsic rewards over the + dreamed trajectory t0 to H (and for the batch BxT). + continues_t0_to_H_BxT: The continue predictor's predictions over the + dreamed trajectory t0 to H (and for the batch BxT). + value_predictions_t0_to_H_BxT: The critic's value predictions over the + dreamed trajectory t0 to H (and for the batch BxT). + + Returns: + The value targets in the shape: [t0toH-1, BxT]. Note that the last step (H) + does not require a value target as it matches the critic's value prediction + anyways. + """ + # The first reward is irrelevant (not used for any VF target). + rewards_t1_to_H_BxT = rewards_t0_to_H_BxT[1:] + if intrinsic_rewards_t1_to_H_BxT is not None: + rewards_t1_to_H_BxT += intrinsic_rewards_t1_to_H_BxT + + # In all the following, when building value targets for t=1 to T=H, + # exclude rewards & continues for t=1 b/c we don't need r1 or c1. + # The target (R1) for V1 is built from r2, c2, and V2/R2. + discount = continues_t0_to_H_BxT[1:] * config.gamma # shape=[2-16, BxT] + Rs = [value_predictions_t0_to_H_BxT[-1]] # Rs indices=[16] + intermediates = ( + rewards_t1_to_H_BxT + + discount * (1 - config.gae_lambda) * value_predictions_t0_to_H_BxT[1:] + ) + # intermediates.shape=[2-16, BxT] + + # Loop through reversed timesteps (axis=1) from T+1 to t=2. + for t in reversed(range(discount.shape[0])): + Rs.append(intermediates[t] + discount[t] * config.gae_lambda * Rs[-1]) + + # Reverse time axis and cut the last entry (value estimate at very end + # cannot be learnt from as it's the same as the ... well ... value estimate). + targets_t0toHm1_BxT = torch.stack(list(reversed(Rs))[:-1], dim=0) + # targets.shape=[t0 to H-1,BxT] + + return targets_t0toHm1_BxT + + def _compute_scaled_value_targets( + self, + *, + module_id: ModuleID, + config: DreamerV3Config, + value_targets_t0_to_Hm1_BxT: torch.Tensor, + value_predictions_t0_to_Hm1_BxT: torch.Tensor, + ) -> torch.Tensor: + """Helper method computing the scaled value targets. + + Args: + module_id: The module_id to compute value targets for. + config: The DreamerV3Config to use. + value_targets_t0_to_Hm1_BxT: The value targets computed by + `self._compute_value_targets` in the shape of (t0 to H-1, BxT) + and of type float32. + value_predictions_t0_to_Hm1_BxT: The critic's value predictions over the + dreamed trajectories (w/o the last timestep). The shape of this + tensor is (t0 to H-1, BxT) and the type is float32. + + Returns: + The scaled value targets used by the actor for REINFORCE policy updates + (using scaled advantages). See [1] eq. 12 for more details. + """ + actor = self.module[module_id].unwrapped().actor + + value_targets_H_B = value_targets_t0_to_Hm1_BxT + value_predictions_H_B = value_predictions_t0_to_Hm1_BxT + + # Compute S: [1] eq. 12. + Per_R_5 = torch.quantile(value_targets_H_B, 0.05) + Per_R_95 = torch.quantile(value_targets_H_B, 0.95) + + # Update EMA values for 5 and 95 percentile, stored as actor network's + # parameters. + # 5 percentile + new_val_pct5 = torch.where( + torch.isnan(actor.ema_value_target_pct5), + # is NaN: Initial values: Just set. + Per_R_5, + # Later update (something already stored in EMA variable): Update EMA. + ( + config.return_normalization_decay * actor.ema_value_target_pct5 + + (1.0 - config.return_normalization_decay) * Per_R_5 + ), + ) + actor.ema_value_target_pct5.data = new_val_pct5 + # 95 percentile + new_val_pct95 = torch.where( + # is NaN: Initial values: Just set. + torch.isnan(actor.ema_value_target_pct95), + # Later update (something already stored in EMA variable): Update EMA. + Per_R_95, + ( + config.return_normalization_decay * actor.ema_value_target_pct95 + + (1.0 - config.return_normalization_decay) * Per_R_95 + ), + ) + actor.ema_value_target_pct95.data = new_val_pct95 + + # [1] eq. 11 (first term). + offset = actor.ema_value_target_pct5 + invscale = torch.clamp( + (actor.ema_value_target_pct95 - actor.ema_value_target_pct5), + min=1e-8, + ) + scaled_value_targets_H_B = (value_targets_H_B - offset) / invscale + scaled_value_predictions_H_B = (value_predictions_H_B - offset) / invscale + + # Return advantages. + return scaled_value_targets_H_B - scaled_value_predictions_H_B diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/dreamerv3_torch_rl_module.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/dreamerv3_torch_rl_module.py new file mode 100644 index 0000000000000000000000000000000000000000..ea7b31c5a1d6fcf50961f3f93b54756d5a813aa2 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/dreamerv3_torch_rl_module.py @@ -0,0 +1,78 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf + +[2] Mastering Atari with Discrete World Models - 2021 +D. Hafner, T. Lillicrap, M. Norouzi, J. Ba +https://arxiv.org/pdf/2010.02193.pdf +""" +from typing import Any, Dict + +import gymnasium as gym +import torch + +from ray.rllib.algorithms.dreamerv3.dreamerv3_rl_module import ( + ACTIONS_ONE_HOT, + DreamerV3RLModule, +) +from ray.rllib.core.columns import Columns +from ray.rllib.core.rl_module.rl_module import RLModule +from ray.rllib.core.rl_module.torch.torch_rl_module import TorchRLModule +from ray.rllib.utils.annotations import override + + +class DreamerV3TorchRLModule(TorchRLModule, DreamerV3RLModule): + """The torch-specific RLModule class for DreamerV3. + + Serves mainly as a thin-wrapper around the `DreamerModel` (a torch.nn.Module) class. + """ + + framework = "torch" + + @override(TorchRLModule) + def _forward_inference(self, batch: Dict[str, Any], **kwargs) -> Dict[str, Any]: + # Call the Dreamer-Model's forward_inference method and return a dict. + with torch.no_grad(): + actions, next_state = self.dreamer_model.forward_inference( + observations=batch[Columns.OBS], + previous_states=batch[Columns.STATE_IN], + is_first=batch["is_first"], + ) + return self._forward_inference_or_exploration_helper(batch, actions, next_state) + + @override(TorchRLModule) + def _forward_exploration(self, batch: Dict[str, Any], **kwargs) -> Dict[str, Any]: + # Call the Dreamer-Model's forward_exploration method and return a dict. + with torch.no_grad(): + actions, next_state = self.dreamer_model.forward_exploration( + observations=batch[Columns.OBS], + previous_states=batch[Columns.STATE_IN], + is_first=batch["is_first"], + ) + return self._forward_inference_or_exploration_helper(batch, actions, next_state) + + @override(RLModule) + def _forward_train(self, batch: Dict[str, Any], **kwargs): + # Call the Dreamer-Model's forward_train method and return its outputs as-is. + return self.dreamer_model.forward_train( + observations=batch[Columns.OBS], + actions=batch[Columns.ACTIONS], + is_first=batch["is_first"], + ) + + def _forward_inference_or_exploration_helper(self, batch, actions, next_state): + # Unfold time dimension. + shape = batch[Columns.OBS].shape + B, T = shape[0], shape[1] + actions = actions.view((B, T) + actions.shape[1:]) + + output = { + Columns.ACTIONS: actions, + ACTIONS_ONE_HOT: actions, + Columns.STATE_OUT: next_state, + } + # Undo one-hot actions? + if isinstance(self.action_space, gym.spaces.Discrete): + output[Columns.ACTIONS] = torch.argmax(actions, dim=-1) + return output diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08a624acb840056b31c4ad31f1bb5bd80e8e9c6a Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/actor_network.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/actor_network.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6491cb563e9fe2c041cdb1b8431c1a5222692843 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/actor_network.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/critic_network.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/critic_network.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ba8cb856b886d7c0cdbd51c886f122e9811f4e9 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/critic_network.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/dreamer_model.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/dreamer_model.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ba877d99ed55b7373cf2fde810e3e1aee9428ad Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/dreamer_model.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/world_model.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/world_model.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e355c7c81b33ac46725544a1e158a9e5cbeb6db Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/__pycache__/world_model.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/actor_network.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/actor_network.py new file mode 100644 index 0000000000000000000000000000000000000000..8dc90f4bdf9dacc4d81fa2dc2825277da85c3f3e --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/actor_network.py @@ -0,0 +1,178 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +import gymnasium as gym +import numpy as np + +from ray.rllib.algorithms.dreamerv3.torch.models.components.mlp import MLP +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +class ActorNetwork(nn.Module): + """The `actor` (policy net) of DreamerV3. + + Consists of a simple MLP for Discrete actions and two MLPs for cont. actions (mean + and stddev). + Also contains two scalar variables to keep track of the percentile-5 and + percentile-95 values of the computed value targets within a batch. This is used to + compute the "scaled value targets" for actor learning. These two variables decay + over time exponentially (see [1] for more details). + """ + + def __init__( + self, + *, + input_size: int, + model_size: str = "XS", + action_space: gym.Space, + ): + """Initializes an ActorNetwork instance. + + Args: + input_size: The input size of the actor network. + model_size: The "Model Size" used according to [1] Appendinx B. + Use None for manually setting the different network sizes. + action_space: The action space the our environment used. + """ + super().__init__() + + self.input_size = input_size + self.model_size = model_size + self.action_space = action_space + + # The EMA decay variables used for the [Percentile(R, 95%) - Percentile(R, 5%)] + # diff to scale value targets for the actor loss. + self.ema_value_target_pct5 = nn.Parameter( + torch.tensor(float("nan")), requires_grad=False + ) + self.ema_value_target_pct95 = nn.Parameter( + torch.tensor(float("nan")), requires_grad=False + ) + + # For discrete actions, use a single MLP that computes logits. + if isinstance(self.action_space, gym.spaces.Discrete): + self.mlp = MLP( + input_size=self.input_size, + model_size=self.model_size, + output_layer_size=self.action_space.n, + ) + # For cont. actions, use separate MLPs for Gaussian mean and stddev. + # TODO (sven): In the author's original code repo, this is NOT the case, + # inputs are pushed through a shared MLP, then only the two output linear + # layers are separate for std- and mean logits. + elif isinstance(action_space, gym.spaces.Box): + output_layer_size = np.prod(action_space.shape) + self.mlp = MLP( + input_size=self.input_size, + model_size=self.model_size, + output_layer_size=output_layer_size, + ) + self.std_mlp = MLP( + input_size=self.input_size, + model_size=self.model_size, + output_layer_size=output_layer_size, + ) + else: + raise ValueError(f"Invalid action space: {action_space}") + + def forward(self, h, z, return_distr_params=False): + """Performs a forward pass through this policy network. + + Args: + h: The deterministic hidden state of the sequence model. [B, dim(h)]. + z: The stochastic discrete representations of the original + observation input. [B, num_categoricals, num_classes]. + return_distr_params: Whether to return (as a second tuple item) the action + distribution parameter tensor created by the policy. + """ + # Flatten last two dims of z. + assert len(z.shape) == 3 + z_shape = z.shape + z = z.view(z_shape[0], -1) + assert len(z.shape) == 2 + out = torch.cat([h, z], dim=-1) + # Send h-cat-z through MLP. + action_logits = self.mlp(out) + + if isinstance(self.action_space, gym.spaces.Discrete): + action_probs = nn.functional.softmax(action_logits, dim=-1) + + # Add the unimix weighting (1% uniform) to the probs. + # See [1]: "Unimix categoricals: We parameterize the categorical + # distributions for the world model representations and dynamics, as well as + # for the actor network, as mixtures of 1% uniform and 99% neural network + # output to ensure a minimal amount of probability mass on every class and + # thus keep log probabilities and KL divergences well behaved." + action_probs = 0.99 * action_probs + 0.01 * (1.0 / self.action_space.n) + + # Danijar's code does: distr = [Distr class](logits=torch.log(probs)). + # Not sure why we don't directly use the already available probs instead. + action_logits = torch.log(action_probs) + + # Distribution parameters are the log(probs) directly. + distr_params = action_logits + distr = self.get_action_dist_object(distr_params) + + action = distr.sample().float().detach() + ( + action_probs - action_probs.detach() + ) + + elif isinstance(self.action_space, gym.spaces.Box): + # Send h-cat-z through MLP to compute stddev logits for Normal dist + std_logits = self.std_mlp(out) + # minstd, maxstd taken from [1] from configs.yaml + minstd = 0.1 + maxstd = 1.0 + + # Distribution parameters are the squashed std_logits and the tanh'd + # mean logits. + # squash std_logits from (-inf, inf) to (minstd, maxstd) + std_logits = (maxstd - minstd) * torch.sigmoid(std_logits + 2.0) + minstd + mean_logits = torch.tanh(action_logits) + + distr_params = torch.cat([mean_logits, std_logits], dim=-1) + distr = self.get_action_dist_object(distr_params) + + action = distr.rsample() + + if return_distr_params: + return action, distr_params + return action + + def get_action_dist_object(self, action_dist_params_T_B): + """Helper method to create an action distribution object from (T, B, ..) params. + + Args: + action_dist_params_T_B: The time-major action distribution parameters. + This could be simply the logits (discrete) or a to-be-split-in-2 + tensor for mean and stddev (continuous). + + Returns: + The torch action distribution object, from which one can sample, compute + log probs, entropy, etc.. + """ + if isinstance(self.action_space, gym.spaces.Discrete): + # Create the distribution object using the unimix'd logits. + distr = torch.distributions.OneHotCategorical(logits=action_dist_params_T_B) + + elif isinstance(self.action_space, gym.spaces.Box): + # Compute Normal distribution from action_logits and std_logits + loc, scale = torch.split( + action_dist_params_T_B, + action_dist_params_T_B.shape[-1] // 2, + dim=-1, + ) + distr = torch.distributions.Normal(loc=loc, scale=scale) + + # If action_space is a box with multiple dims, make individual dims + # independent. + distr = torch.distributions.Independent(distr, len(self.action_space.shape)) + + else: + raise ValueError(f"Action space {self.action_space} not supported!") + + return distr diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..590f20e221c1ca19f9002dde1b490fa1865cb043 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__init__.py @@ -0,0 +1,37 @@ +import numpy as np + +from ray.rllib.utils import force_list +from ray.rllib.utils.framework import try_import_torch + +torch, _ = try_import_torch() + + +def dreamerv3_normal_initializer(parameters): + """From Danijar Hafner's DreamerV3 JAX repo. + + Used on any layer whenever the config for that layer has `winit="normal"`. + + Note: Not identical with Glorot normal. Differs in the std computation + glorot_std = sqrt(2/(fanin+fanout)) + this_std = sqrt(1/AVG(fanin, fanout)) / [somemagicnumber=0.879...] + """ + for param in force_list(parameters): + if param.dim() > 1: + fanin, fanout = _fans(param.shape) + scale = 1.0 / np.mean([fanin, fanout]) + std = np.sqrt(scale) / 0.87962566103423978 + with torch.no_grad(): + param.normal_(0, std) + param.clamp_(-2, 2) + + +def _fans(shape): + if len(shape) == 0: + return 1, 1 + elif len(shape) == 1: + return shape[0], shape[0] + elif len(shape) == 2: + return shape + else: + space = int(np.prod(shape[:-2])) + return shape[-2] * space, shape[-1] * space diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..070a8c1232471b9f4b1727b90359c2829e5f66e9 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/cnn_atari.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/cnn_atari.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9875ae4b88b22271316d28dc8ab6a3d492f3e19 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/cnn_atari.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/continue_predictor.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/continue_predictor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0159a66ec3d5bd4ff3787ce9e4127a287f5c50e9 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/continue_predictor.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/conv_transpose_atari.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/conv_transpose_atari.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f12144f00dc1c31c8966fc0a35a010cd61c4961 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/conv_transpose_atari.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/dynamics_predictor.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/dynamics_predictor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6a1e65c68c1c072d4244ac1471d4ff0a09a8b38 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/dynamics_predictor.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/mlp.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/mlp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6ae79c82068e903386d538842ad8720d8ddae3c Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/mlp.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/representation_layer.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/representation_layer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1268ef53a4472febbcdf1e10a1a52a4ad2eceb3f Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/representation_layer.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/reward_predictor.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/reward_predictor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc9192c04c30dd565973668bfb653475ad9a9b9e Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/reward_predictor.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/reward_predictor_layer.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/reward_predictor_layer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5150a50711e60b6fd16523e27aa3065cdbf2f8d6 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/reward_predictor_layer.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/sequence_model.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/sequence_model.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9001b0c58df099d2bacda492054f23385731c3cf Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/sequence_model.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/vector_decoder.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/vector_decoder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9878a7c8b64cd4d37ff115f3b2f840725b1ac491 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/__pycache__/vector_decoder.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/cnn_atari.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/cnn_atari.py new file mode 100644 index 0000000000000000000000000000000000000000..d75abc9086dd09ff6f95935a604eb0cc93e28d06 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/cnn_atari.py @@ -0,0 +1,70 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +from typing import Optional + +from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + dreamerv3_normal_initializer, +) +from ray.rllib.algorithms.dreamerv3.utils import get_cnn_multiplier +from ray.rllib.core.models.base import ENCODER_OUT +from ray.rllib.core.models.configs import CNNEncoderConfig +from ray.rllib.policy.sample_batch import SampleBatch +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +class CNNAtari(nn.Module): + """An image encoder mapping 64x64 RGB images via 4 CNN layers into a 1D space.""" + + def __init__( + self, + *, + model_size: str = "XS", + cnn_multiplier: Optional[int] = None, + gray_scaled: bool, + ): + """Initializes a CNNAtari instance. + + Args: + model_size: The "Model Size" used according to [1] Appendix B. + Use None for manually setting the `cnn_multiplier`. + cnn_multiplier: Optional override for the additional factor used to multiply + the number of filters with each CNN layer. Starting with + 1 * `cnn_multiplier` filters in the first CNN layer, the number of + filters then increases via `2*cnn_multiplier`, `4*cnn_multiplier`, till + `8*cnn_multiplier`. + gray_scaled: Whether the input is a gray-scaled image (1 color channel) or + not (3 RGB channels). + """ + super().__init__() + + cnn_multiplier = get_cnn_multiplier(model_size, override=cnn_multiplier) + + config = CNNEncoderConfig( + input_dims=[64, 64, 1 if gray_scaled else 3], + cnn_filter_specifiers=[ + [1 * cnn_multiplier, 4, 2], + [2 * cnn_multiplier, 4, 2], + [4 * cnn_multiplier, 4, 2], + [8 * cnn_multiplier, 4, 2], + ], + cnn_use_bias=False, + cnn_use_layernorm=True, + cnn_activation="silu", + cnn_kernel_initializer=dreamerv3_normal_initializer, + flatten_at_end=True, + ) + self.cnn_stack = config.build(framework="torch") + self.output_size = config.output_dims + + def forward(self, inputs): + """Performs a forward pass through the CNN Atari encoder. + + Args: + inputs: The image inputs of shape (B, 64, 64, 3). + """ + return self.cnn_stack({SampleBatch.OBS: inputs})[ENCODER_OUT] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/continue_predictor.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/continue_predictor.py new file mode 100644 index 0000000000000000000000000000000000000000..1bc3c64cfb75ae316d74a6e61208e1de454fd7ef --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/continue_predictor.py @@ -0,0 +1,62 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +from ray.rllib.algorithms.dreamerv3.torch.models.components.mlp import MLP +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +class ContinuePredictor(nn.Module): + """The world-model network sub-component used to predict the `continue` flags . + + Predicted continue flags are used to produce "dream data" to learn the policy in. + + The continue flags are predicted via a linear output used to parameterize a + Bernoulli distribution, from which simply the mode is used (no stochastic + sampling!). In other words, if the sigmoid of the output of the linear layer is + >0.5, we predict a continuation of the episode, otherwise we predict an episode + terminal. + """ + + def __init__(self, *, input_size: int, model_size: str = "XS"): + """Initializes a ContinuePredictor instance. + + Args: + input_size: The input size of the continue predictor. + model_size: The "Model Size" used according to [1] Appendinx B. + Determines the exact size of the underlying MLP. + """ + super().__init__() + + self.mlp = MLP( + input_size=input_size, + model_size=model_size, + output_layer_size=1, + ) + + def forward(self, h, z, return_distribution=False): + """Performs a forward pass through the continue predictor. + + Args: + h: The deterministic hidden state of the sequence model. [B, dim(h)]. + z: The stochastic discrete representations of the original + observation input. [B, num_categoricals, num_classes]. + return_distribution: Whether to return (as a second tuple item) the + Bernoulli distribution object created by the underlying MLP. + """ + z_shape = z.size() + z = z.view(z_shape[0], -1) + + out = torch.cat([h, z], dim=-1) + out = self.mlp(out) + logits = out.squeeze(dim=-1) + bernoulli = torch.distributions.Bernoulli(logits=logits) + # Use the mode of the Bernoulli distribution (greedy, deterministic "sample"). + continue_ = bernoulli.probs > 0.5 + + if return_distribution: + return continue_, bernoulli + return continue_ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/conv_transpose_atari.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/conv_transpose_atari.py new file mode 100644 index 0000000000000000000000000000000000000000..30597f52aea092145c3c815088007b63f6e7fa38 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/conv_transpose_atari.py @@ -0,0 +1,95 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +from typing import Optional + +from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + dreamerv3_normal_initializer, +) +from ray.rllib.algorithms.dreamerv3.utils import get_cnn_multiplier +from ray.rllib.core.models.configs import CNNTransposeHeadConfig +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +class ConvTransposeAtari(nn.Module): + """A Conv2DTranspose decoder to generate Atari images from a latent space. + + Wraps an initial single linear layer with a stack of 4 Conv2DTranspose layers (with + layer normalization) and a diag Gaussian, from which we then sample the final image. + """ + + def __init__( + self, + *, + input_size: int, + model_size: str = "XS", + cnn_multiplier: Optional[int] = None, + gray_scaled: bool, + ): + """Initializes a ConvTransposeAtari instance. + + Args: + input_size: The input size of the ConvTransposeAtari network. + model_size: The "Model Size" used according to [1] Appendinx B. + Use None for manually setting the `cnn_multiplier`. + cnn_multiplier: Optional override for the additional factor used to multiply + the number of filters with each CNN transpose layer. Starting with + 8 * `cnn_multiplier` filters in the first CNN transpose layer, the + number of filters then decreases via `4*cnn_multiplier`, + `2*cnn_multiplier`, till `1*cnn_multiplier`. + gray_scaled: Whether the last Conv2DTranspose layer's output has only 1 + color channel (gray_scaled=True) or 3 RGB channels (gray_scaled=False). + """ + super().__init__() + + cnn_multiplier = get_cnn_multiplier(model_size, override=cnn_multiplier) + self.gray_scaled = gray_scaled + config = CNNTransposeHeadConfig( + input_dims=[input_size], + initial_image_dims=(4, 4, 8 * cnn_multiplier), + initial_dense_weights_initializer=dreamerv3_normal_initializer, + cnn_transpose_filter_specifiers=[ + [4 * cnn_multiplier, 4, 2], + [2 * cnn_multiplier, 4, 2], + [1 * cnn_multiplier, 4, 2], + [1 if self.gray_scaled else 3, 4, 2], + ], + cnn_transpose_use_bias=False, + cnn_transpose_use_layernorm=True, + cnn_transpose_activation="silu", + cnn_transpose_kernel_initializer=dreamerv3_normal_initializer, + ) + # Make sure the output dims match Atari. + # assert config.output_dims == (64, 64, 1 if self.gray_scaled else 3) + + self._transpose_2d_head = config.build(framework="torch") + + def forward(self, h, z): + """Performs a forward pass through the Conv2D transpose decoder. + + Args: + h: The deterministic hidden state of the sequence model. + z: The sequence of stochastic discrete representations of the original + observation input. Note: `z` is not used for the dynamics predictor + model (which predicts z from h). + """ + z_shape = z.size() + z = z.view(z_shape[0], -1) + + input_ = torch.cat([h, z], dim=-1) + + out = self._transpose_2d_head(input_) + + # Interpret output as means of a diag-Gaussian with std=1.0: + # From [2]: + # "Distributions: The image predictor outputs the mean of a diagonal Gaussian + # likelihood with unit variance, ..." + + # Reshape `out` for the diagonal multi-variate Gaussian (each pixel is its own + # independent (b/c diagonal co-variance matrix) variable). + loc = torch.reshape(out, (z_shape[0], -1)) + return loc diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/dynamics_predictor.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/dynamics_predictor.py new file mode 100644 index 0000000000000000000000000000000000000000..64df56079bdad11b2356434e4159f9d74af4f3ec --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/dynamics_predictor.py @@ -0,0 +1,74 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +from typing import Optional + +from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + representation_layer, +) +from ray.rllib.algorithms.dreamerv3.torch.models.components.mlp import MLP +from ray.rllib.algorithms.dreamerv3.utils import get_dense_hidden_units +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +class DynamicsPredictor(nn.Module): + """The dynamics (or "prior") network described in [1], producing prior z-states. + + The dynamics net is used to: + - compute the initial z-state (from the tanh'd initial h-state variable) at the + beginning of a sequence. + - compute prior-z-states during dream data generation. Note that during dreaming, + no actual observations are available and thus no posterior z-states can be computed. + """ + + def __init__( + self, + *, + input_size: int, + model_size: str = "XS", + num_categoricals: Optional[int] = None, + num_classes_per_categorical: Optional[int] = None, + ): + """Initializes a DynamicsPredictor instance. + + Args: + input_size: The input size of the dynamics predictor. + model_size: The "Model Size" used according to [1] Appendinx B. + Use None for manually setting the different parameters. + num_categoricals: Overrides the number of categoricals used in the z-states. + In [1], 32 is used for any model size. + num_classes_per_categorical: Overrides the number of classes within each + categorical used for the z-states. In [1], 32 is used for any model + dimension. + """ + super().__init__() + + self.mlp = MLP( + input_size=input_size, + num_dense_layers=1, + model_size=model_size, + output_layer_size=None, + ) + representation_layer_input_size = get_dense_hidden_units(model_size) + self.representation_layer = representation_layer.RepresentationLayer( + input_size=representation_layer_input_size, + model_size=model_size, + num_categoricals=num_categoricals, + num_classes_per_categorical=num_classes_per_categorical, + ) + + def forward(self, h, return_z_probs=False): + """Performs a forward pass through the dynamics (or "prior") network. + + Args: + h: The deterministic hidden state of the sequence model. + return_z_probs: Whether to return the probabilities for the categorical + distribution (in the shape of [B, num_categoricals, num_classes]) + as a second return value. + """ + out = self.mlp(h) + return self.representation_layer(out, return_z_probs=return_z_probs) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/mlp.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/mlp.py new file mode 100644 index 0000000000000000000000000000000000000000..4e8ed5333684c9ce0bed4a7b29b858b9a4b04ff4 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/mlp.py @@ -0,0 +1,93 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf + +[2] Mastering Atari with Discrete World Models - 2021 +D. Hafner, T. Lillicrap, M. Norouzi, J. Ba +https://arxiv.org/pdf/2010.02193.pdf +""" +from typing import Optional + +from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + dreamerv3_normal_initializer, +) +from ray.rllib.algorithms.dreamerv3.utils import ( + get_dense_hidden_units, + get_num_dense_layers, +) +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +class MLP(nn.Module): + """An MLP primitive used by several DreamerV3 components and described in [1] Fig 5. + + MLP=multi-layer perceptron. + + See Appendix B in [1] for the MLP sizes depending on the given `model_size`. + """ + + def __init__( + self, + *, + input_size: int, + model_size: str = "XS", + num_dense_layers: Optional[int] = None, + dense_hidden_units: Optional[int] = None, + output_layer_size=None, + ): + """Initializes an MLP instance. + + Args: + input_size: The input size of the MLP. + model_size: The "Model Size" used according to [1] Appendinx B. + Use None for manually setting the different network sizes. + num_dense_layers: The number of hidden layers in the MLP. If None, + will use `model_size` and appendix B to figure out this value. + dense_hidden_units: The number of nodes in each hidden layer. If None, + will use `model_size` and appendix B to figure out this value. + output_layer_size: The size of an optional linear (no activation) output + layer. If None, no output layer will be added on top of the MLP dense + stack. + """ + super().__init__() + + self.output_size = None + + num_dense_layers = get_num_dense_layers(model_size, override=num_dense_layers) + dense_hidden_units = get_dense_hidden_units( + model_size, override=dense_hidden_units + ) + + layers = [] + for _ in range(num_dense_layers): + # In this order: layer, normalization, activation. + linear = nn.Linear(input_size, dense_hidden_units, bias=False) + # Use same initializers as the Author in their JAX repo. + dreamerv3_normal_initializer(linear.weight) + layers.append(linear) + layers.append(nn.LayerNorm(dense_hidden_units, eps=0.001)) + layers.append(nn.SiLU()) + input_size = dense_hidden_units + self.output_size = (dense_hidden_units,) + + self.output_layer = None + if output_layer_size: + linear = nn.Linear(input_size, output_layer_size, bias=True) + # Use same initializers as the Author in their JAX repo. + dreamerv3_normal_initializer(linear.weight) + nn.init.zeros_(linear.bias) + layers.append(linear) + self.output_size = (output_layer_size,) + + self._net = nn.Sequential(*layers) + + def forward(self, input_): + """Performs a forward pass through this MLP. + + Args: + input_: The input tensor for the MLP dense stack. + """ + return self._net(input_) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/representation_layer.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/representation_layer.py new file mode 100644 index 0000000000000000000000000000000000000000..caf58c2b657901a9e76e8bbdebe2e30527a17fde --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/representation_layer.py @@ -0,0 +1,133 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf + +[2] Mastering Atari with Discrete World Models - 2021 +D. Hafner, T. Lillicrap, M. Norouzi, J. Ba +https://arxiv.org/pdf/2010.02193.pdf +""" +from typing import Optional + +from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + dreamerv3_normal_initializer, +) +from ray.rllib.algorithms.dreamerv3.utils import ( + get_num_z_categoricals, + get_num_z_classes, +) +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() +if torch: + F = nn.functional + + +class RepresentationLayer(nn.Module): + """A representation (z-state) generating layer. + + The value for z is the result of sampling from a categorical distribution with + shape B x `num_classes`. So a computed z-state consists of `num_categoricals` + one-hot vectors, each of size `num_classes_per_categorical`. + """ + + def __init__( + self, + *, + input_size: int, + model_size: str = "XS", + num_categoricals: Optional[int] = None, + num_classes_per_categorical: Optional[int] = None, + ): + """Initializes a RepresentationLayer instance. + + Args: + input_size: The input size of the representation layer. + model_size: The "Model Size" used according to [1] Appendinx B. + Use None for manually setting the different parameters. + num_categoricals: Overrides the number of categoricals used in the z-states. + In [1], 32 is used for any model size. + num_classes_per_categorical: Overrides the number of classes within each + categorical used for the z-states. In [1], 32 is used for any model + dimension. + """ + self.num_categoricals = get_num_z_categoricals( + model_size, override=num_categoricals + ) + self.num_classes_per_categorical = get_num_z_classes( + model_size, override=num_classes_per_categorical + ) + + super().__init__() + + self.z_generating_layer = nn.Linear( + input_size, + self.num_categoricals * self.num_classes_per_categorical, + bias=True, + ) + # Use same initializers as the Author in their JAX repo. + dreamerv3_normal_initializer(self.z_generating_layer.weight) + + def forward(self, inputs, return_z_probs=False): + """Produces a discrete, differentiable z-sample from some 1D input tensor. + + Pushes the input_ tensor through our dense layer, which outputs + 32(B=num categoricals)*32(c=num classes) logits. Logits are used to: + + 1) sample stochastically + 2) compute probs (via softmax) + 3) make sure the sampling step is differentiable (see [2] Algorithm 1): + sample=one_hot(draw(logits)) + probs=softmax(logits) + sample=sample + probs - stop_grad(probs) + -> Now sample has the gradients of the probs. + + Args: + inputs: The input to our z-generating layer. This might be a) the combined + (concatenated) outputs of the (image?) encoder + the last hidden + deterministic state, or b) the output of the dynamics predictor MLP + network. + return_z_probs: Whether to return the probabilities for the categorical + distribution (in the shape of [B, num_categoricals, num_classes]) + as a second return value. + """ + # Compute the logits (no activation) for our `num_categoricals` Categorical + # distributions (with `num_classes_per_categorical` classes each). + logits = self.z_generating_layer(inputs) + # Reshape the logits to [B, num_categoricals, num_classes] + logits = logits.reshape( + -1, self.num_categoricals, self.num_classes_per_categorical + ) + # Compute the probs (based on logits) via softmax. + probs = F.softmax(logits, dim=-1) + # Add the unimix weighting (1% uniform) to the probs. + # See [1]: "Unimix categoricals: We parameterize the categorical distributions + # for the world model representations and dynamics, as well as for the actor + # network, as mixtures of 1% uniform and 99% neural network output to ensure + # a minimal amount of probability mass on every class and thus keep log + # probabilities and KL divergences well behaved." + probs = 0.99 * probs + 0.01 * (1.0 / self.num_classes_per_categorical) + + # Danijar's code does: distr = [Distr class](logits=torch.log(probs)). + # Not sure why we don't directly use the already available probs instead. + logits = torch.log(probs) + + # Create the distribution object using the unimix'd logits. + distribution = torch.distributions.Independent( + torch.distributions.OneHotCategorical(logits=logits), + reinterpreted_batch_ndims=1, + ) + + # Draw a one-hot sample (B, num_categoricals, num_classes). + sample = distribution.sample() + # Make sure we can take gradients "straight-through" the sampling step + # by adding the probs and subtracting the sg(probs). Note that `sample` + # does not have any gradients as it's the result of a Categorical sample step, + # which is non-differentiable (other than say a Gaussian sample step). + # [1] "The representations are sampled from a vector of softmax distributions + # and we take straight-through gradients through the sampling step." + # [2] Algorithm 1. + differentiable_sample = sample.detach() + probs - probs.detach() + if return_z_probs: + return differentiable_sample, probs + return differentiable_sample diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/reward_predictor.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/reward_predictor.py new file mode 100644 index 0000000000000000000000000000000000000000..98f5920f5890af0f8f1772a9d9b913c4b1c4f67b --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/reward_predictor.py @@ -0,0 +1,86 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + reward_predictor_layer, +) +from ray.rllib.algorithms.dreamerv3.torch.models.components.mlp import MLP +from ray.rllib.algorithms.dreamerv3.utils import get_dense_hidden_units +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +class RewardPredictor(nn.Module): + """Wrapper of MLP and RewardPredictorLayer to predict rewards for the world model. + + Predicted rewards are used to produce "dream data" to learn the policy in. + """ + + def __init__( + self, + *, + input_size: int, + model_size: str = "XS", + num_buckets: int = 255, + lower_bound: float = -20.0, + upper_bound: float = 20.0, + ): + """Initializes a RewardPredictor instance. + + Args: + input_size: The input size of the reward predictor. + model_size: The "Model Size" used according to [1] Appendinx B. + Determines the exact size of the underlying MLP. + num_buckets: The number of buckets to create. Note that the number of + possible symlog'd outcomes from the used distribution is + `num_buckets` + 1: + lower_bound --bucket-- o[1] --bucket-- o[2] ... --bucket-- upper_bound + o=outcomes + lower_bound=o[0] + upper_bound=o[num_buckets] + lower_bound: The symlog'd lower bound for a possible reward value. + Note that a value of -20.0 here already allows individual (actual env) + rewards to be as low as -400M. Buckets will be created between + `lower_bound` and `upper_bound`. + upper_bound: The symlog'd upper bound for a possible reward value. + Note that a value of +20.0 here already allows individual (actual env) + rewards to be as high as 400M. Buckets will be created between + `lower_bound` and `upper_bound`. + """ + super().__init__() + + self.mlp = MLP( + input_size=input_size, + model_size=model_size, + output_layer_size=None, + ) + reward_predictor_input_size = get_dense_hidden_units(model_size) + self.reward_layer = reward_predictor_layer.RewardPredictorLayer( + input_size=reward_predictor_input_size, + num_buckets=num_buckets, + lower_bound=lower_bound, + upper_bound=upper_bound, + ) + + def forward(self, h, z, return_logits=False): + """Computes the expected reward using N equal sized buckets of possible values. + + Args: + h: The deterministic hidden state of the sequence model. [B, dim(h)]. + z: The stochastic discrete representations of the original + observation input. [B, num_categoricals, num_classes]. + return_logits: Whether to return the logits over the reward buckets + as a second return value (besides the expected reward). + """ + # Flatten last two dims of z. + z_shape = z.shape + z = z.view(z_shape[0], -1) + out = torch.cat([h, z], dim=-1) + # Send h-cat-z through MLP. + out = self.mlp(out) + # Return a) mean reward OR b) a tuple: (mean reward, logits over the reward + # buckets). + return self.reward_layer(out, return_logits=return_logits) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/reward_predictor_layer.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/reward_predictor_layer.py new file mode 100644 index 0000000000000000000000000000000000000000..934e322c53027e3750e062a8962c6cdbccf72f6b --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/reward_predictor_layer.py @@ -0,0 +1,106 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf + +[2] Mastering Atari with Discrete World Models - 2021 +D. Hafner, T. Lillicrap, M. Norouzi, J. Ba +https://arxiv.org/pdf/2010.02193.pdf +""" + +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() +if torch: + F = nn.functional + + +class RewardPredictorLayer(nn.Module): + """A layer outputting reward predictions using K bins and two-hot encoding. + + This layer is used in two models in DreamerV3: The reward predictor of the world + model and the value function. K is 255 by default (see [1]) and doesn't change + with the model size. + + Possible predicted reward/values range from symexp(-20.0) to symexp(20.0), which + should cover any possible environment. Outputs of this layer are generated by + generating logits/probs via a single linear layer, then interpreting the probs + as weights for a weighted average of the different possible reward (binned) values. + """ + + def __init__( + self, + *, + input_size: int, + num_buckets: int = 255, + lower_bound: float = -20.0, + upper_bound: float = 20.0, + ): + """Initializes a RewardPredictorLayer instance. + + Args: + input_size: The input size of the reward predictor layer. + num_buckets: The number of buckets to create. Note that the number of + possible symlog'd outcomes from the used distribution is + `num_buckets` + 1: + lower_bound --bucket-- o[1] --bucket-- o[2] ... --bucket-- upper_bound + o=outcomes + lower_bound=o[0] + upper_bound=o[num_buckets] + lower_bound: The symlog'd lower bound for a possible reward value. + Note that a value of -20.0 here already allows individual (actual env) + rewards to be as low as -400M. Buckets will be created between + `lower_bound` and `upper_bound`. + upper_bound: The symlog'd upper bound for a possible reward value. + Note that a value of +20.0 here already allows individual (actual env) + rewards to be as high as 400M. Buckets will be created between + `lower_bound` and `upper_bound`. + """ + self.num_buckets = num_buckets + super().__init__() + + self.lower_bound = lower_bound + self.upper_bound = upper_bound + self.reward_buckets_layer = nn.Linear( + in_features=input_size, out_features=self.num_buckets, bias=True + ) + nn.init.zeros_(self.reward_buckets_layer.weight) + nn.init.zeros_(self.reward_buckets_layer.bias) + # self.reward_buckets_layer.weight.data.fill_(0.0) + # self.reward_buckets_layer.bias.data.fill_(0.0) + + def forward(self, inputs, return_logits=False): + """Computes the expected reward using N equal sized buckets of possible values. + + Args: + inputs: The input tensor for the layer, which computes the reward bucket + weights (logits). [B, dim]. + return_logits: Whether to return the logits over the reward buckets + as a second return value (besides the expected reward). + + Returns: + The expected reward OR a tuple consisting of the expected reward and the + torch `FiniteDiscrete` distribution object. To get the individual bucket + probs, do `[FiniteDiscrete object].probs`. + """ + # Compute the `num_buckets` weights. + logits = self.reward_buckets_layer(inputs) + + # Compute the expected(!) reward using the formula: + # `softmax(Linear(x))` [vectordot] `possible_outcomes`, where + # `possible_outcomes` is the even-spaced (binned) encoding of all possible + # symexp'd reward/values. + probs = F.softmax(logits, dim=-1) + possible_outcomes = torch.linspace( + self.lower_bound, self.upper_bound, self.num_buckets, device=logits.device + ) + # probs=possible_outcomes=[B, `num_buckets`] + + # Simple vector dot product (over last dim) to get the mean reward + # weighted sum, where all weights sum to 1.0. + expected_rewards = torch.sum(probs * possible_outcomes, dim=-1) + # expected_rewards=[B] + + if return_logits: + return expected_rewards, logits + return expected_rewards diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/sequence_model.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/sequence_model.py new file mode 100644 index 0000000000000000000000000000000000000000..38934a016aa6ca811195d808dccd89d9ef018d9a --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/sequence_model.py @@ -0,0 +1,132 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +from typing import Optional + +import gymnasium as gym + +from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + dreamerv3_normal_initializer, +) +from ray.rllib.algorithms.dreamerv3.torch.models.components.mlp import MLP +from ray.rllib.algorithms.dreamerv3.utils import get_dense_hidden_units, get_gru_units +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +class SequenceModel(nn.Module): + """The "sequence model" of the RSSM, computing ht+1 given (ht, zt, at). + + Note: The "internal state" always consists of: + The actions `a` (initially, this is a zeroed-out action), `h`-states (deterministic, + continuous), and `z`-states (stochastic, discrete). + There are two versions of z-states: "posterior" for world model training and "prior" + for creating the dream data. + + Initial internal state values (`a`, `h`, and `z`) are used where ever a new episode + starts within a batch row OR at the beginning of each train batch's B rows, + regardless of whether there was an actual episode boundary or not. Thus, internal + states are not required to be stored in or retrieved from the replay buffer AND + retrieved batches from the buffer must not be zero padded. + + Initial `a` is the zero "one hot" action, e.g. [0.0, 0.0] for Discrete(2), initial + `h` is a separate learned variable, and initial `z` are computed by the "dynamics" + (or "prior") net, using only the initial-h state as input. + + The GRU in this SequenceModel always produces the next h-state, then. + """ + + def __init__( + self, + *, + input_size: int, + model_size: str = "XS", + action_space: gym.Space, + num_gru_units: Optional[int] = None, + ): + """Initializes a SequenceModel instance. + + Args: + input_size: The input size of the pre-layer (Dense) of the sequence model. + model_size: The "Model Size" used according to [1] Appendinx B. + Use None for manually setting the number of GRU units used. + action_space: The action space the our environment used. + num_gru_units: Overrides the number of GRU units (dimension of the h-state). + If None, use the value given through `model_size` + (see [1] Appendix B). + """ + super().__init__() + + num_gru_units = get_gru_units(model_size, override=num_gru_units) + self.action_space = action_space + + # In Danijar's code, there is an additional layer (units=[model_size]) + # prior to the GRU (but always only with 1 layer), which is not mentioned in + # the paper. + # In Danijar's code, this layer is called: `img_in`. + self.pre_gru_layer = MLP( + input_size=input_size, + num_dense_layers=1, + model_size=model_size, + output_layer_size=None, + ) + gru_input_size = get_dense_hidden_units(model_size) + + # Use a custom GRU implementation w/ Normal init, layernorm, no bias + # (just like Danijar's GRU). + # In Danijar's code, this layer is called: `gru`. + self.gru_unit = DreamerV3GRU(input_size=gru_input_size, cell_size=num_gru_units) + + def forward(self, a, h, z): + """ + + Args: + a: The previous action (already one-hot'd if applicable). (B, ...). + h: The previous deterministic hidden state of the sequence model. + (B, num_gru_units) + z: The previous stochastic discrete representations of the original + observation input. (B, num_categoricals, num_classes_per_categorical). + """ + # Flatten last two dims of z. + z_shape = z.shape + z = z.view(z_shape[0], -1) + out = torch.cat([z, a], dim=-1) + # Pass through pre-GRU layer. + out = self.pre_gru_layer(out) + # Pass through GRU (add extra time axis at 0 to make time-major). + h_next, _ = self.gru_unit(out.unsqueeze(0), h.unsqueeze(0)) + h_next = h_next.squeeze(0) # Remove extra time dimension again. + # Return the GRU's output (the next h-state). + return h_next + + +class DreamerV3GRU(nn.Module): + """Analogous to Danijar's JAX GRU unit code.""" + + def __init__(self, input_size, cell_size): + super().__init__() + self.cell_size = cell_size + self.output_size = 3 * self.cell_size + + self.linear = nn.Linear( + input_size + self.cell_size, + self.output_size, + bias=False, + ) + dreamerv3_normal_initializer(list(self.linear.parameters())) + + self.layer_norm = nn.LayerNorm(self.output_size, eps=0.001) + + def forward(self, x, h): + x = torch.cat([h, x], dim=-1) + x = self.linear(x) + x = self.layer_norm(x) + reset, cand, update = torch.split(x, self.cell_size, dim=-1) + reset = torch.sigmoid(reset) + cand = torch.tanh(reset * cand) + update = torch.sigmoid(update - 1) + h = update * cand + (1 - update) * h + return h, h diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/vector_decoder.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/vector_decoder.py new file mode 100644 index 0000000000000000000000000000000000000000..f71931f68f72fefbccf668753322bfe82db75777 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/components/vector_decoder.py @@ -0,0 +1,68 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +import gymnasium as gym + +from ray.rllib.algorithms.dreamerv3.torch.models.components.mlp import MLP +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +class VectorDecoder(nn.Module): + """A simple vector decoder to reproduce non-image (1D vector) observations. + + Wraps an MLP for mean parameter computations and a Gaussian distribution, + from which we then sample using these mean values and a fixed stddev of 1.0. + """ + + def __init__( + self, + *, + input_size: int, + model_size: str = "XS", + observation_space: gym.Space, + ): + """Initializes a VectorDecoder instance. + + Args: + input_size: The input size of the vector decoder. + model_size: The "Model Size" used according to [1] Appendinx B. + Determines the exact size of the underlying MLP. + observation_space: The observation space to decode back into. This must + be a Box of shape (d,), where d >= 1. + """ + super().__init__() + + assert ( + isinstance(observation_space, gym.spaces.Box) + and len(observation_space.shape) == 1 + ) + + self.mlp = MLP( + input_size=input_size, + model_size=model_size, + output_layer_size=observation_space.shape[0], + ) + + def forward(self, h, z): + """Performs a forward pass through the vector encoder. + + Args: + h: The deterministic hidden state of the sequence model. [B, dim(h)]. + z: The stochastic discrete representations of the original + observation input. [B, num_categoricals, num_classes]. + """ + # Flatten last two dims of z. + assert len(z.shape) == 3 + z_shape = z.shape + z = z.view(z_shape[0], -1) + assert len(z.shape) == 2 + out = torch.cat([h, z], dim=-1) + # Send h-cat-z through MLP to get mean values of diag gaussian. + loc = self.mlp(out) + + # Return only the predicted observations (mean, no sample). + return loc diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/critic_network.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/critic_network.py new file mode 100644 index 0000000000000000000000000000000000000000..f4b4fb9567780c65af2d31a6d2d0809032d8e5db --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/critic_network.py @@ -0,0 +1,168 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + reward_predictor_layer, +) +from ray.rllib.algorithms.dreamerv3.torch.models.components.mlp import MLP +from ray.rllib.algorithms.dreamerv3.utils import get_dense_hidden_units +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +class CriticNetwork(nn.Module): + """The critic network described in [1], predicting values for policy learning. + + Contains a copy of itself (EMA net) for weight regularization. + The EMA net is updated after each train step via EMA (using the `ema_decay` + parameter and the actual critic's weights). The EMA net is NOT used for target + computations (we use the actual critic for that), its only purpose is to compute a + weights regularizer term for the critic's loss such that the actual critic does not + move too quickly. + """ + + def __init__( + self, + *, + input_size: int, + model_size: str = "XS", + num_buckets: int = 255, + lower_bound: float = -20.0, + upper_bound: float = 20.0, + ema_decay: float = 0.98, + ): + """Initializes a CriticNetwork instance. + + Args: + input_size: The input size of the critic network. + model_size: The "Model Size" used according to [1] Appendinx B. + Use None for manually setting the different network sizes. + num_buckets: The number of buckets to create. Note that the number of + possible symlog'd outcomes from the used distribution is + `num_buckets` + 1: + lower_bound --bucket-- o[1] --bucket-- o[2] ... --bucket-- upper_bound + o=outcomes + lower_bound=o[0] + upper_bound=o[num_buckets] + lower_bound: The symlog'd lower bound for a possible reward value. + Note that a value of -20.0 here already allows individual (actual env) + rewards to be as low as -400M. Buckets will be created between + `lower_bound` and `upper_bound`. + upper_bound: The symlog'd upper bound for a possible reward value. + Note that a value of +20.0 here already allows individual (actual env) + rewards to be as high as 400M. Buckets will be created between + `lower_bound` and `upper_bound`. + ema_decay: The weight to use for updating the weights of the critic's copy + vs the actual critic. After each training update, the EMA copy of the + critic gets updated according to: + ema_net=(`ema_decay`*ema_net) + (1.0-`ema_decay`)*critic_net + The EMA copy of the critic is used inside the critic loss function only + to produce a regularizer term against the current critic's weights, NOT + to compute any target values. + """ + super().__init__() + + self.input_size = input_size + self.model_size = model_size + self.ema_decay = ema_decay + + # "Fast" critic network(s) (mlp + reward-pred-layer). This is the network + # we actually train with our critic loss. + # IMPORTANT: We also use this to compute the return-targets, BUT we regularize + # the critic loss term such that the weights of this fast critic stay close + # to the EMA weights (see below). + self.mlp = MLP( + input_size=self.input_size, + model_size=self.model_size, + output_layer_size=None, + ) + reward_predictor_input_size = get_dense_hidden_units(self.model_size) + self.return_layer = reward_predictor_layer.RewardPredictorLayer( + input_size=reward_predictor_input_size, + num_buckets=num_buckets, + lower_bound=lower_bound, + upper_bound=upper_bound, + ) + + # Weights-EMA (EWMA) containing networks for critic loss (similar to a + # target net, BUT not used to compute anything, just for the + # weights regularizer term inside the critic loss). + self.mlp_ema = MLP( + input_size=self.input_size, + model_size=self.model_size, + output_layer_size=None, + ) + self.return_layer_ema = reward_predictor_layer.RewardPredictorLayer( + input_size=reward_predictor_input_size, + num_buckets=num_buckets, + lower_bound=lower_bound, + upper_bound=upper_bound, + ) + + def forward(self, h, z, return_logits=False, use_ema=False): + """Performs a forward pass through the critic network. + + Args: + h: The deterministic hidden state of the sequence model. [B, dim(h)]. + z: The stochastic discrete representations of the original + observation input. [B, num_categoricals, num_classes]. + return_logits: Whether also return (as a second tuple item) the logits + computed by the binned return layer (instead of only the value itself). + use_ema: Whether to use the EMA-copy of the critic instead of the actual + critic to perform this computation. + """ + # Flatten last two dims of z. + assert len(z.shape) == 3 + z_shape = z.shape + z = z.view(z_shape[0], -1) + assert len(z.shape) == 2 + out = torch.cat([h, z], dim=-1) + + if not use_ema: + # Send h-cat-z through MLP. + out = self.mlp(out) + # Return expected return OR (expected return, probs of bucket values). + return self.return_layer(out, return_logits=return_logits) + else: + out = self.mlp_ema(out) + return self.return_layer_ema(out, return_logits=return_logits) + + def init_ema(self) -> None: + """Initializes the EMA-copy of the critic from the critic's weights. + + After calling this method, the two networks have identical weights and the EMA + net will be non-trainable. + """ + for param_ema, param in zip(self.mlp_ema.parameters(), self.mlp.parameters()): + param_ema.data.copy_(param.data) + # Make all EMA parameters non-trainable. + param_ema.requires_grad = False + assert param_ema.grad is None + + for param_ema, param in zip( + self.return_layer_ema.parameters(), self.return_layer.parameters() + ): + param_ema.data.copy_(param.data) + # Make all EMA parameters non-trainable. + param_ema.requires_grad = False + assert param_ema.grad is None + + def update_ema(self) -> None: + """Updates the EMA-copy of the critic according to the update formula: + + ema_net=(`ema_decay`*ema_net) + (1.0-`ema_decay`)*critic_net + """ + for param_ema, param in zip(self.mlp_ema.parameters(), self.mlp.parameters()): + param_ema.data.mul_(self.ema_decay).add_( + (1.0 - self.ema_decay) * param.data + ) + + for param_ema, param in zip( + self.return_layer_ema.parameters(), self.return_layer.parameters() + ): + param_ema.data.mul_(self.ema_decay).add_( + (1.0 - self.ema_decay) * param.data + ) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/dreamer_model.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/dreamer_model.py new file mode 100644 index 0000000000000000000000000000000000000000..60e4e044a1bdaadeaea7e4c7025ed5287804056b --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/dreamer_model.py @@ -0,0 +1,518 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +import re + +import gymnasium as gym +import numpy as np + +from ray.rllib.algorithms.dreamerv3.torch.models.actor_network import ActorNetwork +from ray.rllib.algorithms.dreamerv3.torch.models.critic_network import CriticNetwork +from ray.rllib.algorithms.dreamerv3.torch.models.world_model import WorldModel +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.torch_utils import inverse_symlog + +torch, nn = try_import_torch() + + +class DreamerModel(nn.Module): + """The main PyTorch model containing all necessary components for DreamerV3. + + Includes: + - The world model with encoder, decoder, sequence-model (RSSM), dynamics + (generates prior z-state), and "posterior" model (generates posterior z-state). + Predicts env dynamics and produces dreamed trajectories for actor- and critic + learning. + - The actor network (policy). + - The critic network for value function prediction. + """ + + def __init__( + self, + *, + model_size: str = "XS", + action_space: gym.Space, + world_model: WorldModel, + actor: ActorNetwork, + critic: CriticNetwork, + use_curiosity: bool = False, + intrinsic_rewards_scale: float = 0.1, + ): + """Initializes a DreamerModel instance. + + Args: + model_size: The "Model Size" used according to [1] Appendinx B. + Use None for manually setting the different network sizes. + action_space: The action space the our environment used. + world_model: The WorldModel component. + actor: The ActorNetwork component. + critic: The CriticNetwork component. + """ + super().__init__() + + self.model_size = model_size + self.action_space = action_space + self.use_curiosity = use_curiosity + + self.world_model = world_model + self.actor = actor + self.critic = critic + + self.disagree_nets = None + if self.use_curiosity: + raise NotImplementedError + + def forward_inference(self, observations, previous_states, is_first): + """Performs a (non-exploring) action computation step given obs and states. + + Note that all input data should not have a time rank (only a batch dimension). + + Args: + observations: The current environment observation with shape (B, ...). + previous_states: Dict with keys `a`, `h`, and `z` used as input to the RSSM + to produce the next h-state, from which then to compute the action + using the actor network. All values in the dict should have shape + (B, ...) (no time rank). + is_first: Batch of is_first flags. These should be True if a new episode + has been started at the current timestep (meaning `observations` is the + reset observation from the environment). + """ + # Perform one step in the world model (starting from `previous_state` and + # using the observations to yield a current (posterior) state). + states = self.world_model.forward_inference( + observations=observations, + previous_states=previous_states, + is_first=is_first, + ) + # Compute action using our actor network and the current states. + _, distr_params = self.actor( + h=states["h"], z=states["z"], return_distr_params=True + ) + # Use the mode of the distribution (Discrete=argmax, Normal=mean). + distr = self.actor.get_action_dist_object(distr_params) + actions = distr.mode + return actions, {"h": states["h"], "z": states["z"], "a": actions} + + def forward_exploration(self, observations, previous_states, is_first): + """Performs an exploratory action computation step given obs and states. + + Note that all input data should not have a time rank (only a batch dimension). + + Args: + observations: The current environment observation with shape (B, ...). + previous_states: Dict with keys `a`, `h`, and `z` used as input to the RSSM + to produce the next h-state, from which then to compute the action + using the actor network. All values in the dict should have shape + (B, ...) (no time rank). + is_first: Batch of is_first flags. These should be True if a new episode + has been started at the current timestep (meaning `observations` is the + reset observation from the environment). + """ + # Perform one step in the world model (starting from `previous_state` and + # using the observations to yield a current (posterior) state). + states = self.world_model.forward_inference( + observations=observations, + previous_states=previous_states, + is_first=is_first, + ) + # Compute action using our actor network and the current states. + actions = self.actor(h=states["h"], z=states["z"]) + return actions, {"h": states["h"], "z": states["z"], "a": actions} + + def forward_train(self, observations, actions, is_first): + """Performs a training forward pass given observations and actions. + + Note that all input data must have a time rank (batch-major: [B, T, ...]). + + Args: + observations: The environment observations with shape (B, T, ...). Thus, + the batch has B rows of T timesteps each. Note that it's ok to have + episode boundaries (is_first=True) within a batch row. DreamerV3 will + simply insert an initial state before these locations and continue the + sequence modelling (with the RSSM). Hence, there will be no zero + padding. + actions: The actions actually taken in the environment with shape + (B, T, ...). See `observations` docstring for details on how B and T are + handled. + is_first: Batch of is_first flags. These should be True: + - if a new episode has been started at the current timestep (meaning + `observations` is the reset observation from the environment). + - in each batch row at T=0 (first timestep of each of the B batch + rows), regardless of whether the actual env had an episode boundary + there or not. + """ + return self.world_model.forward_train( + observations=observations, + actions=actions, + is_first=is_first, + ) + + def get_initial_state(self): + """Returns the initial state of the dreamer model (a, h-, z-states). + + An initial state is generated using the previous action, the tanh of the + (learned) h-state variable and the dynamics predictor (or "prior net") to + compute z^0 from h0. In this last step, it is important that we do NOT sample + the z^-state (as we would usually do during dreaming), but rather take the mode + (argmax, then one-hot again). + + Note that the initial state is returned without batch dimension. + """ + states = self.world_model.get_initial_state() + + action_dim = ( + self.action_space.n + if isinstance(self.action_space, gym.spaces.Discrete) + else np.prod(self.action_space.shape) + ) + states["a"] = torch.zeros((action_dim,), dtype=torch.float32) + return states + + def dream_trajectory(self, start_states, start_is_terminated, timesteps_H, gamma): + """Dreams trajectories of length H from batch of h- and z-states. + + Note that incoming data will have the shapes (BxT, ...), where the original + batch- and time-dimensions are already folded together. Beginning from this + new batch dim (BxT), we will unroll `timesteps_H` timesteps in a time-major + fashion, such that the dreamed data will have shape (H, BxT, ...). + + Args: + start_states: Dict of `h` and `z` states in the shape of (B, ...) and + (B, num_categoricals, num_classes), respectively, as + computed by a train forward pass. From each individual h-/z-state pair + in the given batch, we will branch off a dreamed trajectory of len + `timesteps_H`. + start_is_terminated: Float flags of shape (B,) indicating whether the + first timesteps of each batch row is already a terminated timestep + (given by the actual environment). + timesteps_H: The number of timesteps to dream for. + gamma: The discount factor gamma. + """ + # Dreamed actions (one-hot encoded for discrete actions). + a_dreamed_t0_to_H = [] + a_dreamed_dist_params_t0_to_H = [] + + h = start_states["h"].detach() + z = start_states["z"].detach() + + # GRU outputs. + h_states_t0_to_H = [h] + # Dynamics model outputs. + z_states_prior_t0_to_H = [z] + + # Compute `a` using actor network (already the first step uses a dreamed action, + # not a sampled one). + a, a_dist_params = self.actor( + # We have to stop the gradients through the states. B/c we are using a + # differentiable Discrete action distribution (straight through gradients + # with `a = stop_gradient(sample(probs)) + probs - stop_gradient(probs)`, + # we otherwise would add dependencies of the `-log(pi(a|s))` REINFORCE loss + # term on actions further back in the trajectory. + h=h.detach(), + z=z.detach(), + return_distr_params=True, + ) + a_dreamed_t0_to_H.append(a) + a_dreamed_dist_params_t0_to_H.append(a_dist_params) + + # Disable all gradients from the world model so they don't get backprop'd + # through twice when computing the actor loss (for cont. actions). + for p in self.world_model.parameters(): + p.requires_grad_(False) + + for i in range(timesteps_H): + # Move one step in the dream using the RSSM. + h = self.world_model.sequence_model(a=a, h=h, z=z) + h_states_t0_to_H.append(h) + + # Compute prior z using dynamics model. + z = self.world_model.dynamics_predictor(h=h) + z_states_prior_t0_to_H.append(z) + + # Compute `a` using actor network. + a, a_dist_params = self.actor( + h=h.detach(), + z=z.detach(), + return_distr_params=True, + ) + a_dreamed_t0_to_H.append(a) + a_dreamed_dist_params_t0_to_H.append(a_dist_params) + + h_states_H_B = torch.stack(h_states_t0_to_H, dim=0) # (T, B, ...) + h_states_HxB = h_states_H_B.reshape([-1] + list(h_states_H_B.shape[2:])) + + z_states_prior_H_B = torch.stack(z_states_prior_t0_to_H, dim=0) # (T, B, ...) + z_states_prior_HxB = z_states_prior_H_B.reshape( + [-1] + list(z_states_prior_H_B.shape[2:]) + ) + + a_dreamed_H_B = torch.stack(a_dreamed_t0_to_H, dim=0) # (T, B, ...) + a_dreamed_dist_params_H_B = torch.stack(a_dreamed_dist_params_t0_to_H, dim=0) + + # Compute r using reward predictor. + r_dreamed_H_B = inverse_symlog( + self.world_model.reward_predictor(h=h_states_HxB, z=z_states_prior_HxB) + ) + r_dreamed_H_B = r_dreamed_H_B.reshape([timesteps_H + 1, -1]) + + # Compute intrinsic rewards. + if self.use_curiosity: + results_HxB = self.disagree_nets.compute_intrinsic_rewards( + h=h_states_HxB, + z=z_states_prior_HxB, + a=a_dreamed_H_B.reshape([-1] + a_dreamed_H_B.shape[2:]), + ) + r_intrinsic_H_B = results_HxB["rewards_intrinsic"] + r_intrinsic_H_B = r_intrinsic_H_B.reshape([timesteps_H + 1, -1])[1:] + curiosity_forward_train_outs = results_HxB["forward_train_outs"] + del results_HxB + + # Compute continues using continue predictor. + c_dreamed_HxB = self.world_model.continue_predictor( + h=h_states_HxB, + z=z_states_prior_HxB, + ) + c_dreamed_H_B = c_dreamed_HxB.reshape([timesteps_H + 1, -1]) + # Force-set first `continue` flags to False iff `start_is_terminated`. + # Note: This will cause the loss-weights for this row in the batch to be + # completely zero'd out. In general, we don't use dreamed data past any + # predicted (or actual first) continue=False flags. + c_dreamed_H_B = torch.cat( + [1.0 - start_is_terminated.unsqueeze(0).float(), c_dreamed_H_B[1:]], dim=0 + ) + + # Loss weights for each individual dreamed timestep. Zero-out all timesteps + # that lie past continue=False flags. B/c our world model does NOT learn how + # to skip terminal/reset episode boundaries, dreamed data crossing such a + # boundary should not be used for critic/actor learning either. + dream_loss_weights_H_B = torch.cumprod(gamma * c_dreamed_H_B, dim=0) / gamma + + # Reactivate world model gradients. + for p in self.world_model.parameters(): + p.requires_grad_(True) + + # Compute the symlog'd value logits (w/o world model gradients; used for the + # critic loss). + _, v_symlog_dreamed_logits_HxB_wm_detached = self.critic( + h=h_states_HxB.detach(), + z=z_states_prior_HxB.detach(), + use_ema=False, + return_logits=True, + ) + + # Compute the value estimates (including world model gradients -> 1 sequence + # model step after the action has been computed; used for the scaled value + # target used in the actor loss for cont. actions). + # Disable all gradients from the critic so they don't get backprop'd + # through twice when computing the actor loss (for cont. actions). + for p in self.critic.parameters(): + p.requires_grad_(False) + v, _ = self.critic( + h=h_states_HxB, + z=z_states_prior_HxB, + use_ema=False, + return_logits=True, + ) + # Reactivate critic gradients. + for p in self.critic.parameters(): + p.requires_grad_(True) + v_dreamed_HxB = inverse_symlog(v) + v_dreamed_H_B = v_dreamed_HxB.reshape([timesteps_H + 1, -1]) + + # Compute the EMA net outputs w/o any gradients. + with torch.no_grad(): + v_symlog_dreamed_ema_HxB = self.critic( + h=h_states_HxB.detach(), + z=z_states_prior_HxB.detach(), + return_logits=False, + use_ema=True, + ) + v_symlog_dreamed_ema_H_B = v_symlog_dreamed_ema_HxB.reshape( + [timesteps_H + 1, -1] + ) + + ret = { + "h_states_t0_to_H_BxT": h_states_H_B, + "z_states_prior_t0_to_H_BxT": z_states_prior_H_B, + "rewards_dreamed_t0_to_H_BxT": r_dreamed_H_B, + "continues_dreamed_t0_to_H_BxT": c_dreamed_H_B, + "actions_dreamed_t0_to_H_BxT": a_dreamed_H_B, + "actions_dreamed_dist_params_t0_to_H_BxT": a_dreamed_dist_params_H_B, + # Critic (w/ world-model grads for actor loss). + "values_dreamed_t0_to_H_BxT": v_dreamed_H_B, + # Critic (world-model detached, for critic loss). + "values_symlog_dreamed_logits_t0_to_HxBxT_wm_detached": v_symlog_dreamed_logits_HxB_wm_detached, + # Critic EMA. + "v_symlog_dreamed_ema_t0_to_H_BxT": v_symlog_dreamed_ema_H_B, + # Loss weights for critic- and actor losses. + "dream_loss_weights_t0_to_H_BxT": dream_loss_weights_H_B, + } + + if self.use_curiosity: + ret["rewards_intrinsic_t1_to_H_B"] = r_intrinsic_H_B + ret.update(curiosity_forward_train_outs) + + if isinstance(self.action_space, gym.spaces.Discrete): + ret["actions_ints_dreamed_t0_to_H_B"] = torch.argmax(a_dreamed_H_B, dim=-1) + + return ret + + def dream_trajectory_with_burn_in( + self, + *, + start_states, + timesteps_burn_in: int, + timesteps_H: int, + observations, # [B, >=timesteps_burn_in] + actions, # [B, timesteps_burn_in (+timesteps_H)?] + use_sampled_actions_in_dream: bool = False, + use_random_actions_in_dream: bool = False, + ): + """Dreams trajectory from N initial observations and initial states. + + Note: This is only used for reporting and debugging, not for actual world-model + or policy training. + + Args: + start_states: The batch of start states (dicts with `a`, `h`, and `z` keys) + to begin dreaming with. These are used to compute the first h-state + using the sequence model. + timesteps_burn_in: For how many timesteps should be use the posterior + z-states (computed by the posterior net and actual observations from + the env)? + timesteps_H: For how many timesteps should we dream using the prior + z-states (computed by the dynamics (prior) net and h-states only)? + Note that the total length of the returned trajectories will + be `timesteps_burn_in` + `timesteps_H`. + observations: The batch (B, T, ...) of observations (to be used only during + burn-in over `timesteps_burn_in` timesteps). + actions: The batch (B, T, ...) of actions to use during a) burn-in over the + first `timesteps_burn_in` timesteps and - possibly - b) during + actual dreaming, iff use_sampled_actions_in_dream=True. + use_sampled_actions_in_dream: If True, instead of using our actor network + to compute fresh actions, we will use the one provided via the `actions` + argument. Note that in the latter case, the `actions` time dimension + must be at least `timesteps_burn_in` + `timesteps_H` long. + use_random_actions_in_dream: Whether to use randomly sampled actions in the + dream. Note that this does not apply to the burn-in phase, during which + we will always use the actions given in the `actions` argument. + """ + assert not (use_sampled_actions_in_dream and use_random_actions_in_dream) + + B = observations.shape[0] + + # Produce initial N internal posterior states (burn-in) using the given + # observations: + states = start_states + for i in range(timesteps_burn_in): + states = self.world_model.forward_inference( + observations=observations[:, i : i + 1], + previous_states=states, + is_first=torch.full((B,), 1.0 if i == 0 else 0.0), + ) + states["a"] = actions[:, i] + + # Start producing the actual dream, using prior states and either the given + # actions, dreamed, or random ones. + h_states_t0_to_H = [states["h"]] + z_states_prior_t0_to_H = [states["z"]] + a_t0_to_H = [states["a"]] + + for j in range(timesteps_H): + # Compute next h using sequence model. + h = self.world_model.sequence_model( + a=states["a"], + h=states["h"], + z=states["z"], + ) + h_states_t0_to_H.append(h) + # Compute z from h, using the dynamics model (we don't have an actual + # observation at this timestep). + z = self.world_model.dynamics_predictor(h=h) + z_states_prior_t0_to_H.append(z) + + # Compute next dreamed action or use sampled one or random one. + if use_sampled_actions_in_dream: + a = actions[:, timesteps_burn_in + j] + elif use_random_actions_in_dream: + if isinstance(self.action_space, gym.spaces.Discrete): + a = torch.randint(self.action_space.n, (B,), dtype=torch.int64) + a = torch.nn.functional.one_hot(a, num_classes=self.action_space.n) + else: + a = torch.rand( + (B,) + self.action_space.shape, dtype=self.action_space.dtype + ) + else: + a = self.actor(h=h, z=z) + a_t0_to_H.append(a) + + states = {"h": h, "z": z, "a": a} + + # Fold time-rank for upcoming batch-predictions (no sequences needed anymore). + h_states_t0_to_H_B = torch.stack(h_states_t0_to_H, dim=0) + h_states_t0_to_HxB = h_states_t0_to_H_B.reshape( + [-1] + list(h_states_t0_to_H_B.shape[2:]) + ) + + z_states_prior_t0_to_H_B = torch.stack(z_states_prior_t0_to_H, dim=0) + z_states_prior_t0_to_HxB = z_states_prior_t0_to_H_B.reshape( + [-1] + list(z_states_prior_t0_to_H_B.shape[2:]) + ) + + a_t0_to_H_B = torch.stack(a_t0_to_H, dim=0) + + # Compute o using decoder. + o_dreamed_t0_to_HxB = self.world_model.decoder( + h=h_states_t0_to_HxB, + z=z_states_prior_t0_to_HxB, + ) + if self.world_model.symlog_obs: + o_dreamed_t0_to_HxB = inverse_symlog(o_dreamed_t0_to_HxB) + + # Compute r using reward predictor. + r_dreamed_t0_to_H_B = inverse_symlog( + self.world_model.reward_predictor( + h=h_states_t0_to_HxB, + z=z_states_prior_t0_to_HxB, + ) + ).reshape([-1, B]) + + # Compute continues using continue predictor. + c_dreamed_t0_to_H_B = self.world_model.continue_predictor( + h=h_states_t0_to_HxB, + z=z_states_prior_t0_to_HxB, + ).reshape([-1, B]) + + # Return everything as time-major (H, B, ...), where H is the timesteps dreamed + # (NOT burn-in'd) and B is a batch dimension (this might or might not include + # an original time dimension from the real env, from all of which we then branch + # out our dream trajectories). + ret = { + "h_states_t0_to_H_BxT": h_states_t0_to_H_B, + "z_states_prior_t0_to_H_BxT": z_states_prior_t0_to_H_B, + # Unfold time-ranks in predictions. + "observations_dreamed_t0_to_H_BxT": torch.reshape( + o_dreamed_t0_to_HxB, [-1, B] + list(observations.shape)[2:] + ), + "rewards_dreamed_t0_to_H_BxT": r_dreamed_t0_to_H_B, + "continues_dreamed_t0_to_H_BxT": c_dreamed_t0_to_H_B, + } + + # Figure out action key (random, sampled from env, dreamed?). + if use_sampled_actions_in_dream: + key = "actions_sampled_t0_to_H_BxT" + elif use_random_actions_in_dream: + key = "actions_random_t0_to_H_BxT" + else: + key = "actions_dreamed_t0_to_H_BxT" + ret[key] = a_t0_to_H_B + + # Also provide int-actions, if discrete action space. + if isinstance(self.action_space, gym.spaces.Discrete): + ret[re.sub("^actions_", "actions_ints_", key)] = torch.argmax( + a_t0_to_H_B, dim=-1 + ) + + return ret diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/world_model.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/world_model.py new file mode 100644 index 0000000000000000000000000000000000000000..5e2b4de597f393212a47bec0eafbb4b0c3a6cb9f --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/torch/models/world_model.py @@ -0,0 +1,431 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" +from typing import Optional + +import gymnasium as gym +import numpy as np +import tree # pip install dm_tree + +from ray.rllib.algorithms.dreamerv3.torch.models.components import ( + representation_layer, +) +from ray.rllib.algorithms.dreamerv3.torch.models.components.continue_predictor import ( + ContinuePredictor, +) +from ray.rllib.algorithms.dreamerv3.torch.models.components.dynamics_predictor import ( + DynamicsPredictor, +) +from ray.rllib.algorithms.dreamerv3.torch.models.components.mlp import MLP +from ray.rllib.algorithms.dreamerv3.torch.models.components.reward_predictor import ( + RewardPredictor, +) +from ray.rllib.algorithms.dreamerv3.torch.models.components.sequence_model import ( + SequenceModel, +) +from ray.rllib.algorithms.dreamerv3.utils import get_dense_hidden_units, get_gru_units +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.torch_utils import symlog + +torch, nn = try_import_torch() +if torch: + F = nn.functional + + +class WorldModel(nn.Module): + """WorldModel component of [1] w/ encoder, decoder, RSSM, reward/cont. predictors. + + See eq. 3 of [1] for all components and their respective in- and outputs. + Note that in the paper, the "encoder" includes both the raw encoder plus the + "posterior net", which produces posterior z-states from observations and h-states. + + Note: The "internal state" of the world model always consists of: + The actions `a` (initially, this is a zeroed-out action), `h`-states (deterministic, + continuous), and `z`-states (stochastic, discrete). + There are two versions of z-states: "posterior" for world model training and "prior" + for creating the dream data. + + Initial internal state values (`a`, `h`, and `z`) are inserted where ever a new + episode starts within a batch row OR at the beginning of each train batch's B rows, + regardless of whether there was an actual episode boundary or not. Thus, internal + states are not required to be stored in or retrieved from the replay buffer AND + retrieved batches from the buffer must not be zero padded. + + Initial `a` is the zero "one hot" action, e.g. [0.0, 0.0] for Discrete(2), initial + `h` is a separate learned variable, and initial `z` are computed by the "dynamics" + (or "prior") net, using only the initial-h state as input. + """ + + def __init__( + self, + *, + model_size: str = "XS", + observation_space: gym.Space, + action_space: gym.Space, + batch_length_T: int = 64, + encoder: nn.Module, + decoder: nn.Module, + num_gru_units: Optional[int] = None, + symlog_obs: bool = True, + ): + """Initializes a WorldModel instance. + + Args: + model_size: The "Model Size" used according to [1] Appendinx B. + Use None for manually setting the different network sizes. + action_space: The action space the our environment used. + batch_length_T: The length (T) of the sequences used for training. The + actual shape of the input data (e.g. rewards) is then: [B, T, ...], + where B is the "batch size", T is the "batch length" (this arg) and + "..." is the dimension of the data (e.g. (64, 64, 3) for Atari image + observations). Note that a single row (within a batch) may contain data + from different episodes, but an already on-going episode is always + finished, before a new one starts within the same row. + encoder: The encoder Model taking observations as inputs and + outputting a 1D latent vector that will be used as input into the + posterior net (z-posterior state generating layer). Inputs are symlogged + if inputs are NOT images. For images, we use normalization between -1.0 + and 1.0 (x / 128 - 1.0) + decoder: The decoder Model taking h- and z-states as inputs and generating + a (possibly symlogged) predicted observation. Note that for images, + the last decoder layer produces the exact, normalized pixel values + (not a Gaussian as described in [1]!). + num_gru_units: The number of GRU units to use. If None, use + `model_size` to figure out this parameter. + symlog_obs: Whether to predict decoded observations in symlog space. + This should be False for image based observations. + According to the paper [1] Appendix E: "NoObsSymlog: This ablation + removes the symlog encoding of inputs to the world model and also + changes the symlog MSE loss in the decoder to a simple MSE loss. + *Because symlog encoding is only used for vector observations*, this + ablation is equivalent to DreamerV3 on purely image-based environments". + """ + super().__init__() + + self.model_size = model_size + self.batch_length_T = batch_length_T + self.symlog_obs = symlog_obs + self.action_space = action_space + a_flat = ( + action_space.n + if isinstance(action_space, gym.spaces.Discrete) + else (np.prod(action_space.shape)) + ) + + # Encoder (latent 1D vector generator) (xt -> lt). + self.encoder = encoder + + self.num_gru_units = get_gru_units( + model_size=self.model_size, + override=num_gru_units, + ) + + # Posterior predictor consisting of an MLP and a RepresentationLayer: + # [ht, lt] -> zt. + # In Danijar's code, this is called: `obs_out`. + self.posterior_mlp = MLP( + input_size=(self.num_gru_units + encoder.output_size[0]), + model_size=self.model_size, + output_layer_size=None, + # In Danijar's code, the posterior predictor only has a single layer, + # no matter the model size: + num_dense_layers=1, + ) + # The (posterior) z-state generating layer. + # In Danijar's code, this is called: `obs_stats`. + self.posterior_representation_layer = representation_layer.RepresentationLayer( + input_size=get_dense_hidden_units(self.model_size), + model_size=self.model_size, + ) + + z_flat = ( + self.posterior_representation_layer.num_categoricals + * self.posterior_representation_layer.num_classes_per_categorical + ) + h_plus_z_flat = self.num_gru_units + z_flat + + # Dynamics (prior z-state) predictor: ht -> z^t + # In Danijar's code, the layers in this network are called: + # `img_out` (1 Linear) and `img_stats` (representation layer). + self.dynamics_predictor = DynamicsPredictor( + input_size=self.num_gru_units, model_size=self.model_size + ) + + # GRU for the RSSM: [at, ht, zt] -> ht+1 + # Initial h-state variable (learnt). + # -> tanh(self.initial_h) -> deterministic state + # Use our Dynamics predictor for initial stochastic state, BUT with greedy + # (mode) instead of sampling. + self.initial_h = nn.Parameter( + torch.zeros(self.num_gru_units), requires_grad=True + ) + # The actual sequence model containing the GRU layer. + # In Danijar's code, the layers in this network are called: + # `img_in` (1 Linear) and `gru` (custom GRU implementation). + self.sequence_model = SequenceModel( + # Only z- and a-state go into pre-layer. The output of that goes then + # into GRU (together with h-state). + input_size=int(z_flat + a_flat), + model_size=self.model_size, + action_space=self.action_space, + num_gru_units=self.num_gru_units, + ) + + # Reward Predictor: [ht, zt] -> rt. + self.reward_predictor = RewardPredictor( + input_size=h_plus_z_flat, + model_size=self.model_size, + ) + # Continue Predictor: [ht, zt] -> ct. + self.continue_predictor = ContinuePredictor( + input_size=h_plus_z_flat, + model_size=self.model_size, + ) + + # Decoder: [ht, zt] -> x^t. + self.decoder = decoder + + def get_initial_state(self) -> dict: + """Returns the (current) initial state of the world model (h- and z-states). + + An initial state is generated using the tanh of the (learned) h-state variable + and the dynamics predictor (or "prior net") to compute z^0 from h0. In this last + step, it is important that we do NOT sample the z^-state (as we would usually + do during dreaming), but rather take the mode (argmax, then one-hot again). + """ + h = torch.tanh(self.initial_h) + # Use the mode, NOT a sample for the initial z-state. + _, z_probs = self.dynamics_predictor(h.unsqueeze(0), return_z_probs=True) + z = z_probs.squeeze(0).argmax(dim=-1) + z = F.one_hot(z, num_classes=z_probs.shape[-1]) + + return {"h": h, "z": z} + + def forward_inference( + self, + observations: "torch.Tensor", + previous_states: dict, + is_first: "torch.Tensor", + ) -> dict: + """Performs a forward step for inference (e.g. environment stepping). + + Works analogous to `forward_train`, except that all inputs are provided + for a single timestep in the shape of [B, ...] (no time dimension!). + + Args: + observations: The batch (B, ...) of observations to be passed through + the encoder network to yield the inputs to the representation layer + (which then can compute the z-states). + previous_states: A dict with `h`, `z`, and `a` keys mapping to the + respective previous states/actions. All of the shape (B, ...), no time + rank. + is_first: The batch (B) of `is_first` flags. + + Returns: + The next deterministic h-state (h(t+1)) as predicted by the sequence model. + """ + B = observations.shape[0] + initial_states = tree.map_structure( + # Repeat only the batch dimension (B times). + lambda s: s.unsqueeze(0).repeat(B, *([1] * len(s.shape))), + self.get_initial_state(), + ) + + # If first, mask it with initial state/actions. + previous_h = self._mask(previous_states["h"], 1.0 - is_first) # zero out + previous_h = previous_h + self._mask(initial_states["h"], is_first) # add init + + previous_z = self._mask(previous_states["z"], 1.0 - is_first) # zero out + previous_z = previous_z + self._mask(initial_states["z"], is_first) # add init + + # Zero out actions (no special learnt initial state). + previous_a = self._mask(previous_states["a"], 1.0 - is_first) + + # Compute new states. + h = self.sequence_model(a=previous_a, h=previous_h, z=previous_z) + z = self.compute_posterior_z(observations=observations, initial_h=h) + + return {"h": h, "z": z} + + def forward_train( + self, + observations: "torch.Tensor", + actions: "torch.Tensor", + is_first: "torch.Tensor", + ) -> dict: + """Performs a forward step for training. + + 1) Forwards all observations [B, T, ...] through the encoder network to yield + o_processed[B, T, ...]. + 2) Uses initial state (h0/z^0/a0[B, 0, ...]) and sequence model (RSSM) to + compute the first internal state (h1 and z^1). + 3) Uses action a[B, 1, ...], z[B, 1, ...] and h[B, 1, ...] to compute the + next h-state (h[B, 2, ...]), etc.. + 4) Repeats 2) and 3) until t=T. + 5) Uses all h[B, T, ...] and z[B, T, ...] to compute predicted/reconstructed + observations, rewards, and continue signals. + 6) Returns predictions from 5) along with all z-states z[B, T, ...] and + the final h-state (h[B, ...] for t=T). + + Should we encounter is_first=True flags in the middle of a batch row (somewhere + within an ongoing sequence of length T), we insert this world model's initial + state again (zero-action, learned init h-state, and prior-computed z^) and + simply continue (no zero-padding). + + Args: + observations: The batch (B, T, ...) of observations to be passed through + the encoder network to yield the inputs to the representation layer + (which then can compute the posterior z-states). + actions: The batch (B, T, ...) of actions to be used in combination with + h-states and computed z-states to yield the next h-states. + is_first: The batch (B, T) of `is_first` flags. + """ + if self.symlog_obs: + observations = symlog(observations) + + # Compute bare encoder outs (not z; this is done later with involvement of the + # sequence model and the h-states). + # Fold time dimension for CNN pass. + shape = observations.shape + B, T = shape[0], shape[1] + observations = observations.view((-1,) + shape[2:]) + encoder_out = self.encoder(observations) + # Unfold time dimension. + encoder_out = encoder_out.view( + ( + B, + T, + ) + + encoder_out.shape[1:] + ) + # Make time major for faster upcoming loop. + encoder_out = encoder_out.transpose(0, 1) + # encoder_out=[T, B, ...] + + initial_states = tree.map_structure( + # Repeat only the batch dimension (B times). + lambda s: s.unsqueeze(0).repeat(B, *([1] * len(s.shape))), + self.get_initial_state(), + ) + + # Make actions and `is_first` time-major. + actions = actions.transpose(0, 1) + is_first = is_first.transpose(0, 1).float() + + # Loop through the T-axis of our samples and perform one computation step at + # a time. This is necessary because the sequence model's output (h(t+1)) depends + # on the current z(t), but z(t) depends on the current sequence model's output + # h(t). + z_t0_to_T = [initial_states["z"]] + z_posterior_probs = [] + z_prior_probs = [] + h_t0_to_T = [initial_states["h"]] + for t in range(self.batch_length_T): + # If first, mask it with initial state/actions. + h_tm1 = self._mask(h_t0_to_T[-1], 1.0 - is_first[t]) # zero out + h_tm1 = h_tm1 + self._mask(initial_states["h"], is_first[t]) # add init + + z_tm1 = self._mask(z_t0_to_T[-1], 1.0 - is_first[t]) # zero out + z_tm1 = z_tm1 + self._mask(initial_states["z"], is_first[t]) # add init + + # Zero out actions (no special learnt initial state). + a_tm1 = self._mask(actions[t - 1], 1.0 - is_first[t]) + + # Perform one RSSM (sequence model) step to get the current h. + h_t = self.sequence_model(a=a_tm1, h=h_tm1, z=z_tm1) + h_t0_to_T.append(h_t) + + posterior_mlp_input = torch.cat([encoder_out[t], h_t], dim=-1) + repr_input = self.posterior_mlp(posterior_mlp_input) + # Draw one z-sample (z(t)) and also get the z-distribution for dynamics and + # representation loss computations. + z_t, z_probs = self.posterior_representation_layer( + repr_input, + return_z_probs=True, + ) + # z_t=[B, num_categoricals, num_classes] + z_posterior_probs.append(z_probs) + z_t0_to_T.append(z_t) + + # Compute the predicted z_t (z^) using the dynamics model. + _, z_probs = self.dynamics_predictor(h_t, return_z_probs=True) + z_prior_probs.append(z_probs) + + # Stack at time dimension to yield: [B, T, ...]. + h_t1_to_T = torch.stack(h_t0_to_T[1:], dim=1) + z_t1_to_T = torch.stack(z_t0_to_T[1:], dim=1) + + # Fold time axis to retrieve the final (loss ready) Independent distribution + # (over `num_categoricals` Categoricals). + z_posterior_probs = torch.stack(z_posterior_probs, dim=1) + z_posterior_probs = z_posterior_probs.view( + (-1,) + z_posterior_probs.shape[2:], + ) + # Fold time axis to retrieve the final (loss ready) Independent distribution + # (over `num_categoricals` Categoricals). + z_prior_probs = torch.stack(z_prior_probs, dim=1) + z_prior_probs = z_prior_probs.view((-1,) + z_prior_probs.shape[2:]) + + # Fold time dimension for parallelization of all dependent predictions: + # observations (reproduction via decoder), rewards, continues. + h_BxT = h_t1_to_T.view((-1,) + h_t1_to_T.shape[2:]) + z_BxT = z_t1_to_T.view((-1,) + z_t1_to_T.shape[2:]) + + obs_distribution_means = self.decoder(h=h_BxT, z=z_BxT) + + # Compute (predicted) reward distributions. + rewards, reward_logits = self.reward_predictor( + h=h_BxT, z=z_BxT, return_logits=True + ) + + # Compute (predicted) continue distributions. + continues, continue_distribution = self.continue_predictor( + h=h_BxT, z=z_BxT, return_distribution=True + ) + + # Return outputs for loss computation. + # Note that all shapes are [BxT, ...] (time axis already folded). + return { + # Obs. + "sampled_obs_symlog_BxT": observations, + "obs_distribution_means_BxT": obs_distribution_means, + # Rewards. + "reward_logits_BxT": reward_logits, + "rewards_BxT": rewards, + # Continues. + "continue_distribution_BxT": continue_distribution, + "continues_BxT": continues, + # Deterministic, continuous h-states (t1 to T). + "h_states_BxT": h_BxT, + # Sampled, discrete posterior z-states and their probs (t1 to T). + "z_posterior_states_BxT": z_BxT, + "z_posterior_probs_BxT": z_posterior_probs, + # Probs of the prior z-states (t1 to T). + "z_prior_probs_BxT": z_prior_probs, + } + + def compute_posterior_z( + self, observations: "torch.Tensor", initial_h: "torch.Tensor" + ) -> "torch.Tensor": + # Fold time dimension for possible CNN pass. + shape = observations.shape + observations = observations.view((-1,) + shape[2:]) + # Compute bare encoder outputs (not including z, which is computed in next step + # with involvement of the previous output (initial_h) of the sequence model). + # encoder_outs=[B, ...] + if self.symlog_obs: + observations = symlog(observations) + encoder_out = self.encoder(observations) + # Concat encoder outs with the h-states. + posterior_mlp_input = torch.cat([encoder_out, initial_h], dim=-1) + # Compute z. + repr_input = self.posterior_mlp(posterior_mlp_input) + # Draw one z-sample (no need to return the distribution here). + z_t = self.posterior_representation_layer(repr_input, return_z_probs=False) + return z_t + + @staticmethod + def _mask(value: "torch.Tensor", mask: "torch.Tensor") -> "torch.Tensor": + return torch.einsum("b...,b->b...", value, mask) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fe7b58cf515ee0c01b83dac8b515a07489c11c91 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__init__.py @@ -0,0 +1,168 @@ +""" +Utility functions for the DreamerV3 ([1]) algorithm. + +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf +""" + +_ALLOWED_MODEL_DIMS = [ + # RLlib debug sizes (not mentioned in [1]). + "nano", + "micro", + "mini", + "XXS", + # Regular sizes (listed in table B in [1]). + "XS", + "S", + "M", + "L", + "XL", +] + + +def get_cnn_multiplier(model_size, override=None): + if override is not None: + return override + + assert model_size in _ALLOWED_MODEL_DIMS + cnn_multipliers = { + "nano": 2, + "micro": 4, + "mini": 8, + "XXS": 16, + "XS": 24, + "S": 32, + "M": 48, + "L": 64, + "XL": 96, + } + return cnn_multipliers[model_size] + + +def get_dense_hidden_units(model_size, override=None): + if override is not None: + return override + + assert model_size in _ALLOWED_MODEL_DIMS + dense_units = { + "nano": 16, + "micro": 32, + "mini": 64, + "XXS": 128, + "XS": 256, + "S": 512, + "M": 640, + "L": 768, + "XL": 1024, + } + return dense_units[model_size] + + +def get_gru_units(model_size, override=None): + if override is not None: + return override + + assert model_size in _ALLOWED_MODEL_DIMS + gru_units = { + "nano": 16, + "micro": 32, + "mini": 64, + "XXS": 128, + "XS": 256, + "S": 512, + "M": 1024, + "L": 2048, + "XL": 4096, + } + return gru_units[model_size] + + +def get_num_z_categoricals(model_size, override=None): + if override is not None: + return override + + assert model_size in _ALLOWED_MODEL_DIMS + gru_units = { + "nano": 4, + "micro": 8, + "mini": 16, + "XXS": 32, + "XS": 32, + "S": 32, + "M": 32, + "L": 32, + "XL": 32, + } + return gru_units[model_size] + + +def get_num_z_classes(model_size, override=None): + if override is not None: + return override + + assert model_size in _ALLOWED_MODEL_DIMS + gru_units = { + "nano": 4, + "micro": 8, + "mini": 16, + "XXS": 32, + "XS": 32, + "S": 32, + "M": 32, + "L": 32, + "XL": 32, + } + return gru_units[model_size] + + +def get_num_curiosity_nets(model_size, override=None): + if override is not None: + return override + + assert model_size in _ALLOWED_MODEL_DIMS + num_curiosity_nets = { + "nano": 8, + "micro": 8, + "mini": 8, + "XXS": 8, + "XS": 8, + "S": 8, + "M": 8, + "L": 8, + "XL": 8, + } + return num_curiosity_nets[model_size] + + +def get_num_dense_layers(model_size, override=None): + if override is not None: + return override + + assert model_size in _ALLOWED_MODEL_DIMS + num_dense_layers = { + "nano": 1, + "micro": 1, + "mini": 1, + "XXS": 1, + "XS": 1, + "S": 2, + "M": 3, + "L": 4, + "XL": 5, + } + return num_dense_layers[model_size] + + +def do_symlog_obs(observation_space, symlog_obs_user_setting): + # If our symlog_obs setting is NOT set specifically (it's set to "auto"), return + # True if we don't have an image observation space, otherwise return False. + + # TODO (sven): Support mixed observation spaces. + + is_image_space = len(observation_space.shape) in [2, 3] + return ( + not is_image_space + if symlog_obs_user_setting == "auto" + else symlog_obs_user_setting + ) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78a3cd829c9577b58605e878c694aae5a6f887ba Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/add_is_firsts_to_batch.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/add_is_firsts_to_batch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c35508ff09f2d7729bb63050d47ec76ab6b2c39 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/add_is_firsts_to_batch.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/debugging.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/debugging.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83f9257a3bf3d3055495b7e748b323746105bd9b Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/debugging.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/summaries.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/summaries.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..487febf04dd98b99a3360ca6ad4d6a28ab737673 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/__pycache__/summaries.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/add_is_firsts_to_batch.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/add_is_firsts_to_batch.py new file mode 100644 index 0000000000000000000000000000000000000000..3479693bf1e7d8421513be9ebb74e7e9ce29ff3d --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/add_is_firsts_to_batch.py @@ -0,0 +1,36 @@ +from typing import Any, List, Optional + +from ray.rllib.connectors.connector_v2 import ConnectorV2 +from ray.rllib.core.rl_module.rl_module import RLModule +from ray.rllib.utils.annotations import override +from ray.rllib.utils.typing import EpisodeType + + +class AddIsFirstsToBatch(ConnectorV2): + """Adds the "is_first" column to the batch.""" + + @override(ConnectorV2) + def __call__( + self, + *, + rl_module: RLModule, + batch: Optional[Any], + episodes: List[EpisodeType], + explore: Optional[bool] = None, + shared_data: Optional[dict] = None, + **kwargs, + ) -> Any: + # If "is_first" already in batch, early out. + if "is_first" in batch: + return batch + + for sa_episode in self.single_agent_episode_iterator(episodes): + self.add_batch_item( + batch, + "is_first", + item_to_add=( + 1.0 if sa_episode.t_started == 0 and len(sa_episode) == 0 else 0.0 + ), + single_agent_episode=sa_episode, + ) + return batch diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/debugging.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/debugging.py new file mode 100644 index 0000000000000000000000000000000000000000..a99d2923d4ad70315e7b8f4fbed13fcdd3ea410f --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/debugging.py @@ -0,0 +1,189 @@ +import gymnasium as gym +import numpy as np +from gymnasium.envs.classic_control.cartpole import CartPoleEnv +from PIL import Image, ImageDraw + +from ray.rllib.utils.framework import try_import_torch + +torch, _ = try_import_torch() + + +class CartPoleDebug(CartPoleEnv): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + low = np.concatenate([np.array([0.0]), self.observation_space.low]) + high = np.concatenate([np.array([1000.0]), self.observation_space.high]) + + self.observation_space = gym.spaces.Box(low, high, shape=(5,), dtype=np.float32) + + self.timesteps_ = 0 + self._next_action = 0 + self._seed = 1 + + def reset(self, *, seed=None, options=None): + ret = super().reset(seed=self._seed) + self._seed += 1 + self.timesteps_ = 0 + self._next_action = 0 + obs = np.concatenate([np.array([self.timesteps_]), ret[0]]) + return obs, ret[1] + + def step(self, action): + ret = super().step(self._next_action) + + self.timesteps_ += 1 + self._next_action = 0 if self._next_action else 1 + + obs = np.concatenate([np.array([self.timesteps_]), ret[0]]) + reward = 0.1 * self.timesteps_ + return (obs, reward) + ret[2:] + + +gym.register("CartPoleDebug-v0", CartPoleDebug) +cartpole_env = gym.make("CartPoleDebug-v0", render_mode="rgb_array") +cartpole_env.reset() + +frozenlake_env = gym.make( + "FrozenLake-v1", render_mode="rgb_array", is_slippery=False, map_name="4x4" +) # desc=["SF", "HG"]) +frozenlake_env.reset() + + +def create_cartpole_dream_image( + dreamed_obs, # real space (not symlog'd) + dreamed_V, # real space (not symlog'd) + dreamed_a, + dreamed_r_tp1, # real space (not symlog'd) + dreamed_ri_tp1, # intrinsic reward + dreamed_c_tp1, # continue flag + value_target, # real space (not symlog'd) + initial_h, + as_tensor=False, +): + # CartPoleDebug + if dreamed_obs.shape == (5,): + # Set the state of our env to the given observation. + cartpole_env.unwrapped.state = np.array(dreamed_obs[1:], dtype=np.float32) + # Normal CartPole-v1 + else: + cartpole_env.unwrapped.state = np.array(dreamed_obs, dtype=np.float32) + + # Produce an RGB-image of the current state. + rgb_array = cartpole_env.render() + + # Add value-, action-, reward-, and continue-prediction information. + image = Image.fromarray(rgb_array) + draw_obj = ImageDraw.Draw(image) + + # fnt = ImageFont.load_default(size=40) + + draw_obj.text( + (5, 6), f"Vt={dreamed_V:.2f} (Rt={value_target:.2f})", fill=(0, 0, 0) + ) # , font=fnt.font, size=30) + draw_obj.text( + (5, 18), + f"at={'<--' if dreamed_a == 0 else '-->'} ({dreamed_a})", + fill=(0, 0, 0), + ) + draw_obj.text((5, 30), f"rt+1={dreamed_r_tp1:.2f}", fill=(0, 0, 0)) + if dreamed_ri_tp1 is not None: + draw_obj.text((5, 42), f"rit+1={dreamed_ri_tp1:.6f}", fill=(0, 0, 0)) + draw_obj.text((5, 54), f"ct+1={dreamed_c_tp1}", fill=(0, 0, 0)) + draw_obj.text((5, 66), f"|h|t={np.mean(np.abs(initial_h)):.5f}", fill=(0, 0, 0)) + + if dreamed_obs.shape == (5,): + draw_obj.text((20, 100), f"t={dreamed_obs[0]}", fill=(0, 0, 0)) + + # Return image. + np_img = np.asarray(image) + if as_tensor: + return torch.from_numpy(np_img, dtype=torch.uint8) + return np_img + + +def create_frozenlake_dream_image( + dreamed_obs, # real space (not symlog'd) + dreamed_V, # real space (not symlog'd) + dreamed_a, + dreamed_r_tp1, # real space (not symlog'd) + dreamed_ri_tp1, # intrinsic reward + dreamed_c_tp1, # continue flag + value_target, # real space (not symlog'd) + initial_h, + as_tensor=False, +): + frozenlake_env.unwrapped.s = np.argmax(dreamed_obs, axis=0) + + # Produce an RGB-image of the current state. + rgb_array = frozenlake_env.render() + + # Add value-, action-, reward-, and continue-prediction information. + image = Image.fromarray(rgb_array) + draw_obj = ImageDraw.Draw(image) + + draw_obj.text((5, 6), f"Vt={dreamed_V:.2f} (Rt={value_target:.2f})", fill=(0, 0, 0)) + action_arrow = ( + "<--" + if dreamed_a == 0 + else "v" + if dreamed_a == 1 + else "-->" + if dreamed_a == 2 + else "^" + ) + draw_obj.text((5, 18), f"at={action_arrow} ({dreamed_a})", fill=(0, 0, 0)) + draw_obj.text((5, 30), f"rt+1={dreamed_r_tp1:.2f}", fill=(0, 0, 0)) + if dreamed_ri_tp1 is not None: + draw_obj.text((5, 42), f"rit+1={dreamed_ri_tp1:.6f}", fill=(0, 0, 0)) + draw_obj.text((5, 54), f"ct+1={dreamed_c_tp1}", fill=(0, 0, 0)) + draw_obj.text((5, 66), f"|h|t={np.mean(np.abs(initial_h)):.5f}", fill=(0, 0, 0)) + + # Return image. + np_img = np.asarray(image) + if as_tensor: + return torch.from_numpy(np_img, dtype=torch.uint8) + return np_img + + +if __name__ == "__main__": + # CartPole debug. + rgb_array = create_cartpole_dream_image( + dreamed_obs=np.array([100.0, 1.0, -0.01, 1.5, 0.02]), + dreamed_V=4.3, + dreamed_a=1, + dreamed_r_tp1=1.0, + dreamed_c_tp1=True, + initial_h=0.0, + value_target=8.0, + ) + # ImageFont.load("arial.pil") + image = Image.fromarray(rgb_array) + image.show() + + # Normal CartPole. + rgb_array = create_cartpole_dream_image( + dreamed_obs=np.array([1.0, -0.01, 1.5, 0.02]), + dreamed_V=4.3, + dreamed_a=1, + dreamed_r_tp1=1.0, + dreamed_c_tp1=True, + initial_h=0.1, + value_target=8.0, + ) + # ImageFont.load("arial.pil") + image = Image.fromarray(rgb_array) + image.show() + + # Frozenlake + rgb_array = create_frozenlake_dream_image( + dreamed_obs=np.array([1.0] + [0.0] * (frozenlake_env.observation_space.n - 1)), + dreamed_V=4.3, + dreamed_a=1, + dreamed_r_tp1=1.0, + dreamed_c_tp1=True, + initial_h=0.1, + value_target=8.0, + ) + image = Image.fromarray(rgb_array) + image.show() diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/summaries.py b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/summaries.py new file mode 100644 index 0000000000000000000000000000000000000000..bcd3898387f12b5b17ad42736ca338351e025755 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/dreamerv3/utils/summaries.py @@ -0,0 +1,407 @@ +""" +[1] Mastering Diverse Domains through World Models - 2023 +D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap +https://arxiv.org/pdf/2301.04104v1.pdf + +[2] Mastering Atari with Discrete World Models - 2021 +D. Hafner, T. Lillicrap, M. Norouzi, J. Ba +https://arxiv.org/pdf/2010.02193.pdf +""" +import numpy as np + +from ray.rllib.algorithms.dreamerv3.utils.debugging import ( + create_cartpole_dream_image, + create_frozenlake_dream_image, +) +from ray.rllib.core import DEFAULT_MODULE_ID +from ray.rllib.core.columns import Columns +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.metrics import ( + LEARNER_RESULTS, + REPLAY_BUFFER_RESULTS, +) +from ray.rllib.utils.torch_utils import inverse_symlog + +torch, _ = try_import_torch() + + +def reconstruct_obs_from_h_and_z( + h_t0_to_H, + z_t0_to_H, + dreamer_model, + obs_dims_shape, + framework="torch", +): + """Returns""" + shape = h_t0_to_H.shape + T = shape[0] # inputs are time-major + B = shape[1] + # Compute actual observations using h and z and the decoder net. + # Note that the last h-state (T+1) is NOT used here as it's already part of + # a new trajectory. + # Use mean() of the Gaussian, no sample! -> No need to construct dist object here. + if framework == "torch": + device = next(iter(dreamer_model.world_model.decoder.parameters())).device + reconstructed_obs_distr_means_TxB = ( + dreamer_model.world_model.decoder( + # Fold time rank. + h=torch.from_numpy(h_t0_to_H).reshape((T * B, -1)).to(device), + z=torch.from_numpy(z_t0_to_H) + .reshape((T * B,) + z_t0_to_H.shape[2:]) + .to(device), + ) + .detach() + .cpu() + .numpy() + ) + else: + reconstructed_obs_distr_means_TxB = dreamer_model.world_model.decoder( + # Fold time rank. + h=h_t0_to_H.reshape((T * B, -1)), + z=z_t0_to_H.reshape((T * B,) + z_t0_to_H.shape[2:]), + ) + + # Unfold time rank again. + reconstructed_obs_T_B = np.reshape( + reconstructed_obs_distr_means_TxB, (T, B) + obs_dims_shape + ) + # Return inverse symlog'd (real env obs space) reconstructed observations. + return reconstructed_obs_T_B + + +def report_dreamed_trajectory( + *, + results, + env, + dreamer_model, + obs_dims_shape, + batch_indices=(0,), + desc=None, + include_images=True, + framework="torch", +): + if not include_images: + return + + dream_data = results["dream_data"] + dreamed_obs_H_B = reconstruct_obs_from_h_and_z( + h_t0_to_H=dream_data["h_states_t0_to_H_BxT"], + z_t0_to_H=dream_data["z_states_prior_t0_to_H_BxT"], + dreamer_model=dreamer_model, + obs_dims_shape=obs_dims_shape, + framework=framework, + ) + func = ( + create_cartpole_dream_image + if env.startswith("CartPole") + else create_frozenlake_dream_image + ) + # Take 0th dreamed trajectory and produce series of images. + for b in batch_indices: + images = [] + for t in range(len(dreamed_obs_H_B) - 1): + images.append( + func( + dreamed_obs=dreamed_obs_H_B[t][b], + dreamed_V=dream_data["values_dreamed_t0_to_H_BxT"][t][b], + dreamed_a=(dream_data["actions_ints_dreamed_t0_to_H_BxT"][t][b]), + dreamed_r_tp1=(dream_data["rewards_dreamed_t0_to_H_BxT"][t + 1][b]), + # `DISAGREE_intrinsic_rewards_H_B` are shifted by 1 already + # (from t1 to H, not t0 to H like all other data here). + dreamed_ri_tp1=( + results["DISAGREE_intrinsic_rewards_H_BxT"][t][b] + if "DISAGREE_intrinsic_rewards_H_BxT" in results + else None + ), + dreamed_c_tp1=( + dream_data["continues_dreamed_t0_to_H_BxT"][t + 1][b] + ), + value_target=results["VALUE_TARGETS_H_BxT"][t][b], + initial_h=dream_data["h_states_t0_to_H_BxT"][t][b], + as_tensor=True, + ).numpy() + ) + # Concat images along width-axis (so they show as a "film sequence" next to each + # other). + results.update( + { + f"dreamed_trajectories{('_'+desc) if desc else ''}_B{b}": ( + np.concatenate(images, axis=1) + ), + } + ) + + +def report_predicted_vs_sampled_obs( + *, + metrics, + sample, + batch_size_B, + batch_length_T, + symlog_obs: bool = True, + do_report: bool = True, +): + """Summarizes sampled data (from the replay buffer) vs world-model predictions. + + World model predictions are based on the posterior states (z computed from actual + observation encoder input + the current h-states). + + Observations: Computes MSE (sampled vs predicted/recreated) over all features. + For image observations, also creates direct image comparisons (sampled images + vs predicted (posterior) ones). + Rewards: Compute MSE (sampled vs predicted). + Continues: Compute MSE (sampled vs predicted). + + Args: + metrics: The MetricsLogger object of the DreamerV3 algo. + sample: The sampled data (dict) from the replay buffer. Already torch-tensor + converted. + batch_size_B: The batch size (B). This is the number of trajectories sampled + from the buffer. + batch_length_T: The batch length (T). This is the length of an individual + trajectory sampled from the buffer. + do_report: Whether to actually log the report (default). If this is set to + False, this function serves as a clean-up on the given metrics, making sure + they do NOT contain anymore any (spacious) data relevant for producing + the report/videos. + """ + fwd_output_key = ( + LEARNER_RESULTS, + DEFAULT_MODULE_ID, + "WORLD_MODEL_fwd_out_obs_distribution_means_b0xT", + ) + # logged as a non-reduced item (still a list) + predicted_observation_means_single_example = metrics.peek( + fwd_output_key, default=[None] + )[-1] + metrics.delete(fwd_output_key, key_error=False) + + final_result_key = ( + f"WORLD_MODEL_sampled_vs_predicted_posterior_b0x{batch_length_T}_videos" + ) + if not do_report: + metrics.delete(final_result_key, key_error=False) + return + + _report_obs( + metrics=metrics, + computed_float_obs_B_T_dims=np.reshape( + predicted_observation_means_single_example, + # WandB videos need to be channels first. + (1, batch_length_T) + sample[Columns.OBS].shape[2:], + ), + sampled_obs_B_T_dims=sample[Columns.OBS][0:1], + metrics_key=final_result_key, + symlog_obs=symlog_obs, + ) + + +def report_dreamed_eval_trajectory_vs_samples( + *, + metrics, + sample, + burn_in_T, + dreamed_T, + dreamer_model, + symlog_obs: bool = True, + do_report: bool = True, + framework="torch", +) -> None: + """Logs dreamed observations, rewards, continues and compares them vs sampled data. + + For obs, we'll try to create videos (side-by-side comparison) of the dreamed, + recreated-from-prior obs vs the sampled ones (over dreamed_T timesteps). + + Args: + metrics: The MetricsLogger object of the DreamerV3 algo. + sample: The sampled data (dict) from the replay buffer. Already torch-tensor + converted. + burn_in_T: The number of burn-in timesteps (these will be skipped over in the + reported video comparisons and MSEs). + dreamed_T: The number of timesteps to produce dreamed data for. + dreamer_model: The DreamerModel to use to create observation vectors/images + from dreamed h- and (prior) z-states. + symlog_obs: Whether to inverse-symlog the computed observations or not. Set this + to True for environments, in which we should symlog the observations. + do_report: Whether to actually log the report (default). If this is set to + False, this function serves as a clean-up on the given metrics, making sure + they do NOT contain anymore any (spacious) data relevant for producing + the report/videos. + """ + dream_data = metrics.peek( + (LEARNER_RESULTS, DEFAULT_MODULE_ID, "dream_data"), + default={}, + ) + metrics.delete(LEARNER_RESULTS, DEFAULT_MODULE_ID, "dream_data", key_error=False) + + final_result_key_obs = f"EVALUATION_sampled_vs_dreamed_prior_H{dreamed_T}_obs" + final_result_key_rew = ( + f"EVALUATION_sampled_vs_dreamed_prior_H{dreamed_T}_rewards_MSE" + ) + final_result_key_cont = ( + f"EVALUATION_sampled_vs_dreamed_prior_H{dreamed_T}_continues_MSE" + ) + if not do_report: + metrics.delete(final_result_key_obs, key_error=False) + metrics.delete(final_result_key_rew, key_error=False) + metrics.delete(final_result_key_cont, key_error=False) + return + + # Obs MSE. + dreamed_obs_H_B = reconstruct_obs_from_h_and_z( + h_t0_to_H=dream_data["h_states_t0_to_H_Bx1"][0], # [0] b/c reduce=None (list) + z_t0_to_H=dream_data["z_states_prior_t0_to_H_Bx1"][0], + dreamer_model=dreamer_model, + obs_dims_shape=sample[Columns.OBS].shape[2:], + framework=framework, + ) + t0 = burn_in_T + tH = t0 + dreamed_T + # Observation MSE and - if applicable - images comparisons. + _report_obs( + metrics=metrics, + # WandB videos need to be 5D (B, L, c, h, w) -> transpose/swap H and B axes. + computed_float_obs_B_T_dims=np.swapaxes(dreamed_obs_H_B, 0, 1)[ + 0:1 + ], # for now: only B=1 + sampled_obs_B_T_dims=sample[Columns.OBS][0:1, t0:tH], + metrics_key=final_result_key_obs, + symlog_obs=symlog_obs, + ) + + # Reward MSE. + _report_rewards( + metrics=metrics, + computed_rewards=dream_data["rewards_dreamed_t0_to_H_Bx1"][0], + sampled_rewards=sample[Columns.REWARDS][:, t0:tH], + metrics_key=final_result_key_rew, + ) + + # Continues MSE. + _report_continues( + metrics=metrics, + computed_continues=dream_data["continues_dreamed_t0_to_H_Bx1"][0], + sampled_continues=(1.0 - sample["is_terminated"])[:, t0:tH], + metrics_key=final_result_key_cont, + ) + + +def report_sampling_and_replay_buffer(*, metrics, replay_buffer): + episodes_in_buffer = replay_buffer.get_num_episodes() + ts_in_buffer = replay_buffer.get_num_timesteps() + replayed_steps = replay_buffer.get_sampled_timesteps() + added_steps = replay_buffer.get_added_timesteps() + + # Summarize buffer, sampling, and train ratio stats. + metrics.log_dict( + { + "capacity": replay_buffer.capacity, + "size_num_episodes": episodes_in_buffer, + "size_timesteps": ts_in_buffer, + "replayed_steps": replayed_steps, + "added_steps": added_steps, + }, + key=REPLAY_BUFFER_RESULTS, + window=1, + ) # window=1 b/c these are current (total count/state) values. + + +def _report_obs( + *, + metrics, + computed_float_obs_B_T_dims, + sampled_obs_B_T_dims, + metrics_key, + symlog_obs, +): + """Summarizes computed- vs sampled observations: MSE and (if applicable) images. + + Args: + metrics: The MetricsLogger object of the DreamerV3 algo. + computed_float_obs_B_T_dims: Computed float observations + (not clipped, not cast'd). Shape=(B, T, [dims ...]). + sampled_obs_B_T_dims: Sampled observations (as-is from the environment, meaning + this could be uint8, 0-255 clipped images). Shape=(B, T, [dims ...]). + metrics_key: The metrics key (or key sequence) under which to log ths resulting + video sequence. + symlog_obs: Whether to inverse-symlog the computed observations or not. Set this + to True for environments, in which we should symlog the observations. + """ + # Videos: Create summary, comparing computed images with actual sampled ones. + # 4=[B, T, w, h] grayscale image; 5=[B, T, w, h, C] RGB image. + if len(sampled_obs_B_T_dims.shape) in [4, 5]: + # WandB videos need to be channels first. + transpose_axes = ( + (0, 1, 4, 2, 3) if len(sampled_obs_B_T_dims.shape) == 5 else (0, 3, 1, 2) + ) + + if symlog_obs: + computed_float_obs_B_T_dims = inverse_symlog(computed_float_obs_B_T_dims) + + # Restore image pixels from normalized (non-symlog'd) data. + if not symlog_obs: + computed_float_obs_B_T_dims = (computed_float_obs_B_T_dims + 1.0) * 128 + sampled_obs_B_T_dims = (sampled_obs_B_T_dims + 1.0) * 128 + sampled_obs_B_T_dims = np.clip(sampled_obs_B_T_dims, 0.0, 255.0).astype( + np.uint8 + ) + sampled_obs_B_T_dims = np.transpose(sampled_obs_B_T_dims, transpose_axes) + computed_images = np.clip(computed_float_obs_B_T_dims, 0.0, 255.0).astype( + np.uint8 + ) + computed_images = np.transpose(computed_images, transpose_axes) + # Concat sampled and computed images along the height axis (3) such that + # real images show below respective predicted ones. + # (B, T, C, h, w) + sampled_vs_computed_images = np.concatenate( + [computed_images, sampled_obs_B_T_dims], + axis=-1, # concat on width axis (looks nicer) + ) + # Add grayscale dim, if necessary. + if len(sampled_obs_B_T_dims.shape) == 2 + 2: + sampled_vs_computed_images = np.expand_dims(sampled_vs_computed_images, -1) + + metrics.log_value( + metrics_key, + sampled_vs_computed_images, + reduce=None, # No reduction, we want the obs tensor to stay in-tact. + window=1, + ) + + +def _report_rewards( + *, + metrics, + computed_rewards, + sampled_rewards, + metrics_key, +): + mse_sampled_vs_computed_rewards = np.mean( + np.square(computed_rewards - sampled_rewards) + ) + mse_sampled_vs_computed_rewards = np.mean(mse_sampled_vs_computed_rewards) + metrics.log_value( + metrics_key, + mse_sampled_vs_computed_rewards, + window=1, + ) + + +def _report_continues( + *, + metrics, + computed_continues, + sampled_continues, + metrics_key, +): + # Continue MSE. + mse_sampled_vs_computed_continues = np.mean( + np.square( + computed_continues - sampled_continues.astype(computed_continues.dtype) + ) + ) + metrics.log_value( + metrics_key, + mse_sampled_vs_computed_continues, + window=1, + ) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f81a5666eb0d5136e91db07d89af273f055ee626 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__init__.py @@ -0,0 +1,23 @@ +from ray.rllib.algorithms.impala.impala import ( + IMPALA, + Impala, + IMPALAConfig, + ImpalaConfig, +) +from ray.rllib.algorithms.impala.impala_tf_policy import ( + ImpalaTF1Policy, + ImpalaTF2Policy, +) +from ray.rllib.algorithms.impala.impala_torch_policy import ImpalaTorchPolicy + +__all__ = [ + "IMPALA", + "IMPALAConfig", + # @OldAPIStack + "ImpalaTF1Policy", + "ImpalaTF2Policy", + "ImpalaTorchPolicy", + # Deprecated names (lowercase) + "ImpalaConfig", + "Impala", +] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..006ece1038dca1a22ba21787f62bd706c4a263f4 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..992310dc8fc76bb0f9a70bb3924e8c85d5217d27 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala_learner.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala_learner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..327a91ea5fc46ce3ec376a6b10dc5a2b718d9d49 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala_learner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala_tf_policy.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala_tf_policy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f275c5048229d7834eda3f872f2b407f68b03a17 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala_tf_policy.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala_torch_policy.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala_torch_policy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68845045e98b53a4e3511de29b0dc54be2dd7c83 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/impala_torch_policy.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d76e1e96ba4cee0ab43232b270bb90131fc79d1 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/vtrace_tf.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/vtrace_tf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53d74c140189ef285dc862e5094b5a66b1a58543 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/vtrace_tf.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/vtrace_torch.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/vtrace_torch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c963d3d6ed157f24f2796fa1c8312f08d5b44c3d Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/__pycache__/vtrace_torch.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala.py new file mode 100644 index 0000000000000000000000000000000000000000..f4314f93f2c9d51b3dc6494571bf4d28b2518ac3 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala.py @@ -0,0 +1,1381 @@ +import copy +import logging +import queue +from typing import Dict, List, Optional, Set, Tuple, Type, Union + +from typing_extensions import Self + +import ray +from ray import ObjectRef +from ray._common.deprecation import DEPRECATED_VALUE, deprecation_warning +from ray.rllib import SampleBatch +from ray.rllib.algorithms.algorithm import Algorithm +from ray.rllib.algorithms.algorithm_config import AlgorithmConfig, NotProvided +from ray.rllib.connectors.learner import AddOneTsToEpisodesAndTruncate, NumpyToTensor +from ray.rllib.core import ( + COMPONENT_ENV_TO_MODULE_CONNECTOR, + COMPONENT_MODULE_TO_ENV_CONNECTOR, +) +from ray.rllib.core.learner.training_data import TrainingData +from ray.rllib.core.rl_module.rl_module import RLModuleSpec +from ray.rllib.execution.buffers.mixin_replay_buffer import MixInMultiAgentReplayBuffer +from ray.rllib.execution.learner_thread import LearnerThread +from ray.rllib.execution.multi_gpu_learner_thread import MultiGPULearnerThread +from ray.rllib.policy.policy import Policy +from ray.rllib.policy.sample_batch import concat_samples +from ray.rllib.utils.annotations import OldAPIStack, override +from ray.rllib.utils.metrics import ( + AGGREGATOR_ACTOR_RESULTS, + ALL_MODULES, + ENV_RUNNER_RESULTS, + LEARNER_GROUP, + LEARNER_RESULTS, + LEARNER_UPDATE_TIMER, + MEAN_NUM_EPISODE_LISTS_RECEIVED, + MEAN_NUM_LEARNER_GROUP_UPDATE_CALLED, + MEAN_NUM_LEARNER_RESULTS_RECEIVED, + NUM_AGENT_STEPS_SAMPLED, + NUM_AGENT_STEPS_TRAINED, + NUM_ENV_STEPS_SAMPLED, + NUM_ENV_STEPS_SAMPLED_LIFETIME, + NUM_ENV_STEPS_TRAINED, + NUM_ENV_STEPS_TRAINED_LIFETIME, + NUM_SYNCH_WORKER_WEIGHTS, + NUM_TRAINING_STEP_CALLS_SINCE_LAST_SYNCH_WORKER_WEIGHTS, + SAMPLE_TIMER, + SYNCH_WORKER_WEIGHTS_TIMER, + TIMERS, +) +from ray.rllib.utils.metrics.learner_info import LearnerInfoBuilder +from ray.rllib.utils.metrics.ray_metrics import ( + DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + TimerAndPrometheusLogger, +) +from ray.rllib.utils.replay_buffers.multi_agent_replay_buffer import ReplayMode +from ray.rllib.utils.replay_buffers.replay_buffer import _ALL_POLICIES +from ray.rllib.utils.schedules.scheduler import Scheduler +from ray.rllib.utils.typing import ( + LearningRateOrSchedule, + PolicyID, + ResultDict, + SampleBatchType, +) +from ray.util.metrics import Counter, Histogram + +logger = logging.getLogger(__name__) + + +LEARNER_RESULTS_CURR_ENTROPY_COEFF_KEY = "curr_entropy_coeff" + + +class IMPALAConfig(AlgorithmConfig): + """Defines a configuration class from which an Impala can be built. + + .. testcode:: + + from ray.rllib.algorithms.impala import IMPALAConfig + + config = ( + IMPALAConfig() + .environment("CartPole-v1") + .env_runners(num_env_runners=1) + .training(lr=0.0003, train_batch_size_per_learner=512) + .learners(num_learners=1) + ) + # Build a Algorithm object from the config and run 1 training iteration. + algo = config.build() + algo.train() + del algo + + .. testcode:: + + from ray.rllib.algorithms.impala import IMPALAConfig + from ray import tune + + config = ( + IMPALAConfig() + .environment("CartPole-v1") + .env_runners(num_env_runners=1) + .training(lr=tune.grid_search([0.0001, 0.0002]), grad_clip=20.0) + .learners(num_learners=1) + ) + # Run with tune. + tune.Tuner( + "IMPALA", + param_space=config, + run_config=tune.RunConfig(stop={"training_iteration": 1}), + ).fit() + """ + + def __init__(self, algo_class=None): + """Initializes a IMPALAConfig instance.""" + self.exploration_config = { # @OldAPIstack + # The Exploration class to use. In the simplest case, this is the name + # (str) of any class present in the `rllib.utils.exploration` package. + # You can also provide the python class directly or the full location + # of your class (e.g. "ray.rllib.utils.exploration.epsilon_greedy. + # EpsilonGreedy"). + "type": "StochasticSampling", + # Add constructor kwargs here (if any). + } + + super().__init__(algo_class=algo_class or IMPALA) + + # fmt: off + # __sphinx_doc_begin__ + + # IMPALA specific settings: + self.vtrace = True + self.vtrace_clip_rho_threshold = 1.0 + self.vtrace_clip_pg_rho_threshold = 1.0 + self.learner_queue_size = 3 + self.timeout_s_sampler_manager = 0.0 + self.timeout_s_aggregator_manager = 0.0 + self.broadcast_interval = 1 + self.num_gpu_loader_threads = 8 + + self.grad_clip = 40.0 + # Note: Only when using enable_rl_module_and_learner=True can the clipping mode + # be configured by the user. On the old API stack, RLlib will always clip by + # global_norm, no matter the value of `grad_clip_by`. + self.grad_clip_by = "global_norm" + + self.vf_loss_coeff = 0.5 + self.entropy_coeff = 0.01 + + # Override some of AlgorithmConfig's default values with IMPALA-specific values. + self.num_learners = 1 + self.num_aggregator_actors_per_learner = 0 + self.rollout_fragment_length = 50 + self.train_batch_size = 500 # @OldAPIstack + self.num_env_runners = 2 + self.lr = 0.0005 + self.min_time_s_per_iteration = 10 + # __sphinx_doc_end__ + # fmt: on + + # IMPALA takes care of its own EnvRunner (weights, connector, metrics) synching. + self._dont_auto_sync_env_runner_states = True + + # `.debugging()` + self._env_runners_only = False + self._skip_learners = False + + self.lr_schedule = None # @OldAPIStack + self.entropy_coeff_schedule = None # @OldAPIStack + self.num_multi_gpu_tower_stacks = 1 # @OldAPIstack + self.minibatch_buffer_size = 1 # @OldAPIstack + self.replay_proportion = 0.0 # @OldAPIstack + self.replay_buffer_num_slots = 0 # @OldAPIstack + self.learner_queue_timeout = 300 # @OldAPIstack + self.opt_type = "adam" # @OldAPIstack + self.decay = 0.99 # @OldAPIstack + self.momentum = 0.0 # @OldAPIstack + self.epsilon = 0.1 # @OldAPIstack + self._separate_vf_optimizer = False # @OldAPIstack + self._lr_vf = 0.0005 # @OldAPIstack + self.num_gpus = 1 # @OldAPIstack + self._tf_policy_handles_more_than_one_loss = True # @OldAPIstack + + # Deprecated settings. + self.num_aggregation_workers = DEPRECATED_VALUE + self.max_requests_in_flight_per_aggregator_worker = DEPRECATED_VALUE + + @override(AlgorithmConfig) + def training( + self, + *, + vtrace: Optional[bool] = NotProvided, + vtrace_clip_rho_threshold: Optional[float] = NotProvided, + vtrace_clip_pg_rho_threshold: Optional[float] = NotProvided, + num_gpu_loader_threads: Optional[int] = NotProvided, + num_multi_gpu_tower_stacks: Optional[int] = NotProvided, + minibatch_buffer_size: Optional[int] = NotProvided, + replay_proportion: Optional[float] = NotProvided, + replay_buffer_num_slots: Optional[int] = NotProvided, + learner_queue_size: Optional[int] = NotProvided, + learner_queue_timeout: Optional[float] = NotProvided, + timeout_s_sampler_manager: Optional[float] = NotProvided, + timeout_s_aggregator_manager: Optional[float] = NotProvided, + broadcast_interval: Optional[int] = NotProvided, + grad_clip: Optional[float] = NotProvided, + opt_type: Optional[str] = NotProvided, + lr_schedule: Optional[List[List[Union[int, float]]]] = NotProvided, + decay: Optional[float] = NotProvided, + momentum: Optional[float] = NotProvided, + epsilon: Optional[float] = NotProvided, + vf_loss_coeff: Optional[float] = NotProvided, + entropy_coeff: Optional[LearningRateOrSchedule] = NotProvided, + entropy_coeff_schedule: Optional[List[List[Union[int, float]]]] = NotProvided, + _separate_vf_optimizer: Optional[bool] = NotProvided, + _lr_vf: Optional[float] = NotProvided, + # Deprecated args. + num_aggregation_workers=DEPRECATED_VALUE, + max_requests_in_flight_per_aggregator_worker=DEPRECATED_VALUE, + **kwargs, + ) -> Self: + """Sets the training related configuration. + + Args: + vtrace: V-trace params (see vtrace_tf/torch.py). + vtrace_clip_rho_threshold: + vtrace_clip_pg_rho_threshold: + num_gpu_loader_threads: The number of GPU-loader threads (per Learner + worker), used to load incoming (CPU) batches to the GPU, if applicable. + The incoming batches are produced by each Learner's LearnerConnector + pipeline. After loading the batches on the GPU, the threads place them + on yet another queue for the Learner thread (only one per Learner + worker) to pick up and perform `forward_train/loss` computations. + num_multi_gpu_tower_stacks: For each stack of multi-GPU towers, how many + slots should we reserve for parallel data loading? Set this to >1 to + load data into GPUs in parallel. This will increase GPU memory usage + proportionally with the number of stacks. + Example: + 2 GPUs and `num_multi_gpu_tower_stacks=3`: + - One tower stack consists of 2 GPUs, each with a copy of the + model/graph. + - Each of the stacks will create 3 slots for batch data on each of its + GPUs, increasing memory requirements on each GPU by 3x. + - This enables us to preload data into these stacks while another stack + is performing gradient calculations. + minibatch_buffer_size: How many train batches should be retained for + minibatching. This conf only has an effect if `num_epochs > 1`. + replay_proportion: Set >0 to enable experience replay. Saved samples will + be replayed with a p:1 proportion to new data samples. + replay_buffer_num_slots: Number of sample batches to store for replay. + The number of transitions saved total will be + (replay_buffer_num_slots * rollout_fragment_length). + learner_queue_size: Max queue size for train batches feeding into the + learner. + learner_queue_timeout: Wait for train batches to be available in minibatch + buffer queue this many seconds. This may need to be increased e.g. when + training with a slow environment. + timeout_s_sampler_manager: The timeout for waiting for sampling results + for workers -- typically if this is too low, the manager won't be able + to retrieve ready sampling results. + timeout_s_aggregator_manager: The timeout for waiting for replay worker + results -- typically if this is too low, the manager won't be able to + retrieve ready replay requests. + broadcast_interval: Number of training step calls before weights are + broadcasted to rollout workers that are sampled during any iteration. + grad_clip: If specified, clip the global norm of gradients by this amount. + opt_type: Either "adam" or "rmsprop". + lr_schedule: Learning rate schedule. In the format of + [[timestep, lr-value], [timestep, lr-value], ...] + Intermediary timesteps will be assigned to interpolated learning rate + values. A schedule should normally start from timestep 0. + decay: Decay setting for the RMSProp optimizer, in case `opt_type=rmsprop`. + momentum: Momentum setting for the RMSProp optimizer, in case + `opt_type=rmsprop`. + epsilon: Epsilon setting for the RMSProp optimizer, in case + `opt_type=rmsprop`. + vf_loss_coeff: Coefficient for the value function term in the loss function. + entropy_coeff: Coefficient for the entropy regularizer term in the loss + function. + entropy_coeff_schedule: Decay schedule for the entropy regularizer. + _separate_vf_optimizer: Set this to true to have two separate optimizers + optimize the policy-and value networks. Only supported for some + algorithms (APPO, IMPALA) on the old API stack. + _lr_vf: If _separate_vf_optimizer is True, define separate learning rate + for the value network. + + Returns: + This updated AlgorithmConfig object. + """ + if num_aggregation_workers != DEPRECATED_VALUE: + deprecation_warning( + old="config.training(num_aggregation_workers=..)", + help="Aggregator workers are no longer supported on the old API " + "stack! To use aggregation (and GPU pre-loading) on the new API " + "stack, activate the new API stack, then set " + "`config.learners(num_aggregator_actors_per_learner=..)`. Good " + "choices are normally 1 or 2, but this depends on your overall " + "setup, especially your `EnvRunner` throughput.", + error=True, + ) + if max_requests_in_flight_per_aggregator_worker != DEPRECATED_VALUE: + deprecation_warning( + old="config.training(max_requests_in_flight_per_aggregator_worker=..)", + help="Aggregator workers are no longer supported on the old API " + "stack! To use aggregation (and GPU pre-loading) on the new API " + "stack, activate the new API stack and THEN set " + "`config.learners(max_requests_in_flight_per_aggregator_actor=..)" + "`.", + error=True, + ) + + # Pass kwargs onto super's `training()` method. + super().training(**kwargs) + + if vtrace is not NotProvided: + self.vtrace = vtrace + if vtrace_clip_rho_threshold is not NotProvided: + self.vtrace_clip_rho_threshold = vtrace_clip_rho_threshold + if vtrace_clip_pg_rho_threshold is not NotProvided: + self.vtrace_clip_pg_rho_threshold = vtrace_clip_pg_rho_threshold + if num_gpu_loader_threads is not NotProvided: + self.num_gpu_loader_threads = num_gpu_loader_threads + if num_multi_gpu_tower_stacks is not NotProvided: + self.num_multi_gpu_tower_stacks = num_multi_gpu_tower_stacks + if minibatch_buffer_size is not NotProvided: + self.minibatch_buffer_size = minibatch_buffer_size + if replay_proportion is not NotProvided: + self.replay_proportion = replay_proportion + if replay_buffer_num_slots is not NotProvided: + self.replay_buffer_num_slots = replay_buffer_num_slots + if learner_queue_size is not NotProvided: + self.learner_queue_size = learner_queue_size + if learner_queue_timeout is not NotProvided: + self.learner_queue_timeout = learner_queue_timeout + if broadcast_interval is not NotProvided: + self.broadcast_interval = broadcast_interval + if timeout_s_sampler_manager is not NotProvided: + self.timeout_s_sampler_manager = timeout_s_sampler_manager + if timeout_s_aggregator_manager is not NotProvided: + self.timeout_s_aggregator_manager = timeout_s_aggregator_manager + if grad_clip is not NotProvided: + self.grad_clip = grad_clip + if opt_type is not NotProvided: + self.opt_type = opt_type + if lr_schedule is not NotProvided: + self.lr_schedule = lr_schedule + if decay is not NotProvided: + self.decay = decay + if momentum is not NotProvided: + self.momentum = momentum + if epsilon is not NotProvided: + self.epsilon = epsilon + if vf_loss_coeff is not NotProvided: + self.vf_loss_coeff = vf_loss_coeff + if entropy_coeff is not NotProvided: + self.entropy_coeff = entropy_coeff + if entropy_coeff_schedule is not NotProvided: + self.entropy_coeff_schedule = entropy_coeff_schedule + if _separate_vf_optimizer is not NotProvided: + self._separate_vf_optimizer = _separate_vf_optimizer + if _lr_vf is not NotProvided: + self._lr_vf = _lr_vf + + return self + + def debugging( + self, + *, + _env_runners_only: Optional[bool] = NotProvided, + _skip_learners: Optional[bool] = NotProvided, + **kwargs, + ) -> Self: + """Sets the debugging related configuration. + + Args: + _env_runners_only: If True, only run (remote) EnvRunner requests, discard + their episode/training data, but log their metrics results. Aggregator- + and Learner actors won't be used. + _skip_learners: If True, no `update` requests are sent to the LearnerGroup + and Learner actors. Only EnvRunners and aggregator actors (if + applicable) are used. + """ + super().debugging(**kwargs) + + if _env_runners_only is not NotProvided: + self._env_runners_only = _env_runners_only + if _skip_learners is not NotProvided: + self._skip_learners = _skip_learners + + return self + + @override(AlgorithmConfig) + def validate(self) -> None: + # Call the super class' validation method first. + super().validate() + + # IMPALA and APPO need vtrace (A3C Policies no longer exist). + if not self.vtrace: + self._value_error( + "IMPALA and APPO do NOT support vtrace=False anymore! Set " + "`config.training(vtrace=True)`." + ) + + # New API stack checks. + if self.enable_env_runner_and_connector_v2: + # Does NOT support aggregation workers yet or a mixin replay buffer. + if self.replay_ratio != 0.0: + self._value_error( + "The new API stack in combination with the new EnvRunner API " + "does NOT support a mixin replay buffer yet for " + f"{self} (set `config.replay_proportion` to 0.0)!" + ) + # `lr_schedule` checking. + if self.lr_schedule is not None: + self._value_error( + "`lr_schedule` is deprecated and must be None! Use the " + "`lr` setting to setup a schedule." + ) + # Entropy coeff schedule checking. + if self.entropy_coeff_schedule is not None: + self._value_error( + "`entropy_coeff_schedule` is deprecated and must be None! Use the " + "`entropy_coeff` setting to setup a schedule." + ) + Scheduler.validate( + fixed_value_or_schedule=self.entropy_coeff, + setting_name="entropy_coeff", + description="entropy coefficient", + ) + if self.minibatch_size is not None and not ( + (self.minibatch_size % self.rollout_fragment_length == 0) + and self.minibatch_size <= self.total_train_batch_size + ): + self._value_error( + f"`minibatch_size` ({self.minibatch_size}) must either be None " + "or a multiple of `rollout_fragment_length` " + f"({self.rollout_fragment_length}) while at the same time smaller " + "than or equal to `total_train_batch_size` " + f"({self.total_train_batch_size})!" + ) + # Old API stack checks. + else: + if isinstance(self.entropy_coeff, float) and self.entropy_coeff < 0.0: + self._value_error("`entropy_coeff` must be >= 0.0") + + # If two separate optimizers/loss terms used for tf, must also set + # `_tf_policy_handles_more_than_one_loss` to True. + if ( + self.framework_str in ["tf", "tf2"] + and self._separate_vf_optimizer is True + and self._tf_policy_handles_more_than_one_loss is False + ): + self._value_error( + "`_tf_policy_handles_more_than_one_loss` must be set to True, for " + "TFPolicy to support more than one loss term/optimizer! Try setting " + "config.training(_tf_policy_handles_more_than_one_loss=True)." + ) + + @property + def replay_ratio(self) -> float: + """Returns replay ratio (between 0.0 and 1.0) based off self.replay_proportion. + + Formula: ratio = 1 / proportion + """ + return (1 / self.replay_proportion) if self.replay_proportion > 0 else 0.0 + + @override(AlgorithmConfig) + def get_default_learner_class(self): + if self.framework_str == "torch": + from ray.rllib.algorithms.impala.torch.impala_torch_learner import ( + IMPALATorchLearner, + ) + + return IMPALATorchLearner + elif self.framework_str in ["tf2", "tf"]: + raise ValueError( + "TensorFlow is no longer supported on the new API stack! " + "Use `framework='torch'`." + ) + else: + raise ValueError( + f"The framework {self.framework_str} is not supported. " + "Use `framework='torch'`." + ) + + @override(AlgorithmConfig) + def get_default_rl_module_spec(self) -> RLModuleSpec: + if self.framework_str == "torch": + from ray.rllib.algorithms.ppo.torch.default_ppo_torch_rl_module import ( + DefaultPPOTorchRLModule, + ) + + return RLModuleSpec(module_class=DefaultPPOTorchRLModule) + else: + raise ValueError( + f"The framework {self.framework_str} is not supported. " + "Use either 'torch' or 'tf2'." + ) + + @override(AlgorithmConfig) + def build_learner_connector( + self, + input_observation_space, + input_action_space, + device=None, + ): + connector = super().build_learner_connector( + input_observation_space, + input_action_space, + device, + ) + if self.add_default_connectors_to_learner_pipeline: + # Extend all episodes by one artificial timestep to allow the value function + # net to compute the bootstrap values (and add a mask to the batch to know, + # which slots to mask out). + connector.prepend(AddOneTsToEpisodesAndTruncate()) + # Remove the NumpyToTensor connector if we have the GPULoaderThreads. + if self.num_aggregator_actors_per_learner > 0: + connector.remove(NumpyToTensor) + return connector + + +ImpalaConfig = IMPALAConfig + + +class IMPALA(Algorithm): + """Importance weighted actor/learner architecture (IMPALA) Algorithm + + == Overview of data flow in IMPALA == + 1. Policy evaluation in parallel across `num_env_runners` actors produces + batches of size `rollout_fragment_length * num_envs_per_env_runner`. + 2. If enabled, the replay buffer stores and produces batches of size + `rollout_fragment_length * num_envs_per_env_runner`. + 3. If enabled, the minibatch ring buffer stores and replays batches of + size `train_batch_size` up to `num_epochs` times per batch. + 4. The learner thread executes data parallel SGD across `num_gpus` GPUs + on batches of size `train_batch_size`. + """ + + @classmethod + @override(Algorithm) + def get_default_config(cls) -> IMPALAConfig: + return IMPALAConfig() + + @classmethod + @override(Algorithm) + def get_default_policy_class( + cls, config: AlgorithmConfig + ) -> Optional[Type[Policy]]: + if config.framework_str == "torch": + from ray.rllib.algorithms.impala.impala_torch_policy import ( + ImpalaTorchPolicy, + ) + + return ImpalaTorchPolicy + + elif config.framework_str == "tf": + from ray.rllib.algorithms.impala.impala_tf_policy import ( + ImpalaTF1Policy, + ) + + return ImpalaTF1Policy + else: + from ray.rllib.algorithms.impala.impala_tf_policy import ( + ImpalaTF2Policy, + ) + + return ImpalaTF2Policy + + @override(Algorithm) + def setup(self, config: AlgorithmConfig): + super().setup(config) + + # Queue of data to be sent to the Learner. + self.data_to_place_on_learner = [] + self.local_mixin_buffer = None # @OldAPIStack + self._batch_being_built = [] # @OldAPIStack + + # Create extra aggregation workers and assign each rollout worker to one of + # them. + self._episode_packs_being_built = [] + self._ma_batches_being_built: Dict[int, list] = { + i: [] for i in range(self.config.num_learners or 1) + } + + # Create local mixin buffer if on old API stack and replay + # proportion is set. + if not self.config.enable_rl_module_and_learner: + if self.config.replay_proportion > 0.0: + self.local_mixin_buffer = MixInMultiAgentReplayBuffer( + capacity=( + self.config.replay_buffer_num_slots + if self.config.replay_buffer_num_slots > 0 + else 1 + ), + replay_ratio=self.config.replay_ratio, + replay_mode=ReplayMode.LOCKSTEP, + ) + + # This variable is used to keep track of the statistics from the most recent + # update of the learner group + self._results = {} + + if not self.config.enable_rl_module_and_learner: + # Create and start the learner thread. + self._learner_thread = make_learner_thread(self.env_runner, self.config) + self._learner_thread.start() + + @override(Algorithm) + def training_step(self): + with TimerAndPrometheusLogger(self._metrics_impala_training_step_time): + # Old API stack. + if not self.config.enable_rl_module_and_learner: + return self._training_step_old_api_stack() + + do_async_updates = self.config.num_learners > 0 + + # Asynchronously request all EnvRunners to sample and return their current + # (e.g. ConnectorV2) states and sampling metrics/stats. + # Note that each item in `episode_refs` is a reference to a list of Episodes. + with self.metrics.log_time((TIMERS, SAMPLE_TIMER)): + ( + episode_refs, + connector_states, + env_runner_metrics, + env_runner_indices_to_update, + ) = self._sample_and_get_connector_states() + # Reduce EnvRunner metrics over the n EnvRunners. + self.metrics.aggregate( + env_runner_metrics, + key=ENV_RUNNER_RESULTS, + ) + + # Log the average number of sample results (list of episodes) received. + self.metrics.log_value( + (ENV_RUNNER_RESULTS, MEAN_NUM_EPISODE_LISTS_RECEIVED), + len(episode_refs), + ) + + # Only run EnvRunners, nothing else. + if self.config._env_runners_only: + return + + # "Batch" collected episode refs into groups, such that exactly + # `total_train_batch_size` timesteps are sent to + # `LearnerGroup.update()`. + if self.config.num_aggregator_actors_per_learner > 0: + with TimerAndPrometheusLogger( + self._metrics_impala_training_step_aggregator_preprocessing_time + ): + data_packages_for_aggregators = self._pre_queue_episode_refs( + episode_refs, + package_size=self.config.train_batch_size_per_learner, + ) + self.metrics.log_value( + (AGGREGATOR_ACTOR_RESULTS, "mean_num_input_packages"), + len(episode_refs), + ) + + ma_batches_refs_remote_results = ( + self._aggregator_actor_manager.fetch_ready_async_reqs( + return_obj_refs=True, + tags="get_batches", + ) + ) + ma_batches_refs = [] + for call_result in ma_batches_refs_remote_results: + ma_batches_refs.append( + (call_result.actor_id, call_result.get()) + ) + self.metrics.log_value( + (AGGREGATOR_ACTOR_RESULTS, "mean_num_output_batches"), + len(ma_batches_refs), + ) + + while data_packages_for_aggregators: + num_agg = self.config.num_aggregator_actors_per_learner * ( + self.config.num_learners or 1 + ) + packs, data_packages_for_aggregators = ( + data_packages_for_aggregators[:num_agg], + data_packages_for_aggregators[num_agg:], + ) + sent = self._aggregator_actor_manager.foreach_actor_async( + func="get_batch", + kwargs=[dict(episode_refs=p) for p in packs], + tag="get_batches", + ) + + _dropped = self.config.train_batch_size_per_learner * ( + len(packs) - sent + ) + if _dropped > 0: + self._metrics_impala_training_step_env_steps_dropped.inc( + value=_dropped + ) + self.metrics.log_value( + ( + AGGREGATOR_ACTOR_RESULTS, + "num_env_steps_dropped_lifetime", + ), + _dropped, + reduce="sum", + ) + # Get n lists of m ObjRef[MABatch] (m=num_learners) to perform n calls to + # all learner workers with the already GPU-located batches. + data_packages_for_learner_group = self._pre_queue_batch_refs( + ma_batches_refs + ) + if len(data_packages_for_learner_group) > 0: + self._metrics_impala_training_step_input_batches.inc( + value=len(data_packages_for_learner_group) + ) + else: + self._metrics_impala_training_step_zero_input_batches.inc( + value=1 + ) + + self.metrics.log_value( + (AGGREGATOR_ACTOR_RESULTS, "num_env_steps_aggregated_lifetime"), + self.config.train_batch_size_per_learner + * (self.config.num_learners or 1) + * len(data_packages_for_learner_group), + reduce="sum", + with_throughput=True, + ) + + else: + data_packages_for_learner_group = self._pre_queue_episode_refs( + episode_refs, package_size=self.config.total_train_batch_size + ) + + # Skip Learner update calls. + if self.config._skip_learners: + return + + # Call the LearnerGroup's `update()` method. + with self.metrics.log_time((TIMERS, LEARNER_UPDATE_TIMER)): + self.metrics.log_value( + key=MEAN_NUM_LEARNER_GROUP_UPDATE_CALLED, + value=len(data_packages_for_learner_group), + ) + rl_module_state = None + num_learner_group_results_received = 0 + + return_state = ( + self.metrics.peek( + NUM_TRAINING_STEP_CALLS_SINCE_LAST_SYNCH_WORKER_WEIGHTS, + default=0, + ) + >= self.config.broadcast_interval + ) + with TimerAndPrometheusLogger( + self._metrics_impala_training_step_learner_group_loop_time + ): + for ( + batch_ref_or_episode_list_ref + ) in data_packages_for_learner_group: + timesteps = { + NUM_ENV_STEPS_SAMPLED_LIFETIME: self.metrics.peek( + (ENV_RUNNER_RESULTS, NUM_ENV_STEPS_SAMPLED_LIFETIME), + default=0, + ), + NUM_ENV_STEPS_TRAINED_LIFETIME: self.metrics.peek( + ( + LEARNER_RESULTS, + ALL_MODULES, + NUM_ENV_STEPS_TRAINED_LIFETIME, + ), + default=0, + ), + } + # Update from batch refs coming from AggregatorActors. + if self.config.num_aggregator_actors_per_learner > 0: + assert len(batch_ref_or_episode_list_ref) == ( + self.config.num_learners or 1 + ) + training_data = TrainingData( + batch_refs=batch_ref_or_episode_list_ref + ) + # Update from episodes refs coming from EnvRunner actors. + else: + training_data = TrainingData( + episodes_refs=batch_ref_or_episode_list_ref + ) + learner_results = self.learner_group.update( + training_data=training_data, + async_update=do_async_updates, + return_state=return_state, + timesteps=timesteps, + num_epochs=self.config.num_epochs, + minibatch_size=self.config.minibatch_size, + shuffle_batch_per_epoch=self.config.shuffle_batch_per_epoch, + ) + # Only request weights from 1st Learner - at most - once per + # `training_step` call. + return_state = False + + num_learner_group_results_received += len(learner_results) + # Extract the last (most recent) weights matrix, if available. + for result_from_1_learner in learner_results: + rl_module_state = result_from_1_learner.pop( + "_rl_module_state_after_update", rl_module_state + ) + self.metrics.aggregate( + stats_dicts=learner_results, + key=LEARNER_RESULTS, + ) + self.metrics.log_value( + key=(LEARNER_GROUP, MEAN_NUM_LEARNER_RESULTS_RECEIVED), + value=num_learner_group_results_received, + ) + + self.metrics.log_value( + NUM_TRAINING_STEP_CALLS_SINCE_LAST_SYNCH_WORKER_WEIGHTS, 1, reduce="sum" + ) + + # Update LearnerGroup's own stats. + self.metrics.log_dict(self.learner_group.get_stats(), key=LEARNER_GROUP) + + # Figure out, whether we should sync/broadcast the (remote) EnvRunner states. + # Note: `learner_results` is a List of n (num async calls) Lists of m + # (num Learner workers) ResultDicts each. + if rl_module_state is not None: + self.metrics.set_value( + NUM_TRAINING_STEP_CALLS_SINCE_LAST_SYNCH_WORKER_WEIGHTS, 0 + ) + self.metrics.log_value(NUM_SYNCH_WORKER_WEIGHTS, 1, reduce="sum") + with self.metrics.log_time((TIMERS, SYNCH_WORKER_WEIGHTS_TIMER)): + with TimerAndPrometheusLogger( + self._metrics_impala_training_step_sync_env_runner_state_time + ): + self.env_runner_group.sync_env_runner_states( + config=self.config, + connector_states=connector_states, + rl_module_state=rl_module_state, + env_to_module=self.env_to_module_connector, + module_to_env=self.module_to_env_connector, + ) + + def _sample_and_get_connector_states(self): + with TimerAndPrometheusLogger( + self._metrics_impala_sample_and_get_connector_states_time + ): + env_runner_indices_to_update = set() + episode_refs = [] + connector_states = [] + env_runner_metrics = [] + num_healthy_remote_workers = ( + self.env_runner_group.num_healthy_remote_workers() + ) + + # Perform asynchronous sampling on all (healthy) remote rollout workers. + if num_healthy_remote_workers > 0: + async_results = ( + self.env_runner_group.foreach_env_runner_async_fetch_ready( + func="sample_get_state_and_metrics", + tag="sample_get_state_and_metrics", + timeout_seconds=self.config.timeout_s_sampler_manager, + return_obj_refs=False, + return_actor_ids=True, + ) + ) + # Get results from the n different async calls and store those EnvRunner + # indices we should update. + results = [] + for r in async_results: + env_runner_indices_to_update.add(r[0]) + results.append(r[1]) + + for (episodes, states, metrics) in results: + episode_refs.append(episodes) + connector_states.append(states) + env_runner_metrics.append(metrics) + # Sample from the local EnvRunner. + else: + episodes = self.env_runner.sample() + env_runner_metrics = [self.env_runner.get_metrics()] + episode_refs = [ray.put(episodes)] + connector_states = [ + self.env_runner.get_state( + components=[ + COMPONENT_ENV_TO_MODULE_CONNECTOR, + COMPONENT_MODULE_TO_ENV_CONNECTOR, + ] + ) + ] + + return ( + episode_refs, + connector_states, + env_runner_metrics, + env_runner_indices_to_update, + ) + + def _pre_queue_episode_refs( + self, episode_refs: List[ObjectRef], package_size: int + ) -> List[List[ObjectRef]]: + # Each element in this list is itself a list of ObjRef[Episodes]. + # Each ObjRef was returned by one EnvRunner from a single sample() call. + episodes: List[List[ObjectRef]] = [] + + for ref in episode_refs: + self._episode_packs_being_built.append(ref) + if ( + len(self._episode_packs_being_built) + * self.config.num_envs_per_env_runner + * self.config.get_rollout_fragment_length() + >= package_size + ): + episodes.append(self._episode_packs_being_built) + self._episode_packs_being_built = [] + + return episodes + + def _pre_queue_batch_refs( + self, batch_refs: List[Tuple[int, ObjectRef]] + ) -> List[List[ObjectRef]]: + # `batch_refs` is a list of tuple(aggregator_actor_id, ObjRef[MABatch]). + + # Each ObjRef[MABatch] was returned by one AggregatorActor from a single + # `get_batch()` call. + # TODO (sven): Add this comment, once valid: + # .. and the underlying MABatch is already located on a particular GPU + # (matching one particular Learner). + for agg_actor_id, ma_batch_ref in batch_refs: + learner_actor_id = self._aggregator_actor_to_learner[agg_actor_id] + self._ma_batches_being_built[learner_actor_id].append(ma_batch_ref) + + # Construct an n-group of batches (n=num_learners) as long as we still have + # at least one batch per learner in our queue. + batch_refs_for_learner_group: List[List[ObjectRef]] = [] + while all( + learner_list for learner_list in self._ma_batches_being_built.values() + ): + batch_refs_for_learner_group.append( + [ + learner_list.pop(0) + for learner_list in self._ma_batches_being_built.values() + ] + ) + + return batch_refs_for_learner_group + + @override(Algorithm) + def _set_up_metrics(self): + super()._set_up_metrics() + + self._metrics_impala_training_step_time = Histogram( + name="rllib_algorithms_impala_training_step_time", + description="Time spent in IMPALA.training_step()", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_impala_training_step_time.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + self._metrics_impala_training_step_aggregator_preprocessing_time = Histogram( + name="rllib_algorithms_impala_training_step_aggregator_preprocessing_time", + description="Time spent preprocessing episodes with aggregator actor in the IMPALA.training_step()", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_impala_training_step_aggregator_preprocessing_time.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + self._metrics_impala_training_step_learner_group_loop_time = Histogram( + name="rllib_algorithms_impala_training_step_learner_group_loop_time", + description="Time spent in the learner group update calls loop, in the IMPALA.training_step()", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_impala_training_step_learner_group_loop_time.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + self._metrics_impala_training_step_sync_env_runner_state_time = Histogram( + name="rllib_algorithms_impala_training_step_sync_env_runner_state_time", + description="Time spent on syncing EnvRunner states in the IMPALA.training_step()", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_impala_training_step_sync_env_runner_state_time.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + self._metrics_impala_sample_and_get_connector_states_time = Histogram( + name="rllib_algorithms_impala_sample_and_get_connector_states_time", + description="Time spent in IMPALA._sample_and_get_connector_states()", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_impala_sample_and_get_connector_states_time.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + self._metrics_impala_training_step_input_batches = Counter( + name="rllib_algorithms_impala_training_step_input_batches_counter", + description="Number of input batches processed and passed to the learner in the IMPALA.training_step()", + tag_keys=("rllib",), + ) + self._metrics_impala_training_step_input_batches.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + self._metrics_impala_training_step_zero_input_batches = Counter( + name="rllib_algorithms_impala_training_step_zero_input_batches_counter", + description="Number of times zero input batches were ready in the IMPALA.training_step()", + tag_keys=("rllib",), + ) + self._metrics_impala_training_step_zero_input_batches.set_default_tags( + {"rllib": self.__class__.__name__} + ) + self._metrics_impala_training_step_env_steps_dropped = Counter( + name="rllib_algorithms_impala_training_step_env_steps_dropped_counter", + description="Number of env steps dropped when sending data to the aggregator actors in the IMPALA.training_step()", + tag_keys=("rllib",), + ) + self._metrics_impala_training_step_env_steps_dropped.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + @OldAPIStack + def _training_step_old_api_stack(self): + # First, check, whether our learner thread is still healthy. + if not self._learner_thread.is_alive(): + raise RuntimeError("The learner thread died while training!") + + # Get sampled SampleBatches from our workers (by ray references if we use + # tree-aggregation). + unprocessed_sample_batches = self._get_samples_from_workers_old_api_stack( + return_object_refs=False, + ) + # Tag workers that actually produced ready sample batches this iteration. + # Those workers will have to get updated at the end of the iteration. + workers_that_need_updates = { + worker_id for worker_id, _ in unprocessed_sample_batches + } + + # Resolve collected batches here on local process (using the mixin buffer). + batches = self._process_experiences_old_api_stack(unprocessed_sample_batches) + + # Increase sampling counters now that we have the actual SampleBatches on + # the local process (and can measure their sizes). + for batch in batches: + self._counters[NUM_ENV_STEPS_SAMPLED] += batch.count + self._counters[NUM_AGENT_STEPS_SAMPLED] += batch.agent_steps() + + # Concatenate single batches into batches of size `total_train_batch_size`. + self._concatenate_batches_and_pre_queue(batches) + # Move train batches (of size `total_train_batch_size`) onto learner queue. + self._place_processed_samples_on_learner_thread_queue() + # Extract most recent train results from learner thread. + train_results = self._process_trained_results() + + # Sync worker weights (only those policies that were actually updated). + with self._timers[SYNCH_WORKER_WEIGHTS_TIMER]: + pids = list(train_results.keys()) + self._update_workers_old_api_stack( + workers_that_need_updates=workers_that_need_updates, + policy_ids=pids, + ) + + # With a training step done, try to bring any aggregators back to life + # if necessary. + # AggregatorActor are stateless, so we do not need to restore any + # state here. + if self._aggregator_actor_manager: + self._aggregator_actor_manager.probe_unhealthy_actors( + timeout_seconds=self.config.env_runner_health_probe_timeout_s, + mark_healthy=True, + ) + + return train_results + + @OldAPIStack + def _get_samples_from_workers_old_api_stack( + self, + return_object_refs: Optional[bool] = False, + ) -> List[Tuple[int, Union[ObjectRef, SampleBatchType]]]: + """Get samples from rollout workers for training. + + Args: + return_object_refs: If True, return ObjectRefs instead of the samples + directly. This is useful when using aggregator workers so that data + collected on rollout workers is directly de referenced on the aggregator + workers instead of first in the driver and then on the aggregator + workers. + + Returns: + a list of tuples of (worker_index, sample batch or ObjectRef to a sample + batch) + + """ + with self._timers[SAMPLE_TIMER]: + # Sample from healthy remote workers by default. If there is no healthy + # worker (either because they have all died, or because there was none to + # begin) check if the local_worker exists. If the local worker has an + # env_instance (either because there are no remote workers or + # self.config.create_local_env_runner == True), then sample from the + # local worker. Otherwise just return an empty list. + if self.env_runner_group.num_healthy_remote_workers() > 0: + # Perform asynchronous sampling on all (remote) rollout workers. + self.env_runner_group.foreach_env_runner_async( + lambda worker: worker.sample() + ) + sample_batches: List[ + Tuple[int, ObjectRef] + ] = self.env_runner_group.fetch_ready_async_reqs( + timeout_seconds=self.config.timeout_s_sampler_manager, + return_obj_refs=return_object_refs, + ) + elif self.config.num_env_runners == 0 or ( + self.env_runner and self.env_runner.async_env is not None + ): + # Sampling from the local worker + sample_batch = self.env_runner.sample() + if return_object_refs: + sample_batch = ray.put(sample_batch) + sample_batches = [(0, sample_batch)] + else: + # Not much we can do. Return empty list and wait. + sample_batches = [] + + return sample_batches + + @OldAPIStack + def _process_experiences_old_api_stack( + self, + worker_to_sample_batches: List[Tuple[int, SampleBatch]], + ) -> List[SampleBatchType]: + """Process sample batches directly on the driver, for training. + + Args: + worker_to_sample_batches: List of (worker_id, sample_batch) tuples. + + Returns: + Batches that have been processed by the mixin buffer. + + """ + batches = [b for _, b in worker_to_sample_batches] + processed_batches = [] + + for batch in batches: + assert not isinstance( + batch, ObjectRef + ), "`IMPALA._process_experiences_old_api_stack` can not handle ObjectRefs!" + batch = batch.decompress_if_needed() + # Only make a pass through the buffer, if replay proportion is > 0.0 (and + # we actually have one). + if self.local_mixin_buffer: + self.local_mixin_buffer.add(batch) + batch = self.local_mixin_buffer.replay(_ALL_POLICIES) + else: + # TODO(jjyao) somehow deep copy the batch + # fix a memory leak issue. Need to investigate more + # to know why. + batch = batch.copy() + if batch: + processed_batches.append(batch) + + return processed_batches + + @OldAPIStack + def _concatenate_batches_and_pre_queue(self, batches: List[SampleBatch]) -> None: + """Concatenate batches that are being returned from rollout workers + + Args: + batches: List of batches of experiences from EnvRunners. + """ + + def aggregate_into_larger_batch(): + if ( + sum(b.count for b in self._batch_being_built) + >= self.config.total_train_batch_size + ): + batch_to_add = concat_samples(self._batch_being_built) + self.data_to_place_on_learner.append(batch_to_add) + self._batch_being_built = [] + + for batch in batches: + # TODO (sven): Strange bug after a RolloutWorker crash and proper + # restart. The bug is related to (old, non-V2) connectors being used and + # seems to happen inside the AgentCollector's `add_action_reward_next_obs` + # method, at the end of which the number of vf_preds (and all other + # extra action outs) in the batch is one smaller than the number of obs/ + # actions/rewards, which then leads to a malformed train batch. + # IMPALA/APPO crash inside the loss function (during v-trace operations) + # b/c of the resulting shape mismatch. The following if-block prevents + # this from happening and it can be removed once we are on the new API + # stack for good (and use the new connectors and also no longer + # AgentCollectors, RolloutWorkers, Policies, TrajectoryView API, etc..): + if ( + self.config.batch_mode == "truncate_episodes" + and self.config.restart_failed_env_runners + ): + if any( + SampleBatch.VF_PREDS in pb + and ( + pb[SampleBatch.VF_PREDS].shape[0] + != pb[SampleBatch.REWARDS].shape[0] + ) + for pb in batch.policy_batches.values() + ): + continue + + self._batch_being_built.append(batch) + aggregate_into_larger_batch() + + @OldAPIStack + def _place_processed_samples_on_learner_thread_queue(self) -> None: + """Place processed samples on the learner queue for training.""" + for i, batch in enumerate(self.data_to_place_on_learner): + try: + self._learner_thread.inqueue.put( + batch, + # Setting block = True for the very last item in our list prevents + # the learner thread, this main thread, and the GPU loader threads + # from thrashing when there are more samples than the learner can + # reasonably process. + # see https://github.com/ray-project/ray/pull/26581#issuecomment-1187877674 # noqa + block=i == len(self.data_to_place_on_learner) - 1, + ) + self._counters["num_samples_added_to_queue"] += ( + batch.agent_steps() + if self.config.count_steps_by == "agent_steps" + else batch.count + ) + except queue.Full: + self._counters["num_times_learner_queue_full"] += 1 + + self.data_to_place_on_learner.clear() + + @OldAPIStack + def _process_trained_results(self) -> ResultDict: + """Process training results that are outputed by the learner thread. + + Returns: + Aggregated results from the learner thread after an update is completed. + + """ + # Get learner outputs/stats from output queue. + num_env_steps_trained = 0 + num_agent_steps_trained = 0 + learner_infos = [] + # Loop through output queue and update our counts. + for _ in range(self._learner_thread.outqueue.qsize()): + ( + env_steps, + agent_steps, + learner_results, + ) = self._learner_thread.outqueue.get(timeout=0.001) + + num_env_steps_trained += env_steps + num_agent_steps_trained += agent_steps + if learner_results: + learner_infos.append(learner_results) + # Nothing new happened since last time, use the same learner stats. + if not learner_infos: + final_learner_info = copy.deepcopy(self._learner_thread.learner_info) + # Accumulate learner stats using the `LearnerInfoBuilder` utility. + else: + builder = LearnerInfoBuilder() + for info in learner_infos: + builder.add_learn_on_batch_results_multi_agent(info) + final_learner_info = builder.finalize() + + # Update the steps trained counters. + self._counters[NUM_ENV_STEPS_TRAINED] += num_env_steps_trained + self._counters[NUM_AGENT_STEPS_TRAINED] += num_agent_steps_trained + + return final_learner_info + + @OldAPIStack + def _update_workers_old_api_stack( + self, + workers_that_need_updates: Set[int], + policy_ids: Optional[List[PolicyID]] = None, + ) -> None: + """Updates all RolloutWorkers that require updating. + + Updates only if NUM_TRAINING_STEP_CALLS_SINCE_LAST_SYNCH_WORKER_WEIGHTS has been + reached and the worker has sent samples in this iteration. Also only updates + those policies, whose IDs are given via `policies` (if None, update all + policies). + + Args: + workers_that_need_updates: Set of worker IDs that need to be updated. + policy_ids: Optional list of Policy IDs to update. If None, will update all + policies on the to-be-updated workers. + """ + # Update global vars of the local worker. + if self.config.policy_states_are_swappable: + self.env_runner.lock() + global_vars = { + "timestep": self._counters[NUM_AGENT_STEPS_TRAINED], + "num_grad_updates_per_policy": { + pid: self.env_runner.policy_map[pid].num_grad_updates + for pid in policy_ids or [] + }, + } + self.env_runner.set_global_vars(global_vars, policy_ids=policy_ids) + if self.config.policy_states_are_swappable: + self.env_runner.unlock() + + # Only need to update workers if there are remote workers. + self._counters[NUM_TRAINING_STEP_CALLS_SINCE_LAST_SYNCH_WORKER_WEIGHTS] += 1 + if ( + self.env_runner_group.num_remote_workers() > 0 + and self._counters[NUM_TRAINING_STEP_CALLS_SINCE_LAST_SYNCH_WORKER_WEIGHTS] + >= self.config.broadcast_interval + and workers_that_need_updates + ): + if self.config.policy_states_are_swappable: + self.env_runner.lock() + weights = self.env_runner.get_weights(policy_ids) + if self.config.policy_states_are_swappable: + self.env_runner.unlock() + weights_ref = ray.put(weights) + + self._learner_thread.policy_ids_updated.clear() + self._counters[NUM_TRAINING_STEP_CALLS_SINCE_LAST_SYNCH_WORKER_WEIGHTS] = 0 + self._counters[NUM_SYNCH_WORKER_WEIGHTS] += 1 + self.env_runner_group.foreach_env_runner( + func=lambda w: w.set_weights(ray.get(weights_ref), global_vars), + local_env_runner=False, + remote_worker_ids=list(workers_that_need_updates), + timeout_seconds=0, # Don't wait for the workers to finish. + ) + + @override(Algorithm) + def _compile_iteration_results_old_api_stack(self, *args, **kwargs): + result = super()._compile_iteration_results_old_api_stack(*args, **kwargs) + if not self.config.enable_rl_module_and_learner: + result = self._learner_thread.add_learner_metrics( + result, overwrite_learner_info=False + ) + return result + + +Impala = IMPALA + + +@OldAPIStack +def make_learner_thread(local_worker, config): + if not config["simple_optimizer"]: + logger.info( + "Enabling multi-GPU mode, {} GPUs, {} parallel tower-stacks".format( + config["num_gpus"], config["num_multi_gpu_tower_stacks"] + ) + ) + num_stacks = config["num_multi_gpu_tower_stacks"] + buffer_size = config["minibatch_buffer_size"] + if num_stacks < buffer_size: + logger.warning( + "In multi-GPU mode you should have at least as many " + "multi-GPU tower stacks (to load data into on one device) as " + "you have stack-index slots in the buffer! You have " + f"configured {num_stacks} stacks and a buffer of size " + f"{buffer_size}. Setting " + f"`minibatch_buffer_size={num_stacks}`." + ) + config["minibatch_buffer_size"] = num_stacks + + learner_thread = MultiGPULearnerThread( + local_worker, + num_gpus=config["num_gpus"], + lr=config["lr"], + train_batch_size=config["train_batch_size"], + num_multi_gpu_tower_stacks=config["num_multi_gpu_tower_stacks"], + num_sgd_iter=config["num_epochs"], + learner_queue_size=config["learner_queue_size"], + learner_queue_timeout=config["learner_queue_timeout"], + num_data_load_threads=config["num_gpu_loader_threads"], + ) + else: + learner_thread = LearnerThread( + local_worker, + minibatch_buffer_size=config["minibatch_buffer_size"], + num_sgd_iter=config["num_epochs"], + learner_queue_size=config["learner_queue_size"], + learner_queue_timeout=config["learner_queue_timeout"], + ) + return learner_thread diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala_learner.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala_learner.py new file mode 100644 index 0000000000000000000000000000000000000000..ecedaf1ce1f7b34c1c3e40672c6ffe722c618f9a --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala_learner.py @@ -0,0 +1,524 @@ +import logging +import queue +import threading +import time +from collections import deque +from typing import Any, Dict, Union + +import ray +from ray.rllib.algorithms.appo.utils import CircularBuffer +from ray.rllib.algorithms.impala.impala import LEARNER_RESULTS_CURR_ENTROPY_COEFF_KEY +from ray.rllib.core import COMPONENT_RL_MODULE +from ray.rllib.core.learner.learner import Learner +from ray.rllib.core.learner.training_data import TrainingData +from ray.rllib.core.rl_module.apis import ValueFunctionAPI +from ray.rllib.utils.annotations import ( + OverrideToImplementCustomLogic_CallToSuperRecommended, + override, +) +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.lambda_defaultdict import LambdaDefaultDict +from ray.rllib.utils.metrics import ( + ALL_MODULES, + NUM_ENV_STEPS_SAMPLED_LIFETIME, +) +from ray.rllib.utils.metrics.metrics_logger import MetricsLogger +from ray.rllib.utils.metrics.ray_metrics import ( + DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + TimerAndPrometheusLogger, +) +from ray.rllib.utils.schedules.scheduler import Scheduler +from ray.rllib.utils.typing import ModuleID, ResultDict +from ray.util.metrics import Gauge, Histogram + +logger = logging.getLogger(__name__) + +torch, _ = try_import_torch() + +GPU_LOADER_QUEUE_WAIT_TIMER = "gpu_loader_queue_wait_timer" +GPU_LOADER_LOAD_TO_GPU_TIMER = "gpu_loader_load_to_gpu_timer" +LEARNER_THREAD_IN_QUEUE_WAIT_TIMER = "learner_thread_in_queue_wait_timer" +LEARNER_THREAD_ENV_STEPS_DROPPED = "learner_thread_env_steps_dropped" +LEARNER_THREAD_UPDATE_TIMER = "learner_thread_update_timer" +RAY_GET_EPISODES_TIMER = "ray_get_episodes_timer" + +QUEUE_SIZE_GPU_LOADER_QUEUE = "queue_size_gpu_loader_queue" +QUEUE_SIZE_LEARNER_THREAD_QUEUE = "queue_size_learner_thread_queue" +QUEUE_SIZE_RESULTS_QUEUE = "queue_size_results_queue" + +_CURRENT_GLOBAL_TIMESTEPS = None + + +class IMPALALearner(Learner): + @override(Learner) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Ray metrics + self._metrics_learner_impala_update = Histogram( + name="rllib_learner_impala_update_time", + description="Time spent in the 'IMPALALearner.update()' method.", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_learner_impala_update.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + self._metrics_learner_impala_update_solve_refs = Histogram( + name="rllib_learner_impala_update_solve_refs_time", + description="Time spent on resolving refs in the 'Learner.update()'", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_learner_impala_update_solve_refs.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + self._metrics_learner_impala_update_make_batch_if_necessary = Histogram( + name="rllib_learner_impala_update_make_batch_if_necessary_time", + description="Time spent on making a batch in the 'Learner.update()'.", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_learner_impala_update_make_batch_if_necessary.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + self._metrics_learner_impala_get_learner_state_time = Histogram( + name="rllib_learner_impala_get_learner_state_time", + description="Time spent on get_state() in IMPALALearner.update().", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_learner_impala_get_learner_state_time.set_default_tags( + {"rllib": self.__class__.__name__} + ) + + @override(Learner) + def build(self) -> None: + super().build() + + # TODO (sven): We replace the dummy RLock here for APPO/IMPALA, b/c these algos + # require this for thread safety reasons. + # An RLock breaks our current OfflineData and OfflinePreLearner logic, in which + # the Learner (which contains a MetricsLogger) is serialized and deserialized. + # We will have to fix this offline RL logic first, then can remove this hack + # here and return to always using the RLock. + self.metrics._threading_lock = threading.RLock() + self._num_updates = 0 + self._num_updates_lock = threading.Lock() + + # Dict mapping module IDs to the respective entropy Scheduler instance. + self.entropy_coeff_schedulers_per_module: Dict[ + ModuleID, Scheduler + ] = LambdaDefaultDict( + lambda module_id: Scheduler( + fixed_value_or_schedule=( + self.config.get_config_for_module(module_id).entropy_coeff + ), + framework=self.framework, + device=self._device, + ) + ) + + # Create and start the GPU-loader thread. It picks up train-ready batches from + # the "GPU-loader queue" and loads them to the GPU, then places the GPU batches + # on the "update queue" for the actual RLModule forward pass and loss + # computations. + self._gpu_loader_in_queue = queue.Queue() + + # Default is to have a learner thread. + if not hasattr(self, "_learner_thread_in_queue"): + self._learner_thread_in_queue = deque(maxlen=self.config.learner_queue_size) + + # TODO (sven): Figure out a way to use a results queue instaad of the "reduce + # metrics each 20 updates" logic right now. + # # Results queue for reduced Learner metrics. + # # self._learner_thread_out_queue = deque(maxlen=1) + + # Create and start the GPU loader thread(s). + if self.config.num_gpus_per_learner > 0: + self._gpu_loader_threads = [ + _GPULoaderThread( + in_queue=self._gpu_loader_in_queue, + out_queue=self._learner_thread_in_queue, + device=self._device, + metrics_logger=self.metrics, + ) + for _ in range(self.config.num_gpu_loader_threads) + ] + for t in self._gpu_loader_threads: + t.start() + + # Create and start the Learner thread. + self._learner_thread = _LearnerThread( + update_method=Learner.update, + in_queue=self._learner_thread_in_queue, + # TODO (sven): Figure out a way to use a results queue instaad of the "reduce + # metrics each 20 updates" logic right now. + # out_queue=self._learner_thread_out_queue, + learner=self, + ) + self._learner_thread.start() + + @override(Learner) + def update( + self, + training_data: TrainingData, + *, + timesteps: Dict[str, Any], + return_state: bool = False, + **kwargs, + ) -> ResultDict: + """ + + Args: + batch: + timesteps: + return_state: Whether to include one of the Learner worker's state from + after the update step in the returned results dict (under the + `_rl_module_state_after_update` key). Note that after an update, all + Learner workers' states should be identical, so we use the first + Learner's state here. Useful for avoiding an extra `get_weights()` call, + e.g. for synchronizing EnvRunner weights. + **kwargs: + + Returns: + + """ + global _CURRENT_GLOBAL_TIMESTEPS + _CURRENT_GLOBAL_TIMESTEPS = timesteps or {} + + with TimerAndPrometheusLogger(self._metrics_learner_impala_update): + # Get the train batch from the object store. + with TimerAndPrometheusLogger( + self._metrics_learner_impala_update_solve_refs + ): + training_data.solve_refs() + + with TimerAndPrometheusLogger( + self._metrics_learner_impala_update_make_batch_if_necessary + ): + batch = self._make_batch_if_necessary(training_data=training_data) + assert batch is not None + + if self.config.num_gpus_per_learner > 0: + self._gpu_loader_in_queue.put(batch) + self.metrics.log_value( + (ALL_MODULES, QUEUE_SIZE_GPU_LOADER_QUEUE), + self._gpu_loader_in_queue.qsize(), + ) + else: + if isinstance(self._learner_thread_in_queue, CircularBuffer): + ts_dropped = self._learner_thread_in_queue.add(batch) + self.metrics.log_value( + (ALL_MODULES, LEARNER_THREAD_ENV_STEPS_DROPPED), + ts_dropped, + reduce="sum", + ) + else: + # Enqueue to Learner thread's in-queue. + _LearnerThread.enqueue( + self._learner_thread_in_queue, batch, self.metrics + ) + + # TODO (sven): Find a better way to limit the number of (mostly) unnecessary + # metrics reduces. + with self._num_updates_lock: + count = self._num_updates + result = {} + + if count >= 20: + with self._num_updates_lock: + self._num_updates = 0 + result = self.metrics.reduce() + + if return_state: + with TimerAndPrometheusLogger( + self._metrics_learner_impala_get_learner_state_time + ): + learner_state = self.get_state( + # Only return the state of those RLModules that are trainable. + components=[ + COMPONENT_RL_MODULE + "/" + mid + for mid in self.module.keys() + if self.should_module_be_updated(mid) + ], + inference_only=True, + ) + learner_state[COMPONENT_RL_MODULE] = ray.put( + learner_state[COMPONENT_RL_MODULE] + ) + result["_rl_module_state_after_update"] = learner_state + + return result + + # TODO (sven): Figure out a way to use a results queue instaad of the "reduce + # metrics each 20 updates" logic right now. + # try: + # result = self._learner_thread_out_queue.popleft() + # except IndexError: + # result = {} + + # if return_state: + # learner_state = self.get_state( + # # Only return the state of those RLModules that are trainable. + # components=[ + # COMPONENT_RL_MODULE + "/" + mid + # for mid in self.module.keys() + # if self.should_module_be_updated(mid) + # ], + # inference_only=True, + # ) + # learner_state[COMPONENT_RL_MODULE] = ray.put( + # learner_state[COMPONENT_RL_MODULE] + # ) + # result["_rl_module_state_after_update"] = learner_state + + # return result + + @OverrideToImplementCustomLogic_CallToSuperRecommended + def before_gradient_based_update(self, *, timesteps: Dict[str, Any]) -> None: + super().before_gradient_based_update(timesteps=timesteps) + + for module_id in self.module.keys(): + # Update entropy coefficient via our Scheduler. + new_entropy_coeff = self.entropy_coeff_schedulers_per_module[ + module_id + ].update(timestep=timesteps.get(NUM_ENV_STEPS_SAMPLED_LIFETIME, 0)) + self.metrics.log_value( + (module_id, LEARNER_RESULTS_CURR_ENTROPY_COEFF_KEY), + new_entropy_coeff, + window=1, + ) + + @override(Learner) + def remove_module(self, module_id: str): + super().remove_module(module_id) + self.entropy_coeff_schedulers_per_module.pop(module_id) + + @classmethod + @override(Learner) + def rl_module_required_apis(cls) -> list[type]: + # In order for a PPOLearner to update an RLModule, it must implement the + # following APIs: + return [ValueFunctionAPI] + + +ImpalaLearner = IMPALALearner + + +class _GPULoaderThread(threading.Thread): + def __init__( + self, + *, + in_queue: queue.Queue, + out_queue: deque, + device: torch.device, + metrics_logger: MetricsLogger, + ): + super().__init__(name="_GPULoaderThread") + self.daemon = True + + self._in_queue = in_queue + self._out_queue = out_queue + self._ts_dropped = 0 + self._device = device + self.metrics = metrics_logger + + self._metrics_impala_gpu_loader_thread_step_time = Histogram( + name="rllib_learner_impala_gpu_loader_thread_step_time", + description="Time taken in seconds for gpu loader thread _step.", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_impala_gpu_loader_thread_step_time.set_default_tags( + {"rllib": "IMPALA/GPULoaderThread"} + ) + + self._metrics_impala_gpu_loader_thread_step_in_queue_get_time = Histogram( + name="rllib_learner_impala_gpu_loader_thread_step_get_time", + description="Time taken in seconds for gpu loader thread _step _in_queue.get().", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_impala_gpu_loader_thread_step_in_queue_get_time.set_default_tags( + {"rllib": "IMPALA/GPULoaderThread"} + ) + + self._metrics_impala_gpu_loader_thread_step_load_to_gpu_time = Histogram( + name="rllib_learner_impala_gpu_loader_thread_step_load_to_gpu_time", + description="Time taken in seconds for GPU loader thread _step to load batch to GPU.", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_impala_gpu_loader_thread_step_load_to_gpu_time.set_default_tags( + {"rllib": "IMPALA/GPULoaderThread"} + ) + + self._metrics_impala_gpu_loader_thread_in_qsize_beginning_of_step = Gauge( + name="rllib_impala_gpu_loader_thread_in_qsize_beginning_of_step", + description="Size of the _GPULoaderThread in-queue size, at the beginning of the step.", + tag_keys=("rllib",), + ) + self._metrics_impala_gpu_loader_thread_in_qsize_beginning_of_step.set_default_tags( + {"rllib": "IMPALA/GPULoaderThread"} + ) + + def run(self) -> None: + while True: + with TimerAndPrometheusLogger( + self._metrics_impala_gpu_loader_thread_step_time + ): + self._step() + + def _step(self) -> None: + self._metrics_impala_gpu_loader_thread_in_qsize_beginning_of_step.set( + value=self._in_queue.qsize() + ) + # Get a new batch from the data (in-queue). + with self.metrics.log_time((ALL_MODULES, GPU_LOADER_QUEUE_WAIT_TIMER)): + with TimerAndPrometheusLogger( + self._metrics_impala_gpu_loader_thread_step_in_queue_get_time + ): + ma_batch_on_cpu = self._in_queue.get() + + # Load the batch onto the GPU device. + with self.metrics.log_time((ALL_MODULES, GPU_LOADER_LOAD_TO_GPU_TIMER)): + with TimerAndPrometheusLogger( + self._metrics_impala_gpu_loader_thread_step_load_to_gpu_time + ): + ma_batch_on_gpu = ma_batch_on_cpu.to_device( + self._device, pin_memory=False + ) + + if isinstance(self._out_queue, CircularBuffer): + ts_dropped = self._out_queue.add(ma_batch_on_gpu) + self.metrics.log_value( + (ALL_MODULES, LEARNER_THREAD_ENV_STEPS_DROPPED), + ts_dropped, + reduce="sum", + ) + else: + # Enqueue to Learner thread's in-queue. + _LearnerThread.enqueue(self._out_queue, ma_batch_on_gpu, self.metrics) + + +class _LearnerThread(threading.Thread): + def __init__( + self, + *, + update_method, + in_queue: Union[deque, CircularBuffer], + # TODO (sven): Figure out a way to use a results queue instaad of the "reduce + # metrics each 20 updates" logic right now. + # out_queue: deque, + learner, + ): + super().__init__(name="_LearnerThread") + self.daemon = True + self.learner = learner + self.stopped = False + + self._update_method = update_method + self._in_queue: Union[deque, CircularBuffer] = in_queue + # TODO (sven): Figure out a way to use a results queue instaad of the "reduce + # metrics each 20 updates" logic right now. + # self._out_queue: deque = out_queue + + # Ray metrics + self._metrics_learner_impala_thread_step = Histogram( + name="rllib_learner_impala_learner_thread_step_time", + description="Time taken in seconds for learner thread _step.", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_learner_impala_thread_step.set_default_tags( + {"rllib": "IMPALA/LearnerThread"} + ) + + self._metrics_learner_impala_thread_step_update = Histogram( + name="rllib_learner_impala_learner_thread_step_update_time", + description="Time taken in seconds for learner thread _step update.", + boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS, + tag_keys=("rllib",), + ) + self._metrics_learner_impala_thread_step_update.set_default_tags( + {"rllib": "IMPALA/LearnerThread"} + ) + + def run(self) -> None: + while not self.stopped: + with TimerAndPrometheusLogger(self._metrics_learner_impala_thread_step): + self.step() + + def step(self): + global _CURRENT_GLOBAL_TIMESTEPS + + # Get a new batch from the GPU-data (deque.pop -> newest item first). + with self.learner.metrics.log_time( + (ALL_MODULES, LEARNER_THREAD_IN_QUEUE_WAIT_TIMER) + ): + # Get a new batch from the GPU-data (learner queue OR circular buffer). + if isinstance(self._in_queue, CircularBuffer): + ma_batch_on_gpu = self._in_queue.sample() + else: + # Queue is empty: Sleep a tiny bit to avoid CPU-thrashing. + while not self._in_queue: + time.sleep(0.0001) + # Consume from the left (oldest batches first). + # If we consumed from the right, we would run into the danger of + # learning from newer batches (left side) most times, BUT sometimes + # grabbing older batches (right area of deque). + ma_batch_on_gpu = self._in_queue.popleft() + + # Add this check here in case thread has been stopped while we were waiting for + # a batch from the queue/buffer. + if self.stopped: + return + + # Call the update method on the batch. + with self.learner.metrics.log_time((ALL_MODULES, LEARNER_THREAD_UPDATE_TIMER)): + # TODO (sven): For multi-agent AND SGD iter > 1, we need to make sure + # this thread has the information about the min minibatches necessary + # (due to different agents taking different steps in the env, e.g. + # MA-CartPole). + with TimerAndPrometheusLogger( + self._metrics_learner_impala_thread_step_update + ): + self._update_method( + self=self.learner, + training_data=TrainingData(batch=ma_batch_on_gpu), + timesteps=_CURRENT_GLOBAL_TIMESTEPS, + _no_metrics_reduce=True, + ) + # TODO (sven): Figure out a way to use a results queue instaad of the "reduce + # metrics each 20 updates" logic right now. + # self._out_queue.append(results) + with self.learner._num_updates_lock: + self.learner._num_updates += 1 + + @staticmethod + def enqueue(learner_queue: deque, batch, metrics): + # Right-append to learner queue (a deque). If full, drops the leftmost + # (oldest) item in the deque. + # Note that we consume from the left (oldest first), which is why the queue size + # should probably always be small'ish (<< 10), otherwise we run into the danger + # of training with very old samples. + # If we consumed from the right, we would run into the danger of learning + # from newer batches (left side) most times, BUT sometimes grabbing a + # really old batches (right area of deque). + if len(learner_queue) == learner_queue.maxlen: + metrics.log_value( + (ALL_MODULES, LEARNER_THREAD_ENV_STEPS_DROPPED), + learner_queue.popleft().env_steps(), + reduce="sum", + ) + learner_queue.append(batch) + + # Log current queue size. + metrics.log_value( + (ALL_MODULES, QUEUE_SIZE_LEARNER_THREAD_QUEUE), + len(learner_queue), + ) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala_tf_policy.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala_tf_policy.py new file mode 100644 index 0000000000000000000000000000000000000000..94ee60e2026003a1c5a2d8e91c1422db77f0b894 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala_tf_policy.py @@ -0,0 +1,449 @@ +"""Adapted from A3CTFPolicy to add V-trace. + +Keep in sync with changes to A3CTFPolicy and VtraceSurrogatePolicy.""" + +import logging +from typing import Dict, List, Optional, Type, Union + +import gymnasium as gym +import numpy as np + +from ray.rllib.algorithms.impala import vtrace_tf as vtrace +from ray.rllib.evaluation.postprocessing import compute_bootstrap_value +from ray.rllib.models.modelv2 import ModelV2 +from ray.rllib.models.tf.tf_action_dist import Categorical, TFActionDistribution +from ray.rllib.policy.dynamic_tf_policy_v2 import DynamicTFPolicyV2 +from ray.rllib.policy.eager_tf_policy_v2 import EagerTFPolicyV2 +from ray.rllib.policy.sample_batch import SampleBatch +from ray.rllib.policy.tf_mixins import ( + EntropyCoeffSchedule, + GradStatsMixin, + LearningRateSchedule, + ValueNetworkMixin, +) +from ray.rllib.utils import force_list +from ray.rllib.utils.annotations import override +from ray.rllib.utils.framework import try_import_tf +from ray.rllib.utils.tf_utils import explained_variance +from ray.rllib.utils.typing import ( + LocalOptimizer, + ModelGradients, + TensorType, + TFPolicyV2Type, +) + +tf1, tf, tfv = try_import_tf() + +logger = logging.getLogger(__name__) + + +class VTraceLoss: + def __init__( + self, + actions, + actions_logp, + actions_entropy, + dones, + behaviour_action_logp, + behaviour_logits, + target_logits, + discount, + rewards, + values, + bootstrap_value, + dist_class, + model, + valid_mask, + config, + vf_loss_coeff=0.5, + entropy_coeff=0.01, + clip_rho_threshold=1.0, + clip_pg_rho_threshold=1.0, + ): + """Policy gradient loss with vtrace importance weighting. + + VTraceLoss takes tensors of shape [T, B, ...], where `B` is the + batch_size. The reason we need to know `B` is for V-trace to properly + handle episode cut boundaries. + + Args: + actions: An int|float32 tensor of shape [T, B, ACTION_SPACE]. + actions_logp: A float32 tensor of shape [T, B]. + actions_entropy: A float32 tensor of shape [T, B]. + dones: A bool tensor of shape [T, B]. + behaviour_action_logp: Tensor of shape [T, B]. + behaviour_logits: A list with length of ACTION_SPACE of float32 + tensors of shapes + [T, B, ACTION_SPACE[0]], + ..., + [T, B, ACTION_SPACE[-1]] + target_logits: A list with length of ACTION_SPACE of float32 + tensors of shapes + [T, B, ACTION_SPACE[0]], + ..., + [T, B, ACTION_SPACE[-1]] + discount: A float32 scalar. + rewards: A float32 tensor of shape [T, B]. + values: A float32 tensor of shape [T, B]. + bootstrap_value: A float32 tensor of shape [B]. + dist_class: action distribution class for logits. + valid_mask: A bool tensor of valid RNN input elements (#2992). + config: Algorithm config dict. + """ + + # Compute vtrace on the CPU for better performance. + with tf.device("/cpu:0"): + self.vtrace_returns = vtrace.multi_from_logits( + behaviour_action_log_probs=behaviour_action_logp, + behaviour_policy_logits=behaviour_logits, + target_policy_logits=target_logits, + actions=tf.unstack(actions, axis=2), + discounts=tf.cast(~tf.cast(dones, tf.bool), tf.float32) * discount, + rewards=rewards, + values=values, + bootstrap_value=bootstrap_value, + dist_class=dist_class, + model=model, + clip_rho_threshold=tf.cast(clip_rho_threshold, tf.float32), + clip_pg_rho_threshold=tf.cast(clip_pg_rho_threshold, tf.float32), + ) + self.value_targets = self.vtrace_returns.vs + + # The policy gradients loss. + masked_pi_loss = tf.boolean_mask( + actions_logp * self.vtrace_returns.pg_advantages, valid_mask + ) + self.pi_loss = -tf.reduce_sum(masked_pi_loss) + self.mean_pi_loss = -tf.reduce_mean(masked_pi_loss) + + # The baseline loss. + delta = tf.boolean_mask(values - self.vtrace_returns.vs, valid_mask) + delta_squarred = tf.math.square(delta) + self.vf_loss = 0.5 * tf.reduce_sum(delta_squarred) + self.mean_vf_loss = 0.5 * tf.reduce_mean(delta_squarred) + + # The entropy loss. + masked_entropy = tf.boolean_mask(actions_entropy, valid_mask) + self.entropy = tf.reduce_sum(masked_entropy) + self.mean_entropy = tf.reduce_mean(masked_entropy) + + # The summed weighted loss. + self.total_loss = self.pi_loss - self.entropy * entropy_coeff + + # Optional vf loss (or in a separate term due to separate + # optimizers/networks). + self.loss_wo_vf = self.total_loss + if not config["_separate_vf_optimizer"]: + self.total_loss += self.vf_loss * vf_loss_coeff + + +def _make_time_major(policy, seq_lens, tensor): + """Swaps batch and trajectory axis. + + Args: + policy: Policy reference + seq_lens: Sequence lengths if recurrent or None + tensor: A tensor or list of tensors to reshape. + trajectory item. + + Returns: + res: A tensor with swapped axes or a list of tensors with + swapped axes. + """ + if isinstance(tensor, list): + return [_make_time_major(policy, seq_lens, t) for t in tensor] + + if policy.is_recurrent(): + B = tf.shape(seq_lens)[0] + T = tf.shape(tensor)[0] // B + else: + # Important: chop the tensor into batches at known episode cut + # boundaries. + # TODO: (sven) this is kind of a hack and won't work for + # batch_mode=complete_episodes. + T = policy.config["rollout_fragment_length"] + B = tf.shape(tensor)[0] // T + rs = tf.reshape(tensor, tf.concat([[B, T], tf.shape(tensor)[1:]], axis=0)) + + # swap B and T axes + res = tf.transpose(rs, [1, 0] + list(range(2, 1 + int(tf.shape(tensor).shape[0])))) + + return res + + +class VTraceClipGradients: + """VTrace version of gradient computation logic.""" + + def __init__(self): + """No special initialization required.""" + pass + + def compute_gradients_fn( + self, optimizer: LocalOptimizer, loss: TensorType + ) -> ModelGradients: + # Supporting more than one loss/optimizer. + trainable_variables = self.model.trainable_variables() + if self.config["_tf_policy_handles_more_than_one_loss"]: + optimizers = force_list(optimizer) + losses = force_list(loss) + assert len(optimizers) == len(losses) + clipped_grads_and_vars = [] + for optim, loss_ in zip(optimizers, losses): + grads_and_vars = optim.compute_gradients(loss_, trainable_variables) + clipped_g_and_v = [] + for g, v in grads_and_vars: + if g is not None: + clipped_g, _ = tf.clip_by_global_norm( + [g], self.config["grad_clip"] + ) + clipped_g_and_v.append((clipped_g[0], v)) + clipped_grads_and_vars.append(clipped_g_and_v) + + self.grads = [g for g_and_v in clipped_grads_and_vars for (g, v) in g_and_v] + # Only one optimizer and and loss term. + else: + grads_and_vars = optimizer.compute_gradients( + loss, self.model.trainable_variables() + ) + grads = [g for (g, v) in grads_and_vars] + self.grads, _ = tf.clip_by_global_norm(grads, self.config["grad_clip"]) + clipped_grads_and_vars = list(zip(self.grads, trainable_variables)) + + return clipped_grads_and_vars + + +class VTraceOptimizer: + """Optimizer function for VTrace policies.""" + + def __init__(self): + pass + + # TODO: maybe standardize this function, so the choice of optimizers are more + # predictable for common algorithms. + def optimizer( + self, + ) -> Union["tf.keras.optimizers.Optimizer", List["tf.keras.optimizers.Optimizer"]]: + config = self.config + if config["opt_type"] == "adam": + if config["framework"] == "tf2": + optim = tf.keras.optimizers.Adam(self.cur_lr) + if config["_separate_vf_optimizer"]: + return optim, tf.keras.optimizers.Adam(config["_lr_vf"]) + else: + optim = tf1.train.AdamOptimizer(self.cur_lr) + if config["_separate_vf_optimizer"]: + return optim, tf1.train.AdamOptimizer(config["_lr_vf"]) + else: + if config["_separate_vf_optimizer"]: + raise ValueError( + "RMSProp optimizer not supported for separate" + "vf- and policy losses yet! Set `opt_type=adam`" + ) + + if tfv == 2: + optim = tf.keras.optimizers.RMSprop( + self.cur_lr, config["decay"], config["momentum"], config["epsilon"] + ) + else: + optim = tf1.train.RMSPropOptimizer( + self.cur_lr, config["decay"], config["momentum"], config["epsilon"] + ) + + return optim + + +# We need this builder function because we want to share the same +# custom logics between TF1 dynamic and TF2 eager policies. +def get_impala_tf_policy(name: str, base: TFPolicyV2Type) -> TFPolicyV2Type: + """Construct an ImpalaTFPolicy inheriting either dynamic or eager base policies. + + Args: + base: Base class for this policy. DynamicTFPolicyV2 or EagerTFPolicyV2. + + Returns: + A TF Policy to be used with Impala. + """ + + # VTrace mixins are placed in front of more general mixins to make sure + # their functions like optimizer() overrides all the other implementations + # (e.g., LearningRateSchedule.optimizer()) + class ImpalaTFPolicy( + VTraceClipGradients, + VTraceOptimizer, + LearningRateSchedule, + EntropyCoeffSchedule, + GradStatsMixin, + ValueNetworkMixin, + base, + ): + def __init__( + self, + observation_space, + action_space, + config, + existing_model=None, + existing_inputs=None, + ): + # First thing first, enable eager execution if necessary. + base.enable_eager_execution_if_necessary() + + # Initialize base class. + base.__init__( + self, + observation_space, + action_space, + config, + existing_inputs=existing_inputs, + existing_model=existing_model, + ) + ValueNetworkMixin.__init__(self, config) + + # If Learner API is used, we don't need any loss-specific mixins. + # However, we also would like to avoid creating special Policy-subclasses + # for this as the entire Policy concept will soon not be used anymore with + # the new Learner- and RLModule APIs. + GradStatsMixin.__init__(self) + VTraceClipGradients.__init__(self) + VTraceOptimizer.__init__(self) + LearningRateSchedule.__init__(self, config["lr"], config["lr_schedule"]) + EntropyCoeffSchedule.__init__( + self, config["entropy_coeff"], config["entropy_coeff_schedule"] + ) + + # Note: this is a bit ugly, but loss and optimizer initialization must + # happen after all the MixIns are initialized. + self.maybe_initialize_optimizer_and_loss() + + @override(base) + def loss( + self, + model: Union[ModelV2, "tf.keras.Model"], + dist_class: Type[TFActionDistribution], + train_batch: SampleBatch, + ) -> Union[TensorType, List[TensorType]]: + model_out, _ = model(train_batch) + action_dist = dist_class(model_out, model) + + if isinstance(self.action_space, gym.spaces.Discrete): + is_multidiscrete = False + output_hidden_shape = [self.action_space.n] + elif isinstance(self.action_space, gym.spaces.MultiDiscrete): + is_multidiscrete = True + output_hidden_shape = self.action_space.nvec.astype(np.int32) + else: + is_multidiscrete = False + output_hidden_shape = 1 + + def make_time_major(*args, **kw): + return _make_time_major( + self, train_batch.get(SampleBatch.SEQ_LENS), *args, **kw + ) + + actions = train_batch[SampleBatch.ACTIONS] + dones = train_batch[SampleBatch.TERMINATEDS] + rewards = train_batch[SampleBatch.REWARDS] + behaviour_action_logp = train_batch[SampleBatch.ACTION_LOGP] + behaviour_logits = train_batch[SampleBatch.ACTION_DIST_INPUTS] + unpacked_behaviour_logits = tf.split( + behaviour_logits, output_hidden_shape, axis=1 + ) + unpacked_outputs = tf.split(model_out, output_hidden_shape, axis=1) + values = model.value_function() + values_time_major = make_time_major(values) + bootstrap_values_time_major = make_time_major( + train_batch[SampleBatch.VALUES_BOOTSTRAPPED] + ) + bootstrap_value = bootstrap_values_time_major[-1] + + if self.is_recurrent(): + max_seq_len = tf.reduce_max(train_batch[SampleBatch.SEQ_LENS]) + mask = tf.sequence_mask(train_batch[SampleBatch.SEQ_LENS], max_seq_len) + mask = tf.reshape(mask, [-1]) + else: + mask = tf.ones_like(rewards) + + # Prepare actions for loss + loss_actions = ( + actions if is_multidiscrete else tf.expand_dims(actions, axis=1) + ) + + # Inputs are reshaped from [B * T] => [(T|T-1), B] for V-trace calc. + self.vtrace_loss = VTraceLoss( + actions=make_time_major(loss_actions), + actions_logp=make_time_major(action_dist.logp(actions)), + actions_entropy=make_time_major(action_dist.multi_entropy()), + dones=make_time_major(dones), + behaviour_action_logp=make_time_major(behaviour_action_logp), + behaviour_logits=make_time_major(unpacked_behaviour_logits), + target_logits=make_time_major(unpacked_outputs), + discount=self.config["gamma"], + rewards=make_time_major(rewards), + values=values_time_major, + bootstrap_value=bootstrap_value, + dist_class=Categorical if is_multidiscrete else dist_class, + model=model, + valid_mask=make_time_major(mask), + config=self.config, + vf_loss_coeff=self.config["vf_loss_coeff"], + entropy_coeff=self.entropy_coeff, + clip_rho_threshold=self.config["vtrace_clip_rho_threshold"], + clip_pg_rho_threshold=self.config["vtrace_clip_pg_rho_threshold"], + ) + + if self.config.get("_separate_vf_optimizer"): + return self.vtrace_loss.loss_wo_vf, self.vtrace_loss.vf_loss + else: + return self.vtrace_loss.total_loss + + @override(base) + def stats_fn(self, train_batch: SampleBatch) -> Dict[str, TensorType]: + values_batched = _make_time_major( + self, + train_batch.get(SampleBatch.SEQ_LENS), + self.model.value_function(), + ) + + return { + "cur_lr": tf.cast(self.cur_lr, tf.float64), + "policy_loss": self.vtrace_loss.mean_pi_loss, + "entropy": self.vtrace_loss.mean_entropy, + "entropy_coeff": tf.cast(self.entropy_coeff, tf.float64), + "var_gnorm": tf.linalg.global_norm(self.model.trainable_variables()), + "vf_loss": self.vtrace_loss.mean_vf_loss, + "vf_explained_var": explained_variance( + tf.reshape(self.vtrace_loss.value_targets, [-1]), + tf.reshape(values_batched, [-1]), + ), + } + + @override(base) + def postprocess_trajectory( + self, + sample_batch: SampleBatch, + other_agent_batches: Optional[SampleBatch] = None, + episode=None, + ): + # Call super's postprocess_trajectory first. + # sample_batch = super().postprocess_trajectory( + # sample_batch, other_agent_batches, episode + # ) + + if self.config["vtrace"]: + # Add the SampleBatch.VALUES_BOOTSTRAPPED column, which we'll need + # inside the loss for vtrace calculations. + sample_batch = compute_bootstrap_value(sample_batch, self) + + return sample_batch + + @override(base) + def get_batch_divisibility_req(self) -> int: + return self.config["rollout_fragment_length"] + + ImpalaTFPolicy.__name__ = name + ImpalaTFPolicy.__qualname__ = name + + return ImpalaTFPolicy + + +ImpalaTF1Policy = get_impala_tf_policy("ImpalaTF1Policy", DynamicTFPolicyV2) +ImpalaTF2Policy = get_impala_tf_policy("ImpalaTF2Policy", EagerTFPolicyV2) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala_torch_policy.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala_torch_policy.py new file mode 100644 index 0000000000000000000000000000000000000000..ee58654cab7b0aeafdb2864ce15d2c9b5508e2dc --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/impala_torch_policy.py @@ -0,0 +1,425 @@ +import logging +from typing import Dict, List, Optional, Type, Union + +import gymnasium as gym +import numpy as np + +import ray +from ray.rllib.evaluation.postprocessing import compute_bootstrap_value +from ray.rllib.models.action_dist import ActionDistribution +from ray.rllib.models.modelv2 import ModelV2 +from ray.rllib.models.torch.torch_action_dist import TorchCategorical +from ray.rllib.policy.sample_batch import SampleBatch +from ray.rllib.policy.torch_mixins import ( + EntropyCoeffSchedule, + LearningRateSchedule, + ValueNetworkMixin, +) +from ray.rllib.policy.torch_policy_v2 import TorchPolicyV2 +from ray.rllib.utils.annotations import override +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.numpy import convert_to_numpy +from ray.rllib.utils.torch_utils import ( + apply_grad_clipping, + explained_variance, + global_norm, + sequence_mask, +) +from ray.rllib.utils.typing import TensorType + +torch, nn = try_import_torch() + +logger = logging.getLogger(__name__) + + +class VTraceLoss: + def __init__( + self, + actions, + actions_logp, + actions_entropy, + dones, + behaviour_action_logp, + behaviour_logits, + target_logits, + discount, + rewards, + values, + bootstrap_value, + dist_class, + model, + valid_mask, + config, + vf_loss_coeff=0.5, + entropy_coeff=0.01, + clip_rho_threshold=1.0, + clip_pg_rho_threshold=1.0, + ): + """Policy gradient loss with vtrace importance weighting. + + VTraceLoss takes tensors of shape [T, B, ...], where `B` is the + batch_size. The reason we need to know `B` is for V-trace to properly + handle episode cut boundaries. + + Args: + actions: An int|float32 tensor of shape [T, B, ACTION_SPACE]. + actions_logp: A float32 tensor of shape [T, B]. + actions_entropy: A float32 tensor of shape [T, B]. + dones: A bool tensor of shape [T, B]. + behaviour_action_logp: Tensor of shape [T, B]. + behaviour_logits: A list with length of ACTION_SPACE of float32 + tensors of shapes + [T, B, ACTION_SPACE[0]], + ..., + [T, B, ACTION_SPACE[-1]] + target_logits: A list with length of ACTION_SPACE of float32 + tensors of shapes + [T, B, ACTION_SPACE[0]], + ..., + [T, B, ACTION_SPACE[-1]] + discount: A float32 scalar. + rewards: A float32 tensor of shape [T, B]. + values: A float32 tensor of shape [T, B]. + bootstrap_value: A float32 tensor of shape [B]. + dist_class: action distribution class for logits. + valid_mask: A bool tensor of valid RNN input elements (#2992). + config: Algorithm config dict. + """ + import ray.rllib.algorithms.impala.vtrace_torch as vtrace + + if valid_mask is None: + valid_mask = torch.ones_like(actions_logp) + + # Compute vtrace on the CPU for better perf + # (devices handled inside `vtrace.multi_from_logits`). + device = behaviour_action_logp[0].device + self.vtrace_returns = vtrace.multi_from_logits( + behaviour_action_log_probs=behaviour_action_logp, + behaviour_policy_logits=behaviour_logits, + target_policy_logits=target_logits, + actions=torch.unbind(actions, dim=2), + discounts=(1.0 - dones.float()) * discount, + rewards=rewards, + values=values, + bootstrap_value=bootstrap_value, + dist_class=dist_class, + model=model, + clip_rho_threshold=clip_rho_threshold, + clip_pg_rho_threshold=clip_pg_rho_threshold, + ) + # Move v-trace results back to GPU for actual loss computing. + self.value_targets = self.vtrace_returns.vs.to(device) + + # The policy gradients loss. + self.pi_loss = -torch.sum( + actions_logp * self.vtrace_returns.pg_advantages.to(device) * valid_mask + ) + + # The baseline loss. + delta = (values - self.value_targets) * valid_mask + self.vf_loss = 0.5 * torch.sum(torch.pow(delta, 2.0)) + + # The entropy loss. + self.entropy = torch.sum(actions_entropy * valid_mask) + self.mean_entropy = self.entropy / torch.sum(valid_mask) + + # The summed weighted loss. + self.total_loss = self.pi_loss - self.entropy * entropy_coeff + + # Optional vf loss (or in a separate term due to separate + # optimizers/networks). + self.loss_wo_vf = self.total_loss + if not config["_separate_vf_optimizer"]: + self.total_loss += self.vf_loss * vf_loss_coeff + + +def make_time_major(policy, seq_lens, tensor): + """Swaps batch and trajectory axis. + + Args: + policy: Policy reference + seq_lens: Sequence lengths if recurrent or None + tensor: A tensor or list of tensors to reshape. + + Returns: + res: A tensor with swapped axes or a list of tensors with + swapped axes. + """ + if isinstance(tensor, (list, tuple)): + return [make_time_major(policy, seq_lens, t) for t in tensor] + + if policy.is_recurrent(): + B = seq_lens.shape[0] + T = tensor.shape[0] // B + else: + # Important: chop the tensor into batches at known episode cut + # boundaries. + # TODO: (sven) this is kind of a hack and won't work for + # batch_mode=complete_episodes. + T = policy.config["rollout_fragment_length"] + B = tensor.shape[0] // T + rs = torch.reshape(tensor, [B, T] + list(tensor.shape[1:])) + + # Swap B and T axes. + res = torch.transpose(rs, 1, 0) + + return res + + +class VTraceOptimizer: + """Optimizer function for VTrace torch policies.""" + + def __init__(self): + pass + + def optimizer( + self, + ) -> Union[List["torch.optim.Optimizer"], "torch.optim.Optimizer"]: + + if self.config["_separate_vf_optimizer"]: + # Figure out, which parameters of the model belong to the value + # function (and which to the policy net). + dummy_batch = self._lazy_tensor_dict( + self._get_dummy_batch_from_view_requirements() + ) + # Zero out all gradients (set to None) + for param in self.model.parameters(): + param.grad = None + # Perform a dummy forward pass (through the policy net, which should be + # separated from the value function in this particular user setup). + out = self.model(dummy_batch) + # Perform a (dummy) backward pass to be able to see, which params have + # gradients and are therefore used for the policy computations (vs vf + # computations). + torch.sum(out[0]).backward() # [0] -> Model returns out and state-outs. + # Collect policy vs value function params separately. + policy_params = [] + value_params = [] + for param in self.model.parameters(): + if param.grad is None: + value_params.append(param) + else: + policy_params.append(param) + if self.config["opt_type"] == "adam": + return ( + torch.optim.Adam(params=policy_params, lr=self.cur_lr), + torch.optim.Adam(params=value_params, lr=self.cur_lr2), + ) + else: + raise NotImplementedError + + if self.config["opt_type"] == "adam": + return torch.optim.Adam(params=self.model.parameters(), lr=self.cur_lr) + else: + return torch.optim.RMSprop( + params=self.model.parameters(), + lr=self.cur_lr, + weight_decay=self.config["decay"], + momentum=self.config["momentum"], + eps=self.config["epsilon"], + ) + + +# VTrace mixins are placed in front of more general mixins to make sure +# their functions like optimizer() overrides all the other implementations +# (e.g., LearningRateSchedule.optimizer()) +class ImpalaTorchPolicy( + VTraceOptimizer, + LearningRateSchedule, + EntropyCoeffSchedule, + ValueNetworkMixin, + TorchPolicyV2, +): + """PyTorch policy class used with IMPALA.""" + + def __init__(self, observation_space, action_space, config): + config = dict( + ray.rllib.algorithms.impala.impala.IMPALAConfig().to_dict(), **config + ) + config["enable_rl_module_and_learner"] = False + config["enable_env_runner_and_connector_v2"] = False + + # If Learner API is used, we don't need any loss-specific mixins. + # However, we also would like to avoid creating special Policy-subclasses + # for this as the entire Policy concept will soon not be used anymore with + # the new Learner- and RLModule APIs. + VTraceOptimizer.__init__(self) + # Need to initialize learning rate variable before calling + # TorchPolicyV2.__init__. + lr_schedule_additional_args = [] + if config.get("_separate_vf_optimizer"): + lr_schedule_additional_args = ( + [config["_lr_vf"][0][1], config["_lr_vf"]] + if isinstance(config["_lr_vf"], (list, tuple)) + else [config["_lr_vf"], None] + ) + LearningRateSchedule.__init__( + self, config["lr"], config["lr_schedule"], *lr_schedule_additional_args + ) + EntropyCoeffSchedule.__init__( + self, config["entropy_coeff"], config["entropy_coeff_schedule"] + ) + + TorchPolicyV2.__init__( + self, + observation_space, + action_space, + config, + max_seq_len=config["model"]["max_seq_len"], + ) + + ValueNetworkMixin.__init__(self, config) + + self._initialize_loss_from_dummy_batch() + + @override(TorchPolicyV2) + def loss( + self, + model: ModelV2, + dist_class: Type[ActionDistribution], + train_batch: SampleBatch, + ) -> Union[TensorType, List[TensorType]]: + model_out, _ = model(train_batch) + action_dist = dist_class(model_out, model) + + if isinstance(self.action_space, gym.spaces.Discrete): + is_multidiscrete = False + output_hidden_shape = [self.action_space.n] + elif isinstance(self.action_space, gym.spaces.MultiDiscrete): + is_multidiscrete = True + output_hidden_shape = self.action_space.nvec.astype(np.int32) + else: + is_multidiscrete = False + output_hidden_shape = 1 + + def _make_time_major(*args, **kw): + return make_time_major( + self, train_batch.get(SampleBatch.SEQ_LENS), *args, **kw + ) + + actions = train_batch[SampleBatch.ACTIONS] + dones = train_batch[SampleBatch.TERMINATEDS] + rewards = train_batch[SampleBatch.REWARDS] + behaviour_action_logp = train_batch[SampleBatch.ACTION_LOGP] + behaviour_logits = train_batch[SampleBatch.ACTION_DIST_INPUTS] + if isinstance(output_hidden_shape, (list, tuple, np.ndarray)): + unpacked_behaviour_logits = torch.split( + behaviour_logits, list(output_hidden_shape), dim=1 + ) + unpacked_outputs = torch.split(model_out, list(output_hidden_shape), dim=1) + else: + unpacked_behaviour_logits = torch.chunk( + behaviour_logits, output_hidden_shape, dim=1 + ) + unpacked_outputs = torch.chunk(model_out, output_hidden_shape, dim=1) + values = model.value_function() + values_time_major = _make_time_major(values) + bootstrap_values_time_major = _make_time_major( + train_batch[SampleBatch.VALUES_BOOTSTRAPPED] + ) + bootstrap_value = bootstrap_values_time_major[-1] + + if self.is_recurrent(): + max_seq_len = torch.max(train_batch[SampleBatch.SEQ_LENS]) + mask_orig = sequence_mask(train_batch[SampleBatch.SEQ_LENS], max_seq_len) + mask = torch.reshape(mask_orig, [-1]) + else: + mask = torch.ones_like(rewards) + + # Prepare actions for loss. + loss_actions = actions if is_multidiscrete else torch.unsqueeze(actions, dim=1) + + # Inputs are reshaped from [B * T] => [(T|T-1), B] for V-trace calc. + loss = VTraceLoss( + actions=_make_time_major(loss_actions), + actions_logp=_make_time_major(action_dist.logp(actions)), + actions_entropy=_make_time_major(action_dist.entropy()), + dones=_make_time_major(dones), + behaviour_action_logp=_make_time_major(behaviour_action_logp), + behaviour_logits=_make_time_major(unpacked_behaviour_logits), + target_logits=_make_time_major(unpacked_outputs), + discount=self.config["gamma"], + rewards=_make_time_major(rewards), + values=values_time_major, + bootstrap_value=bootstrap_value, + dist_class=TorchCategorical if is_multidiscrete else dist_class, + model=model, + valid_mask=_make_time_major(mask), + config=self.config, + vf_loss_coeff=self.config["vf_loss_coeff"], + entropy_coeff=self.entropy_coeff, + clip_rho_threshold=self.config["vtrace_clip_rho_threshold"], + clip_pg_rho_threshold=self.config["vtrace_clip_pg_rho_threshold"], + ) + + # Store values for stats function in model (tower), such that for + # multi-GPU, we do not override them during the parallel loss phase. + model.tower_stats["pi_loss"] = loss.pi_loss + model.tower_stats["vf_loss"] = loss.vf_loss + model.tower_stats["entropy"] = loss.entropy + model.tower_stats["mean_entropy"] = loss.mean_entropy + model.tower_stats["total_loss"] = loss.total_loss + + values_batched = make_time_major( + self, + train_batch.get(SampleBatch.SEQ_LENS), + values, + ) + model.tower_stats["vf_explained_var"] = explained_variance( + torch.reshape(loss.value_targets, [-1]), torch.reshape(values_batched, [-1]) + ) + + if self.config.get("_separate_vf_optimizer"): + return loss.loss_wo_vf, loss.vf_loss + else: + return loss.total_loss + + @override(TorchPolicyV2) + def stats_fn(self, train_batch: SampleBatch) -> Dict[str, TensorType]: + return convert_to_numpy( + { + "cur_lr": self.cur_lr, + "total_loss": torch.mean( + torch.stack(self.get_tower_stats("total_loss")) + ), + "policy_loss": torch.mean(torch.stack(self.get_tower_stats("pi_loss"))), + "entropy": torch.mean( + torch.stack(self.get_tower_stats("mean_entropy")) + ), + "entropy_coeff": self.entropy_coeff, + "var_gnorm": global_norm(self.model.trainable_variables()), + "vf_loss": torch.mean(torch.stack(self.get_tower_stats("vf_loss"))), + "vf_explained_var": torch.mean( + torch.stack(self.get_tower_stats("vf_explained_var")) + ), + } + ) + + @override(TorchPolicyV2) + def postprocess_trajectory( + self, + sample_batch: SampleBatch, + other_agent_batches: Optional[SampleBatch] = None, + episode=None, + ): + # Call super's postprocess_trajectory first. + # sample_batch = super().postprocess_trajectory( + # sample_batch, other_agent_batches, episode + # ) + + if self.config["vtrace"]: + # Add the SampleBatch.VALUES_BOOTSTRAPPED column, which we'll need + # inside the loss for vtrace calculations. + sample_batch = compute_bootstrap_value(sample_batch, self) + + return sample_batch + + @override(TorchPolicyV2) + def extra_grad_process( + self, optimizer: "torch.optim.Optimizer", loss: TensorType + ) -> Dict[str, TensorType]: + return apply_grad_clipping(self, optimizer, loss) + + @override(TorchPolicyV2) + def get_batch_divisibility_req(self) -> int: + return self.config["rollout_fragment_length"] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da3698f3fff23b57b84216352f1ebb3b9e8bf9fe Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__pycache__/impala_torch_learner.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__pycache__/impala_torch_learner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be9e8723e1c5db49509142eb8aa7912c7c719ab2 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__pycache__/impala_torch_learner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__pycache__/vtrace_torch_v2.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__pycache__/vtrace_torch_v2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63506ac1a833fd6367dfba4b16687cd73038febc Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/__pycache__/vtrace_torch_v2.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/impala_torch_learner.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/impala_torch_learner.py new file mode 100644 index 0000000000000000000000000000000000000000..f52250d16a1c297cd005f59de3a135e17540222c --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/impala_torch_learner.py @@ -0,0 +1,164 @@ +from typing import Dict + +from ray.rllib.algorithms.impala.impala import IMPALAConfig +from ray.rllib.algorithms.impala.impala_learner import IMPALALearner +from ray.rllib.algorithms.impala.torch.vtrace_torch_v2 import ( + make_time_major, + vtrace_torch, +) +from ray.rllib.core.columns import Columns +from ray.rllib.core.learner.learner import ENTROPY_KEY +from ray.rllib.core.learner.torch.torch_learner import TorchLearner +from ray.rllib.utils.annotations import override +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.typing import ModuleID, TensorType + +torch, nn = try_import_torch() + + +class IMPALATorchLearner(IMPALALearner, TorchLearner): + """Implements the IMPALA loss function in torch.""" + + @override(TorchLearner) + def compute_loss_for_module( + self, + *, + module_id: ModuleID, + config: IMPALAConfig, + batch: Dict, + fwd_out: Dict[str, TensorType], + ) -> TensorType: + module = self.module[module_id].unwrapped() + + # TODO (sven): Now that we do the +1ts trick to be less vulnerable about + # bootstrap values at the end of rollouts in the new stack, we might make + # this a more flexible, configurable parameter for users, e.g. + # `v_trace_seq_len` (independent of `rollout_fragment_length`). Separation + # of concerns (sampling vs learning). + rollout_frag_or_episode_len = config.get_rollout_fragment_length() + recurrent_seq_len = batch.get("seq_lens") + + loss_mask = batch[Columns.LOSS_MASK].float() + loss_mask_time_major = make_time_major( + loss_mask, + trajectory_len=rollout_frag_or_episode_len, + recurrent_seq_len=recurrent_seq_len, + ) + size_loss_mask = torch.sum(loss_mask) + + # Behavior actions logp and target actions logp. + behaviour_actions_logp = batch[Columns.ACTION_LOGP] + target_policy_dist = module.get_train_action_dist_cls().from_logits( + fwd_out[Columns.ACTION_DIST_INPUTS] + ) + target_actions_logp = target_policy_dist.logp(batch[Columns.ACTIONS]) + + # Values and bootstrap values. + values = module.compute_values( + batch, embeddings=fwd_out.get(Columns.EMBEDDINGS) + ) + values_time_major = make_time_major( + values, + trajectory_len=rollout_frag_or_episode_len, + recurrent_seq_len=recurrent_seq_len, + ) + assert Columns.VALUES_BOOTSTRAPPED not in batch + # Use as bootstrap values the vf-preds in the next "batch row", except + # for the very last row (which doesn't have a next row), for which the + # bootstrap value does not matter b/c it has a +1ts value at its end + # anyways. So we chose an arbitrary item (for simplicity of not having to + # move new data to the device). + bootstrap_values = torch.cat( + [ + values_time_major[0][1:], # 0th ts values from "next row" + values_time_major[0][0:1], # <- can use any arbitrary value here + ], + dim=0, + ) + + # TODO(Artur): In the old impala code, actions were unsqueezed if they were + # multi_discrete. Find out why and if we need to do the same here. + # actions = actions if is_multidiscrete else torch.unsqueeze(actions, dim=1) + target_actions_logp_time_major = make_time_major( + target_actions_logp, + trajectory_len=rollout_frag_or_episode_len, + recurrent_seq_len=recurrent_seq_len, + ) + behaviour_actions_logp_time_major = make_time_major( + behaviour_actions_logp, + trajectory_len=rollout_frag_or_episode_len, + recurrent_seq_len=recurrent_seq_len, + ) + rewards_time_major = make_time_major( + batch[Columns.REWARDS], + trajectory_len=rollout_frag_or_episode_len, + recurrent_seq_len=recurrent_seq_len, + ) + + # the discount factor that is used should be gamma except for timesteps where + # the episode is terminated. In that case, the discount factor should be 0. + discounts_time_major = ( + 1.0 + - make_time_major( + batch[Columns.TERMINATEDS], + trajectory_len=rollout_frag_or_episode_len, + recurrent_seq_len=recurrent_seq_len, + ).type(dtype=torch.float32) + ) * config.gamma + + # Note that vtrace will compute the main loop on the CPU for better performance. + vtrace_adjusted_target_values, pg_advantages = vtrace_torch( + target_action_log_probs=target_actions_logp_time_major, + behaviour_action_log_probs=behaviour_actions_logp_time_major, + discounts=discounts_time_major, + rewards=rewards_time_major, + values=values_time_major, + bootstrap_values=bootstrap_values, + clip_rho_threshold=config.vtrace_clip_rho_threshold, + clip_pg_rho_threshold=config.vtrace_clip_pg_rho_threshold, + ) + + # The policy gradients loss. + pi_loss = -torch.sum( + target_actions_logp_time_major * pg_advantages * loss_mask_time_major + ) + mean_pi_loss = pi_loss / size_loss_mask + + # The baseline loss. + delta = values_time_major - vtrace_adjusted_target_values + vf_loss = 0.5 * torch.sum(torch.pow(delta, 2.0) * loss_mask_time_major) + mean_vf_loss = vf_loss / size_loss_mask + + # The entropy loss. + entropy_loss = -torch.sum(target_policy_dist.entropy() * loss_mask) + mean_entropy_loss = entropy_loss / size_loss_mask + + # The summed weighted loss. + total_loss = ( + mean_pi_loss + + mean_vf_loss * config.vf_loss_coeff + + ( + mean_entropy_loss + * self.entropy_coeff_schedulers_per_module[ + module_id + ].get_current_value() + ) + ) + + # Log important loss stats. + self.metrics.log_dict( + { + "pi_loss": pi_loss, + "mean_pi_loss": mean_pi_loss, + "vf_loss": vf_loss, + "mean_vf_loss": mean_vf_loss, + ENTROPY_KEY: -mean_entropy_loss, + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + # Return the total loss. + return total_loss + + +ImpalaTorchLearner = IMPALATorchLearner diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/vtrace_torch_v2.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/vtrace_torch_v2.py new file mode 100644 index 0000000000000000000000000000000000000000..bf4c4fa9937321030c08a5f043c16ac6770d0644 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/torch/vtrace_torch_v2.py @@ -0,0 +1,169 @@ +from typing import List, Union + +from ray.rllib.utils.framework import try_import_torch + +torch, nn = try_import_torch() + + +def make_time_major( + tensor: Union["torch.Tensor", List["torch.Tensor"]], + *, + trajectory_len: int = None, + recurrent_seq_len: int = None, +): + """Swaps batch and trajectory axis. + + Args: + tensor: A tensor or list of tensors to swap the axis of. + NOTE: Each tensor must have the shape [B * T] where B is the batch size and + T is the trajectory length. + trajectory_len: The length of each trajectory being transformed. + If None then `recurrent_seq_len` must be set. + recurrent_seq_len: Sequence lengths if recurrent. + If None then `trajectory_len` must be set. + + Returns: + res: A tensor with swapped axes or a list of tensors with + swapped axes. + """ + if isinstance(tensor, (list, tuple)): + return [ + make_time_major(_tensor, trajectory_len, recurrent_seq_len) + for _tensor in tensor + ] + + assert ( + trajectory_len is not None or recurrent_seq_len is not None + ), "Either trajectory_len or recurrent_seq_len must be set." + + # Figure out the sizes of the final B and T axes. + if recurrent_seq_len is not None: + assert len(tensor.shape) == 2 + # Swap B and T axes. + tensor = torch.transpose(tensor, 1, 0) + return tensor + else: + T = trajectory_len + # Zero-pad, if necessary. + tensor_0 = tensor.shape[0] + B = tensor_0 // T + if B != (tensor_0 / T): + assert len(tensor.shape) == 1 + tensor = torch.cat( + [ + tensor, + torch.zeros( + trajectory_len - tensor_0 % T, + dtype=tensor.dtype, + device=tensor.device, + ), + ] + ) + B += 1 + + # Reshape tensor (break up B axis into 2 axes: B and T). + tensor = torch.reshape(tensor, [B, T] + list(tensor.shape[1:])) + + # Swap B and T axes. + tensor = torch.transpose(tensor, 1, 0) + + return tensor + + +def vtrace_torch( + *, + target_action_log_probs: "torch.Tensor", + behaviour_action_log_probs: "torch.Tensor", + discounts: "torch.Tensor", + rewards: "torch.Tensor", + values: "torch.Tensor", + bootstrap_values: "torch.Tensor", + clip_rho_threshold: Union[float, "torch.Tensor"] = 1.0, + clip_pg_rho_threshold: Union[float, "torch.Tensor"] = 1.0, +): + r"""V-trace for softmax policies implemented with torch. + + Calculates V-trace actor critic targets for softmax polices as described in + "IMPALA: Scalable Distributed Deep-RL with Importance Weighted Actor-Learner + Architectures" by Espeholt, Soyer, Munos et al. (https://arxiv.org/abs/1802.01561) + + The V-trace implementation used here closely resembles the one found in the + scalable-agent repository by Google DeepMind, available at + https://github.com/deepmind/scalable_agent. This version has been optimized to + minimize the number of floating-point operations required per V-Trace + calculation, achieved through the use of dynamic programming techniques. It's + important to note that the mathematical expressions used in this implementation + may appear quite different from those presented in the IMPALA paper. + + The following terminology applies: + - `target policy` refers to the policy we are interested in improving. + - `behaviour policy` refers to the policy that generated the given + rewards and actions. + - `T` refers to the time dimension. This is usually either the length of the + trajectory or the length of the sequence if recurrent. + - `B` refers to the batch size. + + Args: + target_action_log_probs: Action log probs from the target policy. A float32 + tensor of shape [T, B]. + behaviour_action_log_probs: Action log probs from the behaviour policy. A + float32 tensor of shape [T, B]. + discounts: A float32 tensor of shape [T, B] with the discount encountered when + following the behaviour policy. This will be 0 for terminal timesteps + (done=True) and gamma (the discount factor) otherwise. + rewards: A float32 tensor of shape [T, B] with the rewards generated by + following the behaviour policy. + values: A float32 tensor of shape [T, B] with the value function estimates + wrt. the target policy. + bootstrap_values: A float32 of shape [B] with the value function estimate at + time T. + clip_rho_threshold: A scalar float32 tensor with the clipping threshold for + importance weights (rho) when calculating the baseline targets (vs). + rho^bar in the paper. + clip_pg_rho_threshold: A scalar float32 tensor with the clipping threshold + on rho_s in \rho_s \delta log \pi(a|x) (r + \gamma v_{s+1} - V(x_s)). + """ + log_rhos = target_action_log_probs - behaviour_action_log_probs + + rhos = torch.exp(log_rhos) + if clip_rho_threshold is not None: + clipped_rhos = torch.clamp(rhos, max=clip_rho_threshold) + else: + clipped_rhos = rhos + + cs = torch.clamp(rhos, max=1.0) + # Append bootstrapped value to get [v1, ..., v_t+1] + values_t_plus_1 = torch.cat( + [values[1:], torch.unsqueeze(bootstrap_values, 0)], axis=0 + ) + + deltas = clipped_rhos * (rewards + discounts * values_t_plus_1 - values) + + # Note: The original IMPALA code (and paper) suggested to perform the following + # v-trace for-loop on the CPU, due to its sequential nature. However, modern GPUs + # are quite optimized for these shorted for-loops, which is why it should be faster + # nowadays to leave these operations on the GPU to avoid the GPU<>CPU transfer + # penalty. This penalty can actually be quite massive on the LEarner actors, given + # all other code is already well optimized. + vs_minus_v_xs = [torch.zeros_like(bootstrap_values, device=deltas.device)] + for i in reversed(range(len(discounts))): + discount_t, c_t, delta_t = discounts[i], cs[i], deltas[i] + vs_minus_v_xs.append(delta_t + discount_t * c_t * vs_minus_v_xs[-1]) + vs_minus_v_xs = torch.stack(vs_minus_v_xs[1:]) + + # Reverse the results back to original order. + vs_minus_v_xs = torch.flip(vs_minus_v_xs, dims=[0]) + + # Add V(x_s) to get v_s. + vs = torch.add(vs_minus_v_xs, values) + + # Advantage for policy gradient. + vs_t_plus_1 = torch.cat([vs[1:], torch.unsqueeze(bootstrap_values, 0)], axis=0) + if clip_pg_rho_threshold is not None: + clipped_pg_rhos = torch.clamp(rhos, max=clip_pg_rho_threshold) + else: + clipped_pg_rhos = rhos + pg_advantages = clipped_pg_rhos * (rewards + discounts * vs_t_plus_1 - values) + + # Make sure no gradients backpropagated through the returned values. + return torch.detach(vs), torch.detach(pg_advantages) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/utils.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f65b76a7f53de6fb3f436b8063341c057529f25b --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/utils.py @@ -0,0 +1,96 @@ +from collections import defaultdict, deque + +import numpy as np + + +class _SleepTimeController: + def __init__(self): + self.L = 0.0 + self.H = 0.4 + + self._recompute_candidates() + + # Defaultdict mapping. + self.results = defaultdict(lambda: deque(maxlen=3)) + + self.iteration = 0 + + def _recompute_candidates(self): + self.center = (self.L + self.H) / 2 + self.low = (self.L + self.center) / 2 + self.high = (self.H + self.center) / 2 + + # Expand a little if range becomes too narrow to avoid + # overoptimization. + if self.H - self.L < 0.00001: + self.L = max(self.center - 0.1, 0.0) + self.H = min(self.center + 0.1, 1.0) + self._recompute_candidates() + # Reduce results, just in case it has grown too much. + c, l, h = ( + self.results[self.center], + self.results[self.low], + self.results[self.high], + ) + self.results = defaultdict(lambda: deque(maxlen=3)) + self.results[self.center] = c + self.results[self.low] = l + self.results[self.high] = h + + @property + def current(self): + if len(self.results[self.center]) < 3: + return self.center + elif len(self.results[self.low]) < 3: + return self.low + else: + return self.high + + def log_result(self, performance): + self.iteration += 1 + + # Skip first 2 iterations for ignoring warm-up effect. + if self.iteration < 2: + return + + self.results[self.current].append(performance) + + # If all candidates have at least 3 results logged, re-evaluate + # and compute new L and H. + center, low, high = self.center, self.low, self.high + if ( + len(self.results[center]) == 3 + and len(self.results[low]) == 3 + and len(self.results[high]) == 3 + ): + perf_center = np.mean(self.results[center]) + perf_low = np.mean(self.results[low]) + perf_high = np.mean(self.results[high]) + # Case: `center` is best. + if perf_center > perf_low and perf_center > perf_high: + self.L = low + self.H = high + # Erase low/high results: We'll not use these again. + self.results.pop(low, None) + self.results.pop(high, None) + # Case: `low` is best. + elif perf_low > perf_center and perf_low > perf_high: + self.H = center + # Erase center/high results: We'll not use these again. + self.results.pop(center, None) + self.results.pop(high, None) + # Case: `high` is best. + else: + self.L = center + # Erase center/low results: We'll not use these again. + self.results.pop(center, None) + self.results.pop(low, None) + + self._recompute_candidates() + + +if __name__ == "__main__": + controller = _SleepTimeController() + for _ in range(1000): + performance = np.random.random() + controller.log_result(performance) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/vtrace_tf.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/vtrace_tf.py new file mode 100644 index 0000000000000000000000000000000000000000..61c6a7e366d52e20a0d2c225749461c81c51b04d --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/vtrace_tf.py @@ -0,0 +1,425 @@ +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Functions to compute V-trace off-policy actor critic targets. + +For details and theory see: + +"IMPALA: Scalable Distributed Deep-RL with +Importance Weighted Actor-Learner Architectures" +by Espeholt, Soyer, Munos et al. + +See https://arxiv.org/abs/1802.01561 for the full paper. + +In addition to the original paper's code, changes have been made +to support MultiDiscrete action spaces. behaviour_policy_logits, +target_policy_logits and actions parameters in the entry point +multi_from_logits method accepts lists of tensors instead of just +tensors. +""" + +import collections + +from ray.rllib.models.tf.tf_action_dist import Categorical +from ray.rllib.utils.framework import try_import_tf + +tf1, tf, tfv = try_import_tf() + +VTraceFromLogitsReturns = collections.namedtuple( + "VTraceFromLogitsReturns", + [ + "vs", + "pg_advantages", + "log_rhos", + "behaviour_action_log_probs", + "target_action_log_probs", + ], +) + +VTraceReturns = collections.namedtuple("VTraceReturns", "vs pg_advantages") + + +def log_probs_from_logits_and_actions( + policy_logits, actions, dist_class=Categorical, model=None +): + return multi_log_probs_from_logits_and_actions( + [policy_logits], [actions], dist_class, model + )[0] + + +def multi_log_probs_from_logits_and_actions(policy_logits, actions, dist_class, model): + """Computes action log-probs from policy logits and actions. + + In the notation used throughout documentation and comments, T refers to the + time dimension ranging from 0 to T-1. B refers to the batch size and + ACTION_SPACE refers to the list of numbers each representing a number of + actions. + + Args: + policy_logits: A list with length of ACTION_SPACE of float32 + tensors of shapes [T, B, ACTION_SPACE[0]], ..., + [T, B, ACTION_SPACE[-1]] with un-normalized log-probabilities + parameterizing a softmax policy. + actions: A list with length of ACTION_SPACE of tensors of shapes + [T, B, ...], ..., [T, B, ...] + with actions. + dist_class: Python class of the action distribution. + + Returns: + A list with length of ACTION_SPACE of float32 tensors of shapes + [T, B], ..., [T, B] corresponding to the sampling log probability + of the chosen action w.r.t. the policy. + """ + log_probs = [] + for i in range(len(policy_logits)): + p_shape = tf.shape(policy_logits[i]) + a_shape = tf.shape(actions[i]) + policy_logits_flat = tf.reshape( + policy_logits[i], tf.concat([[-1], p_shape[2:]], axis=0) + ) + actions_flat = tf.reshape(actions[i], tf.concat([[-1], a_shape[2:]], axis=0)) + log_probs.append( + tf.reshape( + dist_class(policy_logits_flat, model).logp(actions_flat), a_shape[:2] + ) + ) + + return log_probs + + +def from_logits( + behaviour_policy_logits, + target_policy_logits, + actions, + discounts, + rewards, + values, + bootstrap_value, + dist_class=Categorical, + model=None, + clip_rho_threshold=1.0, + clip_pg_rho_threshold=1.0, + name="vtrace_from_logits", +): + """multi_from_logits wrapper used only for tests""" + + res = multi_from_logits( + [behaviour_policy_logits], + [target_policy_logits], + [actions], + discounts, + rewards, + values, + bootstrap_value, + dist_class, + model, + clip_rho_threshold=clip_rho_threshold, + clip_pg_rho_threshold=clip_pg_rho_threshold, + name=name, + ) + + return VTraceFromLogitsReturns( + vs=res.vs, + pg_advantages=res.pg_advantages, + log_rhos=res.log_rhos, + behaviour_action_log_probs=tf.squeeze(res.behaviour_action_log_probs, axis=0), + target_action_log_probs=tf.squeeze(res.target_action_log_probs, axis=0), + ) + + +def multi_from_logits( + behaviour_policy_logits, + target_policy_logits, + actions, + discounts, + rewards, + values, + bootstrap_value, + dist_class, + model, + behaviour_action_log_probs=None, + clip_rho_threshold=1.0, + clip_pg_rho_threshold=1.0, + name="vtrace_from_logits", +): + r"""V-trace for softmax policies. + + Calculates V-trace actor critic targets for softmax polices as described in + + "IMPALA: Scalable Distributed Deep-RL with + Importance Weighted Actor-Learner Architectures" + by Espeholt, Soyer, Munos et al. + + Target policy refers to the policy we are interested in improving and + behaviour policy refers to the policy that generated the given + rewards and actions. + + In the notation used throughout documentation and comments, T refers to the + time dimension ranging from 0 to T-1. B refers to the batch size and + ACTION_SPACE refers to the list of numbers each representing a number of + actions. + + Args: + behaviour_policy_logits: A list with length of ACTION_SPACE of float32 + tensors of shapes + [T, B, ACTION_SPACE[0]], + ..., + [T, B, ACTION_SPACE[-1]] + with un-normalized log-probabilities parameterizing the softmax behaviour + policy. + target_policy_logits: A list with length of ACTION_SPACE of float32 + tensors of shapes + [T, B, ACTION_SPACE[0]], + ..., + [T, B, ACTION_SPACE[-1]] + with un-normalized log-probabilities parameterizing the softmax target + policy. + actions: A list with length of ACTION_SPACE of + tensors of shapes + [T, B, ...], + ..., + [T, B, ...] + with actions sampled from the behaviour policy. + discounts: A float32 tensor of shape [T, B] with the discount encountered + when following the behaviour policy. + rewards: A float32 tensor of shape [T, B] with the rewards generated by + following the behaviour policy. + values: A float32 tensor of shape [T, B] with the value function estimates + wrt. the target policy. + bootstrap_value: A float32 of shape [B] with the value function estimate at + time T. + dist_class: action distribution class for the logits. + model: backing ModelV2 instance + behaviour_action_log_probs: precalculated values of the behaviour actions + clip_rho_threshold: A scalar float32 tensor with the clipping threshold for + importance weights (rho) when calculating the baseline targets (vs). + rho^bar in the paper. + clip_pg_rho_threshold: A scalar float32 tensor with the clipping threshold + on rho_s in \rho_s \delta log \pi(a|x) (r + \gamma v_{s+1} - V(x_s)). + name: The name scope that all V-trace operations will be created in. + + Returns: + A `VTraceFromLogitsReturns` namedtuple with the following fields: + vs: A float32 tensor of shape [T, B]. Can be used as target to train a + baseline (V(x_t) - vs_t)^2. + pg_advantages: A float 32 tensor of shape [T, B]. Can be used as an + estimate of the advantage in the calculation of policy gradients. + log_rhos: A float32 tensor of shape [T, B] containing the log importance + sampling weights (log rhos). + behaviour_action_log_probs: A float32 tensor of shape [T, B] containing + behaviour policy action log probabilities (log \mu(a_t)). + target_action_log_probs: A float32 tensor of shape [T, B] containing + target policy action probabilities (log \pi(a_t)). + """ + + for i in range(len(behaviour_policy_logits)): + behaviour_policy_logits[i] = tf.convert_to_tensor( + behaviour_policy_logits[i], dtype=tf.float32 + ) + target_policy_logits[i] = tf.convert_to_tensor( + target_policy_logits[i], dtype=tf.float32 + ) + + # Make sure tensor ranks are as expected. + # The rest will be checked by from_action_log_probs. + behaviour_policy_logits[i].shape.assert_has_rank(3) + target_policy_logits[i].shape.assert_has_rank(3) + + with tf1.name_scope( + name, + values=[ + behaviour_policy_logits, + target_policy_logits, + actions, + discounts, + rewards, + values, + bootstrap_value, + ], + ): + target_action_log_probs = multi_log_probs_from_logits_and_actions( + target_policy_logits, actions, dist_class, model + ) + + if len(behaviour_policy_logits) > 1 or behaviour_action_log_probs is None: + # can't use precalculated values, recompute them. Note that + # recomputing won't work well for autoregressive action dists + # which may have variables not captured by 'logits' + behaviour_action_log_probs = multi_log_probs_from_logits_and_actions( + behaviour_policy_logits, actions, dist_class, model + ) + + log_rhos = get_log_rhos(target_action_log_probs, behaviour_action_log_probs) + + vtrace_returns = from_importance_weights( + log_rhos=log_rhos, + discounts=discounts, + rewards=rewards, + values=values, + bootstrap_value=bootstrap_value, + clip_rho_threshold=clip_rho_threshold, + clip_pg_rho_threshold=clip_pg_rho_threshold, + ) + + return VTraceFromLogitsReturns( + log_rhos=log_rhos, + behaviour_action_log_probs=behaviour_action_log_probs, + target_action_log_probs=target_action_log_probs, + **vtrace_returns._asdict() + ) + + +def from_importance_weights( + log_rhos, + discounts, + rewards, + values, + bootstrap_value, + clip_rho_threshold=1.0, + clip_pg_rho_threshold=1.0, + name="vtrace_from_importance_weights", +): + r"""V-trace from log importance weights. + + Calculates V-trace actor critic targets as described in + + "IMPALA: Scalable Distributed Deep-RL with + Importance Weighted Actor-Learner Architectures" + by Espeholt, Soyer, Munos et al. + + In the notation used throughout documentation and comments, T refers to the + time dimension ranging from 0 to T-1. B refers to the batch size. This code + also supports the case where all tensors have the same number of additional + dimensions, e.g., `rewards` is [T, B, C], `values` is [T, B, C], + `bootstrap_value` is [B, C]. + + Args: + log_rhos: A float32 tensor of shape [T, B] representing the + log importance sampling weights, i.e. + log(target_policy(a) / behaviour_policy(a)). V-trace performs operations + on rhos in log-space for numerical stability. + discounts: A float32 tensor of shape [T, B] with discounts encountered when + following the behaviour policy. + rewards: A float32 tensor of shape [T, B] containing rewards generated by + following the behaviour policy. + values: A float32 tensor of shape [T, B] with the value function estimates + wrt. the target policy. + bootstrap_value: A float32 of shape [B] with the value function estimate at + time T. + clip_rho_threshold: A scalar float32 tensor with the clipping threshold for + importance weights (rho) when calculating the baseline targets (vs). + rho^bar in the paper. If None, no clipping is applied. + clip_pg_rho_threshold: A scalar float32 tensor with the clipping threshold + on rho_s in \rho_s \delta log \pi(a|x) (r + \gamma v_{s+1} - V(x_s)). If + None, no clipping is applied. + name: The name scope that all V-trace operations will be created in. + + Returns: + A VTraceReturns namedtuple (vs, pg_advantages) where: + vs: A float32 tensor of shape [T, B]. Can be used as target to + train a baseline (V(x_t) - vs_t)^2. + pg_advantages: A float32 tensor of shape [T, B]. Can be used as the + advantage in the calculation of policy gradients. + """ + log_rhos = tf.convert_to_tensor(log_rhos, dtype=tf.float32) + discounts = tf.convert_to_tensor(discounts, dtype=tf.float32) + rewards = tf.convert_to_tensor(rewards, dtype=tf.float32) + values = tf.convert_to_tensor(values, dtype=tf.float32) + bootstrap_value = tf.convert_to_tensor(bootstrap_value, dtype=tf.float32) + if clip_rho_threshold is not None: + clip_rho_threshold = tf.convert_to_tensor(clip_rho_threshold, dtype=tf.float32) + if clip_pg_rho_threshold is not None: + clip_pg_rho_threshold = tf.convert_to_tensor( + clip_pg_rho_threshold, dtype=tf.float32 + ) + + # Make sure tensor ranks are consistent. + rho_rank = log_rhos.shape.ndims # Usually 2. + values.shape.assert_has_rank(rho_rank) + bootstrap_value.shape.assert_has_rank(rho_rank - 1) + discounts.shape.assert_has_rank(rho_rank) + rewards.shape.assert_has_rank(rho_rank) + if clip_rho_threshold is not None: + clip_rho_threshold.shape.assert_has_rank(0) + if clip_pg_rho_threshold is not None: + clip_pg_rho_threshold.shape.assert_has_rank(0) + + with tf1.name_scope( + name, values=[log_rhos, discounts, rewards, values, bootstrap_value] + ): + rhos = tf.math.exp(log_rhos) + if clip_rho_threshold is not None: + clipped_rhos = tf.minimum(clip_rho_threshold, rhos, name="clipped_rhos") + else: + clipped_rhos = rhos + + cs = tf.minimum(1.0, rhos, name="cs") + # Append bootstrapped value to get [v1, ..., v_t+1] + values_t_plus_1 = tf.concat( + [values[1:], tf.expand_dims(bootstrap_value, 0)], axis=0 + ) + deltas = clipped_rhos * (rewards + discounts * values_t_plus_1 - values) + + # All sequences are reversed, computation starts from the back. + sequences = ( + tf.reverse(discounts, axis=[0]), + tf.reverse(cs, axis=[0]), + tf.reverse(deltas, axis=[0]), + ) + + # V-trace vs are calculated through a scan from the back to the + # beginning of the given trajectory. + def scanfunc(acc, sequence_item): + discount_t, c_t, delta_t = sequence_item + return delta_t + discount_t * c_t * acc + + initial_values = tf.zeros_like(bootstrap_value) + vs_minus_v_xs = tf.nest.map_structure( + tf.stop_gradient, + tf.scan( + fn=scanfunc, + elems=sequences, + initializer=initial_values, + parallel_iterations=1, + name="scan", + ), + ) + # Reverse the results back to original order. + vs_minus_v_xs = tf.reverse(vs_minus_v_xs, [0], name="vs_minus_v_xs") + + # Add V(x_s) to get v_s. + vs = tf.add(vs_minus_v_xs, values, name="vs") + + # Advantage for policy gradient. + vs_t_plus_1 = tf.concat([vs[1:], tf.expand_dims(bootstrap_value, 0)], axis=0) + if clip_pg_rho_threshold is not None: + clipped_pg_rhos = tf.minimum( + clip_pg_rho_threshold, rhos, name="clipped_pg_rhos" + ) + else: + clipped_pg_rhos = rhos + pg_advantages = clipped_pg_rhos * (rewards + discounts * vs_t_plus_1 - values) + + # Make sure no gradients backpropagated through the returned values. + return VTraceReturns( + vs=tf.stop_gradient(vs), pg_advantages=tf.stop_gradient(pg_advantages) + ) + + +def get_log_rhos(target_action_log_probs, behaviour_action_log_probs): + """With the selected log_probs for multi-discrete actions of behaviour + and target policies we compute the log_rhos for calculating the vtrace.""" + t = tf.stack(target_action_log_probs) + b = tf.stack(behaviour_action_log_probs) + log_rhos = tf.reduce_sum(t - b, axis=0) + return log_rhos diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/impala/vtrace_torch.py b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/vtrace_torch.py new file mode 100644 index 0000000000000000000000000000000000000000..b63a5181c7ac81e3ccd10477f7b6728a66cfe72b --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/impala/vtrace_torch.py @@ -0,0 +1,359 @@ +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""PyTorch version of the functions to compute V-trace off-policy actor critic +targets. + +For details and theory see: + +"IMPALA: Scalable Distributed Deep-RL with +Importance Weighted Actor-Learner Architectures" +by Espeholt, Soyer, Munos et al. + +See https://arxiv.org/abs/1802.01561 for the full paper. + +In addition to the original paper's code, changes have been made +to support MultiDiscrete action spaces. behaviour_policy_logits, +target_policy_logits and actions parameters in the entry point +multi_from_logits method accepts lists of tensors instead of just +tensors. +""" + +from ray.rllib.algorithms.impala.vtrace_tf import VTraceFromLogitsReturns, VTraceReturns +from ray.rllib.models.torch.torch_action_dist import TorchCategorical +from ray.rllib.utils import force_list +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.torch_utils import convert_to_torch_tensor + +torch, nn = try_import_torch() + + +def log_probs_from_logits_and_actions( + policy_logits, actions, dist_class=TorchCategorical, model=None +): + return multi_log_probs_from_logits_and_actions( + [policy_logits], [actions], dist_class, model + )[0] + + +def multi_log_probs_from_logits_and_actions(policy_logits, actions, dist_class, model): + """Computes action log-probs from policy logits and actions. + + In the notation used throughout documentation and comments, T refers to the + time dimension ranging from 0 to T-1. B refers to the batch size and + ACTION_SPACE refers to the list of numbers each representing a number of + actions. + + Args: + policy_logits: A list with length of ACTION_SPACE of float32 + tensors of shapes [T, B, ACTION_SPACE[0]], ..., + [T, B, ACTION_SPACE[-1]] with un-normalized log-probabilities + parameterizing a softmax policy. + actions: A list with length of ACTION_SPACE of tensors of shapes + [T, B, ...], ..., [T, B, ...] + with actions. + dist_class: Python class of the action distribution. + + Returns: + A list with length of ACTION_SPACE of float32 tensors of shapes + [T, B], ..., [T, B] corresponding to the sampling log probability + of the chosen action w.r.t. the policy. + """ + log_probs = [] + for i in range(len(policy_logits)): + p_shape = policy_logits[i].shape + a_shape = actions[i].shape + policy_logits_flat = torch.reshape(policy_logits[i], (-1,) + tuple(p_shape[2:])) + actions_flat = torch.reshape(actions[i], (-1,) + tuple(a_shape[2:])) + log_probs.append( + torch.reshape( + dist_class(policy_logits_flat, model).logp(actions_flat), a_shape[:2] + ) + ) + + return log_probs + + +def from_logits( + behaviour_policy_logits, + target_policy_logits, + actions, + discounts, + rewards, + values, + bootstrap_value, + dist_class=TorchCategorical, + model=None, + clip_rho_threshold=1.0, + clip_pg_rho_threshold=1.0, +): + """multi_from_logits wrapper used only for tests""" + + res = multi_from_logits( + [behaviour_policy_logits], + [target_policy_logits], + [actions], + discounts, + rewards, + values, + bootstrap_value, + dist_class, + model, + clip_rho_threshold=clip_rho_threshold, + clip_pg_rho_threshold=clip_pg_rho_threshold, + ) + + assert len(res.behaviour_action_log_probs) == 1 + assert len(res.target_action_log_probs) == 1 + return VTraceFromLogitsReturns( + vs=res.vs, + pg_advantages=res.pg_advantages, + log_rhos=res.log_rhos, + behaviour_action_log_probs=res.behaviour_action_log_probs[0], + target_action_log_probs=res.target_action_log_probs[0], + ) + + +def multi_from_logits( + behaviour_policy_logits, + target_policy_logits, + actions, + discounts, + rewards, + values, + bootstrap_value, + dist_class, + model, + behaviour_action_log_probs=None, + clip_rho_threshold=1.0, + clip_pg_rho_threshold=1.0, +): + r"""V-trace for softmax policies. + + Calculates V-trace actor critic targets for softmax polices as described in + + "IMPALA: Scalable Distributed Deep-RL with + Importance Weighted Actor-Learner Architectures" + by Espeholt, Soyer, Munos et al. + + Target policy refers to the policy we are interested in improving and + behaviour policy refers to the policy that generated the given + rewards and actions. + + In the notation used throughout documentation and comments, T refers to the + time dimension ranging from 0 to T-1. B refers to the batch size and + ACTION_SPACE refers to the list of numbers each representing a number of + actions. + + Args: + behaviour_policy_logits: A list with length of ACTION_SPACE of float32 + tensors of shapes [T, B, ACTION_SPACE[0]], ..., + [T, B, ACTION_SPACE[-1]] with un-normalized log-probabilities + parameterizing the softmax behavior policy. + target_policy_logits: A list with length of ACTION_SPACE of float32 + tensors of shapes [T, B, ACTION_SPACE[0]], ..., + [T, B, ACTION_SPACE[-1]] with un-normalized log-probabilities + parameterizing the softmax target policy. + actions: A list with length of ACTION_SPACE of tensors of shapes + [T, B, ...], ..., [T, B, ...] + with actions sampled from the behavior policy. + discounts: A float32 tensor of shape [T, B] with the discount + encountered when following the behavior policy. + rewards: A float32 tensor of shape [T, B] with the rewards generated by + following the behavior policy. + values: A float32 tensor of shape [T, B] with the value function + estimates wrt. the target policy. + bootstrap_value: A float32 of shape [B] with the value function + estimate at time T. + dist_class: action distribution class for the logits. + model: backing ModelV2 instance + behaviour_action_log_probs: Precalculated values of the behavior + actions. + clip_rho_threshold: A scalar float32 tensor with the clipping threshold + for importance weights (rho) when calculating the baseline targets + (vs). rho^bar in the paper. + clip_pg_rho_threshold: A scalar float32 tensor with the clipping + threshold on rho_s in: + \rho_s \delta log \pi(a|x) (r + \gamma v_{s+1} - V(x_s)). + + Returns: + A `VTraceFromLogitsReturns` namedtuple with the following fields: + vs: A float32 tensor of shape [T, B]. Can be used as target to train a + baseline (V(x_t) - vs_t)^2. + pg_advantages: A float 32 tensor of shape [T, B]. Can be used as an + estimate of the advantage in the calculation of policy gradients. + log_rhos: A float32 tensor of shape [T, B] containing the log + importance sampling weights (log rhos). + behaviour_action_log_probs: A float32 tensor of shape [T, B] containing + behaviour policy action log probabilities (log \mu(a_t)). + target_action_log_probs: A float32 tensor of shape [T, B] containing + target policy action probabilities (log \pi(a_t)). + """ + + behaviour_policy_logits = convert_to_torch_tensor( + behaviour_policy_logits, device="cpu" + ) + target_policy_logits = convert_to_torch_tensor(target_policy_logits, device="cpu") + actions = convert_to_torch_tensor(actions, device="cpu") + + # Make sure tensor ranks are as expected. + # The rest will be checked by from_action_log_probs. + for i in range(len(behaviour_policy_logits)): + assert len(behaviour_policy_logits[i].size()) == 3 + assert len(target_policy_logits[i].size()) == 3 + + target_action_log_probs = multi_log_probs_from_logits_and_actions( + target_policy_logits, actions, dist_class, model + ) + + if len(behaviour_policy_logits) > 1 or behaviour_action_log_probs is None: + # can't use precalculated values, recompute them. Note that + # recomputing won't work well for autoregressive action dists + # which may have variables not captured by 'logits' + behaviour_action_log_probs = multi_log_probs_from_logits_and_actions( + behaviour_policy_logits, actions, dist_class, model + ) + + behaviour_action_log_probs = convert_to_torch_tensor( + behaviour_action_log_probs, device="cpu" + ) + behaviour_action_log_probs = force_list(behaviour_action_log_probs) + # log_rhos = target_logp - behavior_logp + log_rhos = get_log_rhos(target_action_log_probs, behaviour_action_log_probs) + + vtrace_returns = from_importance_weights( + log_rhos=log_rhos, + discounts=discounts, + rewards=rewards, + values=values, + bootstrap_value=bootstrap_value, + clip_rho_threshold=clip_rho_threshold, + clip_pg_rho_threshold=clip_pg_rho_threshold, + ) + + return VTraceFromLogitsReturns( + log_rhos=log_rhos, + behaviour_action_log_probs=behaviour_action_log_probs, + target_action_log_probs=target_action_log_probs, + **vtrace_returns._asdict() + ) + + +def from_importance_weights( + log_rhos, + discounts, + rewards, + values, + bootstrap_value, + clip_rho_threshold=1.0, + clip_pg_rho_threshold=1.0, +): + r"""V-trace from log importance weights. + + Calculates V-trace actor critic targets as described in + + "IMPALA: Scalable Distributed Deep-RL with + Importance Weighted Actor-Learner Architectures" + by Espeholt, Soyer, Munos et al. + + In the notation used throughout documentation and comments, T refers to the + time dimension ranging from 0 to T-1. B refers to the batch size. This code + also supports the case where all tensors have the same number of additional + dimensions, e.g., `rewards` is [T, B, C], `values` is [T, B, C], + `bootstrap_value` is [B, C]. + + Args: + log_rhos: A float32 tensor of shape [T, B] representing the log + importance sampling weights, i.e. + log(target_policy(a) / behaviour_policy(a)). V-trace performs + operations on rhos in log-space for numerical stability. + discounts: A float32 tensor of shape [T, B] with discounts encountered + when following the behaviour policy. + rewards: A float32 tensor of shape [T, B] containing rewards generated + by following the behaviour policy. + values: A float32 tensor of shape [T, B] with the value function + estimates wrt. the target policy. + bootstrap_value: A float32 of shape [B] with the value function + estimate at time T. + clip_rho_threshold: A scalar float32 tensor with the clipping threshold + for importance weights (rho) when calculating the baseline targets + (vs). rho^bar in the paper. If None, no clipping is applied. + clip_pg_rho_threshold: A scalar float32 tensor with the clipping + threshold on rho_s in + \rho_s \delta log \pi(a|x) (r + \gamma v_{s+1} - V(x_s)). + If None, no clipping is applied. + + Returns: + A VTraceReturns namedtuple (vs, pg_advantages) where: + vs: A float32 tensor of shape [T, B]. Can be used as target to + train a baseline (V(x_t) - vs_t)^2. + pg_advantages: A float32 tensor of shape [T, B]. Can be used as the + advantage in the calculation of policy gradients. + """ + log_rhos = convert_to_torch_tensor(log_rhos, device="cpu") + discounts = convert_to_torch_tensor(discounts, device="cpu") + rewards = convert_to_torch_tensor(rewards, device="cpu") + values = convert_to_torch_tensor(values, device="cpu") + bootstrap_value = convert_to_torch_tensor(bootstrap_value, device="cpu") + + # Make sure tensor ranks are consistent. + rho_rank = len(log_rhos.size()) # Usually 2. + assert rho_rank == len(values.size()) + assert rho_rank - 1 == len(bootstrap_value.size()), "must have rank {}".format( + rho_rank - 1 + ) + assert rho_rank == len(discounts.size()) + assert rho_rank == len(rewards.size()) + + rhos = torch.exp(log_rhos) + if clip_rho_threshold is not None: + clipped_rhos = torch.clamp_max(rhos, clip_rho_threshold) + else: + clipped_rhos = rhos + + cs = torch.clamp_max(rhos, 1.0) + # Append bootstrapped value to get [v1, ..., v_t+1] + values_t_plus_1 = torch.cat( + [values[1:], torch.unsqueeze(bootstrap_value, 0)], dim=0 + ) + deltas = clipped_rhos * (rewards + discounts * values_t_plus_1 - values) + + vs_minus_v_xs = [torch.zeros_like(bootstrap_value)] + for i in reversed(range(len(discounts))): + discount_t, c_t, delta_t = discounts[i], cs[i], deltas[i] + vs_minus_v_xs.append(delta_t + discount_t * c_t * vs_minus_v_xs[-1]) + vs_minus_v_xs = torch.stack(vs_minus_v_xs[1:]) + # Reverse the results back to original order. + vs_minus_v_xs = torch.flip(vs_minus_v_xs, dims=[0]) + # Add V(x_s) to get v_s. + vs = vs_minus_v_xs + values + + # Advantage for policy gradient. + vs_t_plus_1 = torch.cat([vs[1:], torch.unsqueeze(bootstrap_value, 0)], dim=0) + if clip_pg_rho_threshold is not None: + clipped_pg_rhos = torch.clamp_max(rhos, clip_pg_rho_threshold) + else: + clipped_pg_rhos = rhos + pg_advantages = clipped_pg_rhos * (rewards + discounts * vs_t_plus_1 - values) + + # Make sure no gradients backpropagated through the returned values. + return VTraceReturns(vs=vs.detach(), pg_advantages=pg_advantages.detach()) + + +def get_log_rhos(target_action_log_probs, behaviour_action_log_probs): + """With the selected log_probs for multi-discrete actions of behavior + and target policies we compute the log_rhos for calculating the vtrace.""" + t = torch.stack(target_action_log_probs) + b = torch.stack(behaviour_action_log_probs) + log_rhos = torch.sum(t - b, dim=0) + return log_rhos diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..404fb83b6aac79414874cc32c7ee0bb5c13ab7fe --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__init__.py @@ -0,0 +1,6 @@ +from ray.rllib.algorithms.iql.iql import IQL, IQLConfig + +__all__ = [ + "IQL", + "IQLConfig", +] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37675a0c63fd46b825ab1267a24ec6243ad3feb8 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/default_iql_rl_module.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/default_iql_rl_module.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9080c3e17dc999c16a1144fa7d324acb1509b007 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/default_iql_rl_module.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/iql.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/iql.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0aa03666ab41dc892ceab03238b2730dfda8c563 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/iql.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/iql_learner.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/iql_learner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..658ead61765c9d222bf341f458903834bec7d944 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/__pycache__/iql_learner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/default_iql_rl_module.py b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/default_iql_rl_module.py new file mode 100644 index 0000000000000000000000000000000000000000..95596bd8b91da9bb2c3d1d41c63a5c608d7a8962 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/default_iql_rl_module.py @@ -0,0 +1,35 @@ +from ray.rllib.algorithms.sac.default_sac_rl_module import DefaultSACRLModule +from ray.rllib.core.models.configs import MLPHeadConfig +from ray.rllib.core.rl_module.apis.value_function_api import ValueFunctionAPI +from ray.rllib.utils.annotations import ( + OverrideToImplementCustomLogic_CallToSuperRecommended, + override, +) + + +class DefaultIQLRLModule(DefaultSACRLModule, ValueFunctionAPI): + @override(DefaultSACRLModule) + def setup(self): + # Setup the `DefaultSACRLModule` to get the catalog. + super().setup() + + # Only, if the `RLModule` is used on a `Learner` we build the value network. + if not self.inference_only: + # Build the encoder for the value function. + self.vf_encoder = self.catalog.build_encoder(framework=self.framework) + + # Build the vf head. + self.vf = MLPHeadConfig( + input_dims=self.catalog.latent_dims, + # Note, we use the same layers as for the policy and Q-network. + hidden_layer_dims=self.catalog.pi_and_qf_head_hiddens, + hidden_layer_activation=self.catalog.pi_and_qf_head_activation, + output_layer_activation="linear", + output_layer_dim=1, + ).build(framework=self.framework) + + @override(DefaultSACRLModule) + @OverrideToImplementCustomLogic_CallToSuperRecommended + def get_non_inference_attributes(self): + # Use all of `super`'s attributes and add the value function attributes. + return super().get_non_inference_attributes() + ["vf_encoder", "vf"] diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/iql.py b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/iql.py new file mode 100644 index 0000000000000000000000000000000000000000..8935550027084e6b243e75c7798e4812e28c0661 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/iql.py @@ -0,0 +1,228 @@ +from typing import Optional, Type, Union + +from ray.rllib.algorithms.algorithm_config import AlgorithmConfig, NotProvided +from ray.rllib.algorithms.marwil.marwil import MARWIL, MARWILConfig +from ray.rllib.connectors.common.add_observations_from_episodes_to_batch import ( + AddObservationsFromEpisodesToBatch, +) +from ray.rllib.connectors.learner.add_next_observations_from_episodes_to_train_batch import ( # noqa + AddNextObservationsFromEpisodesToTrainBatch, +) +from ray.rllib.core.learner.learner import Learner +from ray.rllib.core.rl_module.rl_module import RLModuleSpec +from ray.rllib.utils.annotations import override +from ray.rllib.utils.typing import LearningRateOrSchedule, RLModuleSpecType + + +class IQLConfig(MARWILConfig): + """Defines a configuration class from which a new IQL Algorithm can be built + + .. testcode:: + :skipif: True + + from ray.rllib.algorithms.iql import IQLConfig + # Run this from the ray directory root. + config = IQLConfig().training(actor_lr=0.00001, gamma=0.99) + config = config.offline_data( + input_="./rllib/tests/data/pendulum/pendulum-v1_enormous") + + # Build an Algorithm object from the config and run 1 training iteration. + algo = config.build() + algo.train() + + .. testcode:: + :skipif: True + + from ray.rllib.algorithms.iql import IQLConfig + from ray import tune + config = IQLConfig() + # Print out some default values. + print(config.beta) + # Update the config object. + config.training( + lr=tune.grid_search([0.001, 0.0001]), beta=0.75 + ) + # Set the config object's data path. + # Run this from the ray directory root. + config.offline_data( + input_="./rllib/tests/data/pendulum-v1_enormous" + ) + # Set the config object's env, used for evaluation. + config.environment(env="Pendulum-v1") + # Use to_dict() to get the old-style python config dict + # when running with tune. + tune.Tuner( + "IQL", + param_space=config.to_dict(), + ).fit() + """ + + def __init__(self, algo_class=None): + super().__init__(algo_class=algo_class or IQL) + + # fmt: off + # __sphinx_doc_begin__ + # The temperature for the actor loss. + self.beta = 0.1 + + # The expectile to use in expectile regression. + self.expectile = 0.8 + + # The learning rates for the actor, critic and value network(s). + self.actor_lr = 3e-4 + self.critic_lr = 3e-4 + self.value_lr = 3e-4 + # Set `lr` parameter to `None` and ensure it is not used. + self.lr = None + + # If a twin-Q architecture should be used (advisable). + self.twin_q = True + + # How often the target network should be updated. + self.target_network_update_freq = 0 + # The weight for Polyak averaging. + self.tau = 1.0 + + # __sphinx_doc_end__ + # fmt: on + + @override(MARWILConfig) + def training( + self, + *, + twin_q: Optional[bool] = NotProvided, + expectile: Optional[float] = NotProvided, + actor_lr: Optional[LearningRateOrSchedule] = NotProvided, + critic_lr: Optional[LearningRateOrSchedule] = NotProvided, + value_lr: Optional[LearningRateOrSchedule] = NotProvided, + target_network_update_freq: Optional[int] = NotProvided, + tau: Optional[float] = NotProvided, + **kwargs, + ) -> "IQLConfig": + """Sets the training related configuration. + + Args: + beta: The temperature to scaling advantages in exponential terms. + Must be >> 0.0. The higher this parameter the less greedy + (exploitative) the policy becomes. It also means that the policy + is fitting less to the best actions in the dataset. + twin_q: If a twin-Q architecture should be used (advisable). + expectile: The expectile to use in expectile regression for the value + function. For high expectiles the value function tries to match + the upper tail of the Q-value distribution. + actor_lr: The learning rate for the actor network. Actor learning rates + greater than critic learning rates work well in experiments. + critic_lr: The learning rate for the Q-network. Critic learning rates + greater than value function learning rates work well in experiments. + value_lr: The learning rate for the value function network. + target_network_update_freq: The number of timesteps in between the target + Q-network is fixed. Note, too high values here could harm convergence. + The target network is updated via Polyak-averaging. + tau: The update parameter for Polyak-averaging of the target Q-network. + The higher this value the faster the weights move towards the actual + Q-network. + + Return: + This updated `AlgorithmConfig` object. + """ + super().training(**kwargs) + + if twin_q is not NotProvided: + self.twin_q = twin_q + if expectile is not NotProvided: + self.expectile = expectile + if actor_lr is not NotProvided: + self.actor_lr = actor_lr + if critic_lr is not NotProvided: + self.critic_lr = critic_lr + if value_lr is not NotProvided: + self.value_lr = value_lr + if target_network_update_freq is not NotProvided: + self.target_network_update_freq = target_network_update_freq + if tau is not NotProvided: + self.tau = tau + + return self + + @override(MARWILConfig) + def get_default_learner_class(self) -> Union[Type["Learner"], str]: + if self.framework_str == "torch": + from ray.rllib.algorithms.iql.torch.iql_torch_learner import IQLTorchLearner + + return IQLTorchLearner + else: + raise ValueError( + f"The framework {self.framework_str} is not supported. " + "Use `'torch'` instead." + ) + + @override(MARWILConfig) + def get_default_rl_module_spec(self) -> RLModuleSpecType: + if self.framework_str == "torch": + from ray.rllib.algorithms.iql.torch.default_iql_torch_rl_module import ( + DefaultIQLTorchRLModule, + ) + + return RLModuleSpec(module_class=DefaultIQLTorchRLModule) + else: + raise ValueError( + f"The framework {self.framework_str} is not supported. " + "Use `torch` instead." + ) + + @override(MARWILConfig) + def build_learner_connector( + self, + input_observation_space, + input_action_space, + device=None, + ): + pipeline = super().build_learner_connector( + input_observation_space=input_observation_space, + input_action_space=input_action_space, + device=device, + ) + + # Remove unneeded connectors from the MARWIL connector pipeline. + pipeline.remove("AddOneTsToEpisodesAndTruncate") + pipeline.remove("GeneralAdvantageEstimation") + + # Prepend the "add-NEXT_OBS-from-episodes-to-train-batch" connector piece (right + # after the corresponding "add-OBS-..." default piece). + pipeline.insert_after( + AddObservationsFromEpisodesToBatch, + AddNextObservationsFromEpisodesToTrainBatch(), + ) + + return pipeline + + @override(MARWILConfig) + def validate(self) -> None: + # Call super's validation method. + super().validate() + + # Ensure hyperparameters are meaningful. + if self.beta <= 0.0: + self._value_error( + "For meaningful results, `beta` (temperature) parameter must be >> 0.0!" + ) + if not 0.0 < self.expectile < 1.0: + self._value_error( + "For meaningful results, `expectile` parameter must be in (0, 1)." + ) + + @property + def _model_config_auto_includes(self): + return super()._model_config_auto_includes | {"twin_q": self.twin_q} + + +class IQL(MARWIL): + """Implicit Q-learning (derived from MARWIL). + + Uses MARWIL training step. + """ + + @classmethod + @override(MARWIL) + def get_default_config(cls) -> AlgorithmConfig: + return IQLConfig() diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/iql_learner.py b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/iql_learner.py new file mode 100644 index 0000000000000000000000000000000000000000..ef2e2e83e15b5205ef0e88c4b08225a12c900099 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/iql_learner.py @@ -0,0 +1,84 @@ +from typing import Dict + +from ray.rllib.algorithms.dqn.dqn_learner import DQNLearner +from ray.rllib.utils.annotations import ( + OverrideToImplementCustomLogic_CallToSuperRecommended, + override, +) +from ray.rllib.utils.lambda_defaultdict import LambdaDefaultDict +from ray.rllib.utils.typing import ModuleID, TensorType + +QF_TARGET_PREDS = "qf_target_preds" +VF_PREDS_NEXT = "vf_preds_next" +VF_LOSS = "value_loss" + + +class IQLLearner(DQNLearner): + @OverrideToImplementCustomLogic_CallToSuperRecommended + @override(DQNLearner) + def build(self) -> None: + # Build the `DQNLearner` (builds the target network). + super().build() + + # Define the expectile parameter(s). + self.expectile: Dict[ModuleID, TensorType] = LambdaDefaultDict( + lambda module_id: self._get_tensor_variable( + # Note, we want to train with a certain expectile. + [self.config.get_config_for_module(module_id).expectile], + trainable=False, + ) + ) + + # Define the temperature for the actor advantage loss. + self.temperature: Dict[ModuleID, TensorType] = LambdaDefaultDict( + lambda module_id: self._get_tensor_variable( + # Note, we want to train with a certain expectile. + [self.config.get_config_for_module(module_id).beta], + trainable=False, + ) + ) + + # Store loss tensors here temporarily inside the loss function for (exact) + # consumption later by the compute gradients function. + # Keys=(module_id, optimizer_name), values=loss tensors (in-graph). + self._temp_losses = {} + + @override(DQNLearner) + def remove_module(self, module_id: ModuleID) -> None: + """Removes the expectile and temperature for removed modules.""" + # First call `super`'s `remove_module` method. + super().remove_module(module_id) + # Remove the expectile from the mapping. + self.expectile.pop(module_id, None) + # Remove the temperature from the mapping. + self.temperature.pop(module_id, None) + + @override(DQNLearner) + def add_module( + self, + *, + module_id, + module_spec, + config_overrides=None, + new_should_module_be_updated=None + ): + """Adds the expectile and temperature for new modules.""" + # First call `super`'s `add_module` method. + super().add_module( + module_id=module_id, + module_spec=module_spec, + config_overrides=config_overrides, + new_should_module_be_updated=new_should_module_be_updated, + ) + # Add the expectile to the mapping. + self.expectile[module_id] = self._get_tensor_variable( + # Note, we want to train with a certain expectile. + [self.config.get_config_for_module(module_id).beta], + trainable=False, + ) + # Add the temperature to the mapping. + self.temperature[module_id] = self._get_tensor_variable( + # Note, we want to train with a certain expectile. + [self.config.get_config_for_module(module_id).beta], + trainable=False, + ) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__init__.py b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a63246495cfd9c20777e77fd9e6c190e2d138693 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__pycache__/default_iql_torch_rl_module.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__pycache__/default_iql_torch_rl_module.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8473c707152a464b3958f7de1e4da7bfc3797537 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__pycache__/default_iql_torch_rl_module.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__pycache__/iql_torch_learner.cpython-312.pyc b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__pycache__/iql_torch_learner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa63b6b3cc995b9d8063e0e00d73a5313e9fc878 Binary files /dev/null and b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/__pycache__/iql_torch_learner.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/default_iql_torch_rl_module.py b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/default_iql_torch_rl_module.py new file mode 100644 index 0000000000000000000000000000000000000000..318dcc207533a8ed6e7665e4ce13fb87f06120fb --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/default_iql_torch_rl_module.py @@ -0,0 +1,79 @@ +from typing import Any, Dict, Optional + +import gymnasium as gym + +from ray.rllib.algorithms.iql.default_iql_rl_module import DefaultIQLRLModule +from ray.rllib.algorithms.iql.iql_learner import QF_TARGET_PREDS, VF_PREDS_NEXT +from ray.rllib.algorithms.sac.torch.default_sac_torch_rl_module import ( + DefaultSACTorchRLModule, +) +from ray.rllib.core.columns import Columns +from ray.rllib.core.models.base import ENCODER_OUT +from ray.rllib.core.rl_module.apis.value_function_api import ValueFunctionAPI +from ray.rllib.utils.annotations import override +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.typing import TensorType + +torch, nn = try_import_torch() + + +class DefaultIQLTorchRLModule(DefaultSACTorchRLModule, DefaultIQLRLModule): + + framework: str = "torch" + + @override(DefaultSACTorchRLModule) + def _forward_train(self, batch: Dict, **kwargs) -> Dict[str, Any]: + + # Right now, IQL runs only with continuous action spaces. + # TODO (simon): Implement it also for discrete action spaces. + if not isinstance(self.action_space, gym.spaces.Box): + raise ValueError( + f"Unsupported action space type: {type(self.action_space)}. " + "Only continuous action spaces are supported." + ) + + # Call the forward pass of the SAC module. + output = super()._forward_train(batch, **kwargs) + + # Create batches for the forward passes of the target Q-networks and the + # value function. + batch_curr = { + Columns.OBS: batch[Columns.OBS], + Columns.ACTIONS: batch[Columns.ACTIONS], + } + batch_next = {Columns.OBS: batch[Columns.NEXT_OBS]} + + # These target q-values are needed for the value loss and actor loss. + output[QF_TARGET_PREDS] = self._qf_forward_train_helper( + batch_curr, encoder=self.target_qf_encoder, head=self.target_qf + ) + # If a twin-Q architecture is used run its target Q-network. + if self.twin_q: + output[QF_TARGET_PREDS] = torch.min( + output[QF_TARGET_PREDS], + self._qf_forward_train_helper( + batch_curr, encoder=self.target_qf_twin_encoder, head=self.qf_twin + ), + ) + + # Compute values for the current observations. + output[Columns.VF_PREDS] = self.compute_values(batch_curr) + # The values of the next observations are needed for the critic loss. + output[VF_PREDS_NEXT] = self.compute_values(batch_next) + + return output + + @override(ValueFunctionAPI) + def compute_values( + self, + batch: Dict[str, Any], + embeddings: Optional[Any] = None, + ) -> TensorType: + # If no embeddings are provided make a forward pass on the encoder. + if embeddings is None: + embeddings = self.vf_encoder(batch)[ENCODER_OUT] + + # Value head. + vf_out = self.vf(embeddings) + # Squeeze out last dimension (single node value head). + return vf_out.squeeze(-1) diff --git a/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/iql_torch_learner.py b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/iql_torch_learner.py new file mode 100644 index 0000000000000000000000000000000000000000..54a4fd263caa799b186dbaba484158f2a13f2d14 --- /dev/null +++ b/lib/python3.12/site-packages/ray/rllib/algorithms/iql/torch/iql_torch_learner.py @@ -0,0 +1,245 @@ +from typing import Dict + +from ray.rllib.algorithms.algorithm_config import AlgorithmConfig +from ray.rllib.algorithms.dqn.dqn_learner import QF_LOSS_KEY, QF_PREDS +from ray.rllib.algorithms.iql.iql_learner import ( + QF_TARGET_PREDS, + VF_LOSS, + VF_PREDS_NEXT, + IQLLearner, +) +from ray.rllib.algorithms.sac.sac_learner import QF_TWIN_LOSS_KEY, QF_TWIN_PREDS +from ray.rllib.core import ALL_MODULES +from ray.rllib.core.columns import Columns +from ray.rllib.core.learner.learner import ( + POLICY_LOSS_KEY, +) +from ray.rllib.core.learner.torch.torch_learner import TorchLearner +from ray.rllib.utils.annotations import override +from ray.rllib.utils.framework import try_import_torch +from ray.rllib.utils.typing import ModuleID, ParamDict, TensorType + +torch, nn = try_import_torch() + + +class IQLTorchLearner(TorchLearner, IQLLearner): + """Implements the IQL loss on top of `IQLLearner`. + + This Learner implements configure_optimizers_for_module to define + separate optimizers for the policy, Q-, and value networks. When + using a twin-Q network architecture, each Q-network is assigned its + own optimizer—consistent with the SAC algorithm. + + The IQL loss is defined in compute_loss_for_module and consists of + three components: value loss, Q-loss (TD error), and actor (policy) + loss. + + Note that the original IQL implementation performs separate backward + passes for each network. However, due to RLlib's reliance on TorchDDP, + all backward passes must be executed within a single update step. This + constraint can lead to parameter lag and cyclical loss behavior, though + it does not hinder convergence. + """ + + @override(TorchLearner) + def configure_optimizers_for_module( + self, module_id: ModuleID, config: AlgorithmConfig = None + ) -> None: + + # Note, we could have derived directly from SACTorchLearner to + # inherit the setup of optimizers, but that learner comes with + # additional parameters which we do not need. + # Receive the module. + module = self._module[module_id] + + # Define the optimizer for the critic. + # TODO (sven): Maybe we change here naming to `qf` for unification. + params_critic = self.get_parameters(module.qf_encoder) + self.get_parameters( + module.qf + ) + optim_critic = torch.optim.Adam(params_critic, eps=1e-7) + self.register_optimizer( + module_id=module_id, + optimizer_name="qf", + optimizer=optim_critic, + params=params_critic, + lr_or_lr_schedule=config.critic_lr, + ) + # If necessary register also an optimizer for a twin Q network. + if config.twin_q: + params_twin_critic = self.get_parameters( + module.qf_twin_encoder + ) + self.get_parameters(module.qf_twin) + optim_twin_critic = torch.optim.Adam(params_twin_critic, eps=1e-7) + self.register_optimizer( + module_id=module_id, + optimizer_name="qf_twin", + optimizer=optim_twin_critic, + params=params_twin_critic, + lr_or_lr_schedule=config.critic_lr, + ) + + # Define the optimizer for the actor. + params_actor = self.get_parameters(module.pi_encoder) + self.get_parameters( + module.pi + ) + optim_actor = torch.optim.Adam(params_actor, eps=1e-7) + self.register_optimizer( + module_id=module_id, + optimizer_name="policy", + optimizer=optim_actor, + params=params_actor, + lr_or_lr_schedule=config.actor_lr, + ) + + # Define the optimizer for the value function. + params_value = self.get_parameters(module.vf_encoder) + self.get_parameters( + module.vf + ) + optim_value = torch.optim.Adam(params_value, eps=1e-7) + self.register_optimizer( + module_id=module_id, + optimizer_name="value", + optimizer=optim_value, + params=params_value, + lr_or_lr_schedule=config.value_lr, + ) + + @override(TorchLearner) + def compute_loss_for_module( + self, + *, + module_id: ModuleID, + config: AlgorithmConfig, + batch: Dict, + fwd_out: Dict + ): + + # Get the module and hyperparameters. + module = self._module[module_id] + expectile = self.expectile[module_id] + temperature = self.temperature[module_id] + + # Get the action distribution for the actor loss. + action_train_dist_class = module.get_train_action_dist_cls() + action_train_dist = action_train_dist_class.from_logits( + fwd_out[Columns.ACTION_DIST_INPUTS] + ) + + # First, compute the value loss via the target Q-network and current observations. + value_loss = torch.mean( + self._expectile_loss( + fwd_out[QF_TARGET_PREDS] - fwd_out[Columns.VF_PREDS], expectile + ) + ) + + # Second, compute the actor loss using the target-Q network and values. + exp_advantages = torch.minimum( + torch.exp( + temperature * (fwd_out[QF_TARGET_PREDS] - fwd_out[Columns.VF_PREDS]) + ), + torch.Tensor([100.0]).to(self.device), + ) + # Note, we are using here the actions from the data sample. + action_logps = action_train_dist.logp(batch[Columns.ACTIONS]) + # Compute the actor loss. + actor_loss = -torch.mean(exp_advantages.detach() * action_logps) + + # Third, compute the critic loss. + target_critic = ( + batch[Columns.REWARDS] + + config.gamma + * (1 - batch[Columns.TERMINATEDS].float()) + * fwd_out[VF_PREDS_NEXT].detach() + ) + + critic_loss = torch.mean( + torch.nn.MSELoss(reduction="none")(target_critic, fwd_out[QF_PREDS]) + ) + + # If we have a twin-Q architecture, calculate the its loss, too. + if config.twin_q: + critic_twin_loss = ( + torch.mean( + torch.nn.MSELoss(reduction="none")( + target_critic, fwd_out[QF_TWIN_PREDS] + ) + ) + * 0.5 + ) + critic_loss *= 0.5 + + # Compute the total loss. + total_loss = value_loss + actor_loss + critic_loss + + # If we have a twin-Q architecture, add its loss. + if config.twin_q: + total_loss += critic_twin_loss + + # Log metrics. + self.metrics.log_dict( + { + POLICY_LOSS_KEY: actor_loss, + QF_LOSS_KEY: critic_loss, + }, + key=module_id, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + + # Log the losses also in the temporary containers for gradient computation. + self._temp_losses[(module_id, POLICY_LOSS_KEY)] = actor_loss + self._temp_losses[(module_id, QF_LOSS_KEY)] = critic_loss + self._temp_losses[(module_id, VF_LOSS)] = value_loss + + # If a twin-Q architecture is used add metrics and loss. + if config.twin_q: + self.metrics.log_value( + key=(module_id, QF_TWIN_LOSS_KEY), + value=critic_twin_loss, + window=1, # <- single items (should not be mean/ema-reduced over time). + ) + self._temp_losses[(module_id, QF_TWIN_LOSS_KEY)] = critic_twin_loss + + return total_loss + + @override(TorchLearner) + def compute_gradients( + self, loss_per_module: Dict[ModuleID, TensorType], **kwargs + ) -> ParamDict: + grads = {} + for module_id in set(loss_per_module.keys()) - {ALL_MODULES}: + # Loop through optimizers registered for this module. + for optim_name, optim in self.get_optimizers_for_module(module_id): + # Zero the gradients. Note, we need to reset the gradients b/c + # each component for a module operates on the same graph. + optim.zero_grad(set_to_none=True) + + # Compute the gradients for the component and module. + loss_tensor = self._temp_losses.pop((module_id, optim_name + "_loss")) + loss_tensor.backward(retain_graph=True) + # Store the gradients for the component and module. + grads.update( + { + pid: p.grad + for pid, p in self.filter_param_dict_for_optimizer( + self._params, optim + ).items() + } + ) + + # Make sure we updated on all loss terms. + assert not self._temp_losses + return grads + + def _expectile_loss(self, diff: TensorType, expectile: TensorType) -> TensorType: + """Computes the expectile loss. + + Args: + diff: A tensor containing a difference loss. + expectile: The expectile to use for the expectile loss. + + Returns: + The expectile loss of `diff` using `expectile`. + """ + weight = torch.where(diff > 0, expectile, 1 - expectile) + return weight * torch.pow(diff, 2) diff --git a/lib/python3.12/site-packages/ray/util/state/__init__.py b/lib/python3.12/site-packages/ray/util/state/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b8bb885e5408ed8996ebd471990127061e7cfc00 --- /dev/null +++ b/lib/python3.12/site-packages/ray/util/state/__init__.py @@ -0,0 +1,49 @@ +from ray.util.state.api import ( + StateApiClient, + get_actor, + get_job, + get_log, + get_node, + get_objects, + get_placement_group, + get_task, + get_worker, + list_actors, + list_cluster_events, + list_jobs, + list_logs, + list_nodes, + list_objects, + list_placement_groups, + list_runtime_envs, + list_tasks, + list_workers, + summarize_actors, + summarize_objects, + summarize_tasks, +) + +__all__ = [ + "get_actor", + "get_log", + "get_node", + "get_objects", + "get_placement_group", + "get_task", + "get_worker", + "get_job", + "list_actors", + "list_jobs", + "list_nodes", + "list_placement_groups", + "list_tasks", + "list_workers", + "list_objects", + "list_runtime_envs", + "list_logs", + "list_cluster_events", + "summarize_actors", + "summarize_objects", + "summarize_tasks", + "StateApiClient", +] diff --git a/lib/python3.12/site-packages/ray/util/state/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/state/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21bff7ba610ce47fa1c0dea4f4b0f110bd468c33 Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/state/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/state/__pycache__/api.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/state/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..faecbb50035eb313e7b9b525e5aea6b92de9c202 Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/state/__pycache__/api.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/state/__pycache__/common.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/state/__pycache__/common.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af8c20b5956a3de1c7153b0284390993221f429a Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/state/__pycache__/common.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/state/__pycache__/exception.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/state/__pycache__/exception.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5b1f00cd160d9663a8623bad34e5907c582804e Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/state/__pycache__/exception.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/state/__pycache__/state_cli.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/state/__pycache__/state_cli.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19a7a6257a858a1959258795b70125cadd909735 Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/state/__pycache__/state_cli.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/state/__pycache__/state_manager.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/state/__pycache__/state_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b58da99415d2da045332c28eaa993208f78edf94 Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/state/__pycache__/state_manager.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/state/__pycache__/util.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/state/__pycache__/util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c0597bdd483d1bc5eef7b1213f89a711bac8ce8 Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/state/__pycache__/util.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/state/api.py b/lib/python3.12/site-packages/ray/util/state/api.py new file mode 100644 index 0000000000000000000000000000000000000000..e3fa7acb671cff91ef5d28ffaec017298f6023c6 --- /dev/null +++ b/lib/python3.12/site-packages/ray/util/state/api.py @@ -0,0 +1,1465 @@ +import logging +import threading +import urllib +import warnings +from contextlib import contextmanager +from dataclasses import fields +from typing import Any, Dict, Generator, List, Optional, Tuple, Union + +import requests + +import ray +from ray.dashboard.modules.dashboard_sdk import SubmissionClient +from ray.dashboard.utils import ( + get_address_for_submission_client, + ray_address_to_api_server_url, +) +from ray.util.annotations import DeveloperAPI +from ray.util.state.common import ( + DEFAULT_LIMIT, + DEFAULT_RPC_TIMEOUT, + ActorState, + ClusterEventState, + GetApiOptions, + GetLogOptions, + JobState, + ListApiOptions, + NodeState, + ObjectState, + PlacementGroupState, + PredicateType, + RuntimeEnvState, + StateResource, + SummaryApiOptions, + SummaryResource, + SupportedFilterType, + TaskState, + WorkerState, + dict_to_state, +) +from ray.util.state.exception import RayStateApiException, ServerUnavailable + +logger = logging.getLogger(__name__) +_MAX_HTTP_RESPONSE_EXCEPTION_TEXT = 500 + + +@contextmanager +def warnings_on_slow_request( + *, address: str, endpoint: str, timeout: float, explain: bool +): + """A context manager to print warnings if the request is replied slowly. + + Warnings are printed 3 times + + Args: + address: The address of the endpoint. + endpoint: The name of the endpoint. + timeout: Request timeout in seconds. + explain: Whether ot not it will print the warning. + """ + # Do nothing if explain is not specified. + if not explain: + yield + return + + # Prepare timers to print warning. + # Print 3 times with exponential backoff. timeout / 2, timeout / 4, timeout / 8 + def print_warning(elapsed: float): + logger.info( + f"({round(elapsed, 2)} / {timeout} seconds) " + "Waiting for the response from the API server " + f"address {address}{endpoint}.", + ) + + warning_timers = [ + threading.Timer(timeout / i, print_warning, args=[timeout / i]) + for i in [2, 4, 8] + ] + + try: + for timer in warning_timers: + timer.start() + yield + finally: + # Make sure all timers are cancelled once request is terminated. + for timer in warning_timers: + timer.cancel() + + +""" +This file contains API client and methods for querying ray state. + +Usage: + 1. [Recommended] With StateApiClient: + ``` + client = StateApiClient(address="auto") + data = client.list(StateResource.NODES) + ... + ``` + + 2. With SDK APIs: + The API creates a `StateApiClient` for each invocation. So if multiple + invocations of listing are used, it is better to reuse the `StateApiClient` + as suggested above. + ``` + data = list_nodes(address="auto") + ``` +""" + + +@DeveloperAPI +class StateApiClient(SubmissionClient): + """State API Client issues REST GET requests to the server for resource states.""" + + def __init__( + self, + address: Optional[str] = None, + cookies: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, Any]] = None, + ): + """Initialize a StateApiClient and check the connection to the cluster. + + Args: + address: Ray bootstrap address (e.g. `127.0.0.0:6379`, `auto`), or Ray + Client address (e.g. `ray://:10001`), or Ray dashboard + address (e.g. `http://:8265`). + If not provided, it will be detected automatically from any running + local Ray cluster. + cookies: Cookies to use when sending requests to the HTTP job server. + headers: Headers to use when sending requests to the HTTP job server, used + for cases like authentication to a remote cluster. + """ + if requests is None: + raise RuntimeError( + "The Ray state CLI & SDK require the ray[default] " + "installation: `pip install 'ray[default']``" + ) + if not headers: + headers = {"Content-Type": "application/json"} + + # Resolve API server URL + api_server_url = get_address_for_submission_client(address) + + super().__init__( + address=api_server_url, + create_cluster_if_needed=False, + headers=headers, + cookies=cookies, + ) + + @classmethod + def _make_param(cls, options: Union[ListApiOptions, GetApiOptions]) -> Dict: + options_dict = {} + for field in fields(options): + # TODO(rickyyx): We will need to find a way to pass server side timeout + # TODO(rickyyx): We will have to convert filter option + # slightly differently for now. But could we do k,v pair rather than this? + # I see we are also converting dict to XXXApiOptions later on, we could + # probably organize the marshaling a bit better. + if field.name == "filters": + options_dict["filter_keys"] = [] + options_dict["filter_predicates"] = [] + options_dict["filter_values"] = [] + for filter in options.filters: + if len(filter) != 3: + raise ValueError( + f"The given filter has incorrect input type, {filter}. " + "Provide (key, predicate, value) tuples." + ) + filter_k, filter_predicate, filter_val = filter + options_dict["filter_keys"].append(filter_k) + options_dict["filter_predicates"].append(filter_predicate) + options_dict["filter_values"].append(filter_val) + continue + + option_val = getattr(options, field.name) + if option_val is not None: + options_dict[field.name] = option_val + + return options_dict + + def _make_http_get_request( + self, + endpoint: str, + params: Dict, + timeout: float, + _explain: bool = False, + ) -> Dict: + with warnings_on_slow_request( + address=self._address, endpoint=endpoint, timeout=timeout, explain=_explain + ): + # Send a request. + response = None + try: + response = self._do_request( + "GET", + endpoint, + timeout=timeout, + params=params, + ) + # If we have a valid JSON error, don't raise a generic exception but + # instead let the caller parse it to raise a more precise exception. + if ( + response.status_code == 500 + and "application/json" + not in response.headers.get("Content-Type", "") + ): + response.raise_for_status() + except requests.exceptions.RequestException as e: + err_str = f"Failed to make request to {self._address}{endpoint}. " + + # Best-effort to give hints to users on potential reasons of connection + # failure. + err_str += ( + "Failed to connect to API server. Please check the API server " + "log for details. Make sure dependencies are installed with " + "`pip install ray[default]`. Please also check dashboard is " + "available, and included when starting ray cluster, " + "i.e. `ray start --include-dashboard=True --head`. " + ) + if response is None: + raise ServerUnavailable(err_str) + + err_str += f"Response(url={response.url},status={response.status_code})" + raise RayStateApiException(err_str) from e + + try: + # Process the response. + response = response.json() + except requests.exceptions.JSONDecodeError as e: + raise RayStateApiException( + f"Failed to parse Response(url={response.url}, " + f"status={response.status_code}, text='{response.text[:_MAX_HTTP_RESPONSE_EXCEPTION_TEXT]}')" + ) from e + if response["result"] is False: + raise RayStateApiException( + "API server internal error. See dashboard.log file for more details. " + f"Error: {response['msg']}" + ) + + # Dictionary of `ListApiResponse` or `SummaryApiResponse` + return response["data"]["result"] + + def get( + self, + resource: StateResource, + id: str, + options: Optional[GetApiOptions], + _explain: bool = False, + ) -> Optional[ + Union[ + ActorState, + PlacementGroupState, + NodeState, + WorkerState, + TaskState, + List[ObjectState], + JobState, + ] + ]: + """Get resources states by id + + Args: + resource_name: Resource names, i.e. 'workers', 'actors', 'nodes', + 'placement_groups', 'tasks', 'objects'. + 'jobs' and 'runtime-envs' are not supported yet. + id: ID for the resource, i.e. 'node_id' for nodes. + options: Get options. See `GetApiOptions` for details. + _explain: Print the API information such as API + latency or failed query information. + + Returns: + None if not found, and if found: + - ActorState for actors + - PlacementGroupState for placement groups + - NodeState for nodes + - WorkerState for workers + - TaskState for tasks + - JobState for jobs + + Empty list for objects if not found, or list of ObjectState for objects + + Raises: + Exception: This doesn't catch any exceptions raised when the underlying request + call raises exceptions. For example, it could raise `requests.Timeout` + when timeout occurs. + ValueError: + if the resource could not be GET by id, i.e. jobs and runtime-envs. + + """ + # TODO(rickyyx): Make GET not using filters on list operation + params = self._make_param(options) + + RESOURCE_ID_KEY_NAME = { + StateResource.NODES: "node_id", + StateResource.ACTORS: "actor_id", + StateResource.PLACEMENT_GROUPS: "placement_group_id", + StateResource.WORKERS: "worker_id", + StateResource.TASKS: "task_id", + StateResource.OBJECTS: "object_id", + StateResource.JOBS: "submission_id", + } + if resource not in RESOURCE_ID_KEY_NAME: + raise ValueError(f"Can't get {resource.name} by id.") + + params["filter_keys"] = [RESOURCE_ID_KEY_NAME[resource]] + params["filter_predicates"] = ["="] + params["filter_values"] = [id] + params["detail"] = True + endpoint = f"/api/v0/{resource.value}" + + list_api_response = self._make_http_get_request( + endpoint=endpoint, + params=params, + timeout=options.timeout, + _explain=_explain, + ) + result = list_api_response["result"] + + # Empty result + if len(result) == 0: + return None + + result = [dict_to_state(d, resource) for d in result] + if resource == StateResource.OBJECTS: + # NOTE(rickyyx): + # There might be multiple object entries for a single object id + # because a single object could be referenced at different places + # e.g. pinned as local variable, used as parameter + return result + + if resource == StateResource.TASKS: + # There might be multiple task attempts given a task id due to + # task retries. + if len(result) == 1: + return result[0] + return result + + # For the rest of the resources, there should only be a single entry + # for a particular id. + assert len(result) == 1 + return result[0] + + def _print_api_warning( + self, + resource: StateResource, + api_response: dict, + warn_data_source_not_available: bool = True, + warn_data_truncation: bool = True, + warn_limit: bool = True, + warn_server_side_warnings: bool = True, + ): + """Print the API warnings. + + Args: + resource: Resource names, i.e. 'jobs', 'actors', 'nodes', + see `StateResource` for details. + api_response: The dictionarified `ListApiResponse` or `SummaryApiResponse`. + warn_data_source_not_available: Warn when some data sources + are not available. + warn_data_truncation: Warn when results were truncated at + the data source. + warn_limit: Warn when results were limited. + warn_server_side_warnings: Warn when the server side generates warnings + (E.g., when callsites not enabled for listing objects) + """ + # Print warnings if anything was given. + if warn_data_source_not_available: + warning_msgs = api_response.get("partial_failure_warning", None) + if warning_msgs: + warnings.warn(warning_msgs) + + if warn_data_truncation: + # Print warnings if data is truncated at the data source. + num_after_truncation = api_response["num_after_truncation"] + total = api_response["total"] + if total > num_after_truncation: + # NOTE(rickyyx): For now, there's not much users + # could do (neither can we), with hard truncation. + # Unless we allow users to set a higher + # `RAY_MAX_LIMIT_FROM_DATA_SOURCE`, the data will + # always be truncated at the data source. + warnings.warn( + ( + "The returned data may contain incomplete result. " + f"{num_after_truncation} ({total} total from the cluster) " + f"{resource.value} are retrieved from the data source. " + f"{total - num_after_truncation} entries have been truncated. " + f"Max of {num_after_truncation} entries are retrieved " + "from data source to prevent over-sized payloads." + ), + ) + + if warn_limit: + # Print warnings if return data is limited at the API server due to + # limit enforced at the server side + num_filtered = api_response["num_filtered"] + data = api_response["result"] + if num_filtered > len(data): + warnings.warn( + ( + f"Limit last {len(data)} entries " + f"(Total {num_filtered}). Use `--filter` to reduce " + "the amount of data to return or " + "setting a higher limit with `--limit` to see all data. " + ), + ) + + if warn_server_side_warnings: + # Print the additional warnings. + warnings_to_print = api_response.get("warnings", []) + if warnings_to_print: + for warning_to_print in warnings_to_print: + warnings.warn(warning_to_print) + + def _raise_on_missing_output(self, resource: StateResource, api_response: dict): + """Raise an exception when the API resopnse contains a missing output. + + Output can be missing if (1) Failures on some of data source queries (e.g., + `ray list tasks` queries all raylets, and if some of queries fail, it will + contain missing output. If all queries fail, it will just fail). (2) Data + is truncated because the output is too large. + + Args: + resource: Resource names, i.e. 'jobs', 'actors', 'nodes', + see `StateResource` for details. + api_response: The dictionarified `ListApiResponse` or `SummaryApiResponse`. + """ + # Raise an exception if there are partial failures that cause missing output. + warning_msgs = api_response.get("partial_failure_warning", None) + if warning_msgs: + raise RayStateApiException( + f"Failed to retrieve all {resource.value} from the cluster because" + "they are not reachable due to query failures to the data sources. " + "To avoid raising an exception and allow having missing output, " + "set `raise_on_missing_output=False`. " + ) + # Raise an exception is there is data truncation that cause missing output. + total = api_response["total"] + num_after_truncation = api_response["num_after_truncation"] + + if total != num_after_truncation: + raise RayStateApiException( + f"Failed to retrieve all {total} {resource.value} from the cluster " + "because they are not reachable due to data truncation. It happens " + "when the returned data is too large " + # When the data is truncated, the truncation + # threshold == num_after_truncation. We cannot set this to env + # var because the CLI side might not have the correct env var. + f"(> {num_after_truncation}) " + "To avoid raising an exception and allow having missing output, " + "set `raise_on_missing_output=False`. " + ) + + def list( + self, + resource: StateResource, + options: ListApiOptions, + raise_on_missing_output: bool, + _explain: bool = False, + ) -> List[ + Union[ + ActorState, + JobState, + NodeState, + TaskState, + ObjectState, + PlacementGroupState, + RuntimeEnvState, + WorkerState, + ClusterEventState, + ] + ]: + """List resources states + + Args: + resource: Resource names, i.e. 'jobs', 'actors', 'nodes', + see `StateResource` for details. + options: List options. See `ListApiOptions` for details. + raise_on_missing_output: When True, raise an exception if the output + is incomplete. Output can be incomplete if + (1) there's a partial network failure when the source is distributed. + (2) data is truncated because it is too large. + Set it to False to avoid throwing an exception on missing data. + _explain: Print the API information such as API + latency or failed query information. + + Returns: + A list of queried result from `ListApiResponse`, + + Raises: + Exception: This doesn't catch any exceptions raised when the + underlying request call raises exceptions. For example, it could + raise `requests.Timeout` when timeout occurs. + + """ + if options.has_conflicting_filters(): + # return early with empty list when there are conflicting filters + return [] + + endpoint = f"/api/v0/{resource.value}" + params = self._make_param(options) + list_api_response = self._make_http_get_request( + endpoint=endpoint, + params=params, + timeout=options.timeout, + _explain=_explain, + ) + if raise_on_missing_output: + self._raise_on_missing_output(resource, list_api_response) + if _explain: + self._print_api_warning(resource, list_api_response) + return [dict_to_state(d, resource) for d in list_api_response["result"]] + + def summary( + self, + resource: SummaryResource, + *, + options: SummaryApiOptions, + raise_on_missing_output: bool, + _explain: bool = False, + ) -> Dict: + """Summarize resources states + + Args: + resource_name: Resource names, + see `SummaryResource` for details. + options: summary options. See `SummaryApiOptions` for details. + raise_on_missing_output: Raise an exception if the output has missing data. + Output can have missing data if (1) there's a partial network failure + when the source is distributed. (2) data is truncated + because it is too large. + _explain: Print the API information such as API + latency or failed query information. + + Returns: + A dictionary of queried result from `SummaryApiResponse`. + + Raises: + Exception: This doesn't catch any exceptions raised when the + underlying request call raises exceptions. For example, it could + raise `requests.Timeout` when timeout occurs. + """ + params = {"timeout": options.timeout} + endpoint = f"/api/v0/{resource.value}/summarize" + summary_api_response = self._make_http_get_request( + endpoint=endpoint, + params=params, + timeout=options.timeout, + _explain=_explain, + ) + if raise_on_missing_output: + self._raise_on_missing_output(resource, summary_api_response) + if _explain: + # There's no limit applied to summary, so we shouldn't warn. + self._print_api_warning(resource, summary_api_response, warn_limit=False) + return summary_api_response["result"]["node_id_to_summary"] + + +@DeveloperAPI +def get_actor( + id: str, + address: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, + _explain: bool = False, +) -> Optional[ActorState]: + """Get an actor by id. + + Args: + id: Id of the actor + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + timeout: Max timeout value for the state API requests made. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + None if actor not found, or + :class:`ActorState `. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).get( + StateResource.ACTORS, id, GetApiOptions(timeout=timeout), _explain=_explain + ) + + +@DeveloperAPI +def get_job( + id: str, + address: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, + _explain: bool = False, +) -> Optional[JobState]: + """Get a submission job detail by id. + + Args: + id: Submission ID obtained from job API. + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + timeout: Max timeout value for the state API requests made. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + None if job not found, or + :class:`JobState `. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).get( + StateResource.JOBS, + id, + GetApiOptions(timeout=timeout), + _explain=_explain, + ) + + +@DeveloperAPI +def get_placement_group( + id: str, + address: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, + _explain: bool = False, +) -> Optional[PlacementGroupState]: + """Get a placement group by id. + + Args: + id: Id of the placement group + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + timeout: Max timeout value for the state APIs requests made. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + None if actor not found, or + :class:`~ray.util.state.common.PlacementGroupState`. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).get( + StateResource.PLACEMENT_GROUPS, + id, + GetApiOptions(timeout=timeout), + _explain=_explain, + ) + + +@DeveloperAPI +def get_node( + id: str, + address: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, + _explain: bool = False, +) -> Optional[NodeState]: + """Get a node by id. + + Args: + id: Id of the node. + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + timeout: Max timeout value for the state APIs requests made. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + None if actor not found, or + :class:`NodeState `. + + Raises: + RayStateApiException: if the CLI is failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).get( + StateResource.NODES, + id, + GetApiOptions(timeout=timeout), + _explain=_explain, + ) + + +@DeveloperAPI +def get_worker( + id: str, + address: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, + _explain: bool = False, +) -> Optional[WorkerState]: + """Get a worker by id. + + Args: + id: Id of the worker + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + timeout: Max timeout value for the state APIs requests made. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + None if actor not found, or + :class:`WorkerState `. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).get( + StateResource.WORKERS, + id, + GetApiOptions(timeout=timeout), + _explain=_explain, + ) + + +@DeveloperAPI +def get_task( + id: Union[str, "ray.ObjectRef"], + address: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, + _explain: bool = False, +) -> Optional[TaskState]: + """Get task attempts of a task by id. + + Args: + id: String id of the task or ObjectRef that corresponds to task + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + timeout: Max timeout value for the state APIs requests made. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + None if task not found, or a list of + :class:`~ray.util.state.common.TaskState` + from the task attempts. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + str_id: str + if isinstance(id, str): + str_id = id + else: + str_id = id.task_id().hex() + return StateApiClient(address=address).get( + StateResource.TASKS, + str_id, + GetApiOptions(timeout=timeout), + _explain=_explain, + ) + + +@DeveloperAPI +def get_objects( + id: str, + address: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, + _explain: bool = False, +) -> List[ObjectState]: + """Get objects by id. + + There could be more than 1 entry returned since an object could be + referenced at different places. + + Args: + id: Id of the object + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + timeout: Max timeout value for the state APIs requests made. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + List of + :class:`~ray.util.state.common.ObjectState`. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).get( + StateResource.OBJECTS, + id, + GetApiOptions(timeout=timeout), + _explain=_explain, + ) + + +@DeveloperAPI +def list_actors( + address: Optional[str] = None, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + limit: int = DEFAULT_LIMIT, + timeout: int = DEFAULT_RPC_TIMEOUT, + detail: bool = False, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> List[ActorState]: + """List actors in the cluster. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + filters: List of tuples of filter key, predicate (=, or !=), and + the filter value. E.g., `("id", "=", "abcd")` + String filter values are case-insensitive. + limit: Max number of entries returned by the state backend. + timeout: Max timeout value for the state APIs requests made. + detail: When True, more details info (specified in `ActorState`) + will be queried and returned. See + :class:`ActorState `. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + List of + :class:`ActorState `. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).list( + StateResource.ACTORS, + options=ListApiOptions( + limit=limit, + timeout=timeout, + filters=filters, + detail=detail, + ), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +@DeveloperAPI +def list_placement_groups( + address: Optional[str] = None, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + limit: int = DEFAULT_LIMIT, + timeout: int = DEFAULT_RPC_TIMEOUT, + detail: bool = False, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> List[PlacementGroupState]: + """List placement groups in the cluster. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + filters: List of tuples of filter key, predicate (=, or !=), and + the filter value. E.g., `("state", "=", "abcd")` + String filter values are case-insensitive. + limit: Max number of entries returned by the state backend. + timeout: Max timeout value for the state APIs requests made. + detail: When True, more details info (specified in `PlacementGroupState`) + will be queried and returned. See + :class:`~ray.util.state.common.PlacementGroupState`. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + List of :class:`~ray.util.state.common.PlacementGroupState`. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).list( + StateResource.PLACEMENT_GROUPS, + options=ListApiOptions( + limit=limit, timeout=timeout, filters=filters, detail=detail + ), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +@DeveloperAPI +def list_nodes( + address: Optional[str] = None, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + limit: int = DEFAULT_LIMIT, + timeout: int = DEFAULT_RPC_TIMEOUT, + detail: bool = False, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> List[NodeState]: + """List nodes in the cluster. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + filters: List of tuples of filter key, predicate (=, or !=), and + the filter value. E.g., `("node_name", "=", "abcd")` + String filter values are case-insensitive. + limit: Max number of entries returned by the state backend. + timeout: Max timeout value for the state APIs requests made. + detail: When True, more details info (specified in `NodeState`) + will be queried and returned. See + :class:`NodeState `. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + List of dictionarified + :class:`NodeState `. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).list( + StateResource.NODES, + options=ListApiOptions( + limit=limit, timeout=timeout, filters=filters, detail=detail + ), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +@DeveloperAPI +def list_jobs( + address: Optional[str] = None, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + limit: int = DEFAULT_LIMIT, + timeout: int = DEFAULT_RPC_TIMEOUT, + detail: bool = False, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> List[JobState]: + """List jobs submitted to the cluster by :ref:`ray job submission `. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + filters: List of tuples of filter key, predicate (=, or !=), and + the filter value. E.g., `("status", "=", "abcd")` + String filter values are case-insensitive. + limit: Max number of entries returned by the state backend. + timeout: Max timeout value for the state APIs requests made. + detail: When True, more details info (specified in `JobState`) + will be queried and returned. See + :class:`JobState `. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + List of dictionarified + :class:`JobState `. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).list( + StateResource.JOBS, + options=ListApiOptions( + limit=limit, timeout=timeout, filters=filters, detail=detail + ), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +@DeveloperAPI +def list_workers( + address: Optional[str] = None, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + limit: int = DEFAULT_LIMIT, + timeout: int = DEFAULT_RPC_TIMEOUT, + detail: bool = False, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> List[WorkerState]: + """List workers in the cluster. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + filters: List of tuples of filter key, predicate (=, or !=), and + the filter value. E.g., `("is_alive", "=", "True")` + String filter values are case-insensitive. + limit: Max number of entries returned by the state backend. + timeout: Max timeout value for the state APIs requests made. + detail: When True, more details info (specified in `WorkerState`) + will be queried and returned. See + :class:`WorkerState `. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + List of + :class:`WorkerState `. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).list( + StateResource.WORKERS, + options=ListApiOptions( + limit=limit, timeout=timeout, filters=filters, detail=detail + ), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +@DeveloperAPI +def list_tasks( + address: Optional[str] = None, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + limit: int = DEFAULT_LIMIT, + timeout: int = DEFAULT_RPC_TIMEOUT, + detail: bool = False, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> List[TaskState]: + """List tasks in the cluster. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + filters: List of tuples of filter key, predicate (=, or !=), and + the filter value. E.g., `("is_alive", "=", "True")` + String filter values are case-insensitive. + limit: Max number of entries returned by the state backend. + timeout: Max timeout value for the state APIs requests made. + detail: When True, more details info (specified in `TaskState`) + will be queried and returned. See + :class:`TaskState `. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + List of + :class:`TaskState `. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).list( + StateResource.TASKS, + options=ListApiOptions( + limit=limit, timeout=timeout, filters=filters, detail=detail + ), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +@DeveloperAPI +def list_objects( + address: Optional[str] = None, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + limit: int = DEFAULT_LIMIT, + timeout: int = DEFAULT_RPC_TIMEOUT, + detail: bool = False, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> List[ObjectState]: + """List objects in the cluster. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + filters: List of tuples of filter key, predicate (=, or !=), and + the filter value. E.g., `("ip", "=", "0.0.0.0")` + String filter values are case-insensitive. + limit: Max number of entries returned by the state backend. + timeout: Max timeout value for the state APIs requests made. + detail: When True, more details info (specified in `ObjectState`) + will be queried and returned. See + :class:`ObjectState `. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + List of + :class:`ObjectState `. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).list( + StateResource.OBJECTS, + options=ListApiOptions( + limit=limit, timeout=timeout, filters=filters, detail=detail + ), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +@DeveloperAPI +def list_runtime_envs( + address: Optional[str] = None, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + limit: int = DEFAULT_LIMIT, + timeout: int = DEFAULT_RPC_TIMEOUT, + detail: bool = False, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> List[RuntimeEnvState]: + """List runtime environments in the cluster. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + filters: List of tuples of filter key, predicate (=, or !=), and + the filter value. E.g., `("node_id", "=", "abcdef")` + String filter values are case-insensitive. + limit: Max number of entries returned by the state backend. + timeout: Max timeout value for the state APIs requests made. + detail: When True, more details info (specified in `RuntimeEnvState`) + will be queried and returned. See + :class:`RuntimeEnvState `. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Returns: + List of + :class:`RuntimeEnvState `. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).list( + StateResource.RUNTIME_ENVS, + options=ListApiOptions( + limit=limit, timeout=timeout, filters=filters, detail=detail + ), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +@DeveloperAPI +def list_cluster_events( + address: Optional[str] = None, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + limit: int = DEFAULT_LIMIT, + timeout: int = DEFAULT_RPC_TIMEOUT, + detail: bool = False, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> List[Dict]: + return StateApiClient(address=address).list( + StateResource.CLUSTER_EVENTS, + options=ListApiOptions( + limit=limit, timeout=timeout, filters=filters, detail=detail + ), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +""" +Log APIs +""" + + +@DeveloperAPI +def get_log( + address: Optional[str] = None, + node_id: Optional[str] = None, + node_ip: Optional[str] = None, + filename: Optional[str] = None, + actor_id: Optional[str] = None, + task_id: Optional[str] = None, + pid: Optional[int] = None, + follow: bool = False, + tail: int = -1, + timeout: int = DEFAULT_RPC_TIMEOUT, + suffix: str = "out", + encoding: Optional[str] = "utf-8", + errors: Optional[str] = "strict", + submission_id: Optional[str] = None, + attempt_number: int = 0, + _interval: Optional[float] = None, + filter_ansi_code: bool = False, +) -> Generator[str, None, None]: + """Retrieve log file based on file name or some entities ids (pid, actor id, task id). + + Examples: + .. testcode:: + :hide: + + import ray + import time + + ray.shutdown() + ray.init() + + # Wait for the node to be registered to the dashboard + time.sleep(5) + + .. testcode:: + + import ray + from ray.util.state import get_log + + # Node id could be retrieved from list_nodes() or ray.nodes() + node_id = ray.nodes()[0]["NodeID"] + filename = "raylet.out" + for l in get_log(filename=filename, node_id=node_id): + print(l) + + .. testoutput:: + :options: +MOCK + + [2023-05-19 12:35:18,347 I 4259 68399276] (raylet) io_service_pool.cc:35: IOServicePool is running with 1 io_service. + [2023-05-19 12:35:18,348 I 4259 68399276] (raylet) store_runner.cc:32: Allowing the Plasma store to use up to 2.14748GB of memory. + [2023-05-19 12:35:18,348 I 4259 68399276] (raylet) store_runner.cc:48: Starting object store with directory /tmp, fallback /tmp/ray, and huge page support disabled + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If not specified, it will be retrieved from the initialized ray cluster. + node_id: Id of the node containing the logs . + node_ip: Ip of the node containing the logs. (At least one of the node_id and + node_ip have to be supplied when identifying a node). + filename: Name of the file (relative to the ray log directory) to be retrieved. + actor_id: Id of the actor if getting logs from an actor. + task_id: Id of the task if getting logs from a non concurrent actor. + For concurrent actor, please query the log with actor_id. + pid: PID of the worker if getting logs generated by a worker. When querying + with pid, either node_id or node_ip must be supplied. + follow: When set to True, logs will be streamed and followed. + tail: Number of lines to get from the end of the log file. Set to -1 for getting + the entire log. + timeout: Max timeout for requests made when getting the logs. + suffix: The suffix of the log file if query by id of tasks/workers/actors. Default to "out". + encoding: The encoding used to decode the content of the log file. Default is + "utf-8". Use None to get binary data directly. + errors: The error handling scheme to use for decoding errors. Default is + "strict". See https://docs.python.org/3/library/codecs.html#error-handlers + submission_id: Job submission ID if getting log from a submission job. + attempt_number: The attempt number of the task if getting logs generated by a task. + _interval: The interval in secs to print new logs when `follow=True`. + filter_ansi_code: A boolean flag for determining whether to filter ANSI escape codes. + Setting to `True` removes ANSI escape codes from the output. The default value is `False`. + + Return: + A Generator of log line, None for SendType and ReturnType. + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + + api_server_url = ray_address_to_api_server_url(address) + media_type = "stream" if follow else "file" + + options = GetLogOptions( + node_id=node_id, + node_ip=node_ip, + filename=filename, + actor_id=actor_id, + task_id=task_id, + pid=pid, + lines=tail, + interval=_interval, + media_type=media_type, + timeout=timeout, + suffix=suffix, + submission_id=submission_id, + attempt_number=attempt_number, + ) + options_dict = {} + for field in fields(options): + option_val = getattr(options, field.name) + if option_val is not None: + options_dict[field.name] = option_val + + if filter_ansi_code is not None: + options_dict["filter_ansi_code"] = filter_ansi_code + + with requests.get( + f"{api_server_url}/api/v0/logs/{media_type}?" + f"{urllib.parse.urlencode(options_dict)}", + stream=True, + ) as r: + if r.status_code != 200: + raise RayStateApiException(r.text) + for chunk in r.iter_content(chunk_size=None): + if encoding is not None: + chunk = chunk.decode(encoding=encoding, errors=errors) + yield chunk + + +@DeveloperAPI +def list_logs( + address: Optional[str] = None, + node_id: Optional[str] = None, + node_ip: Optional[str] = None, + glob_filter: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, +) -> Dict[str, List[str]]: + """Listing log files available. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If not specified, it will be retrieved from the initialized ray cluster. + node_id: Id of the node containing the logs. + node_ip: Ip of the node containing the logs. + glob_filter: Name of the file (relative to the ray log directory) to be + retrieved. E.g. `glob_filter="*worker*"` for all worker logs. + actor_id: Id of the actor if getting logs from an actor. + timeout: Max timeout for requests made when getting the logs. + _interval: The interval in secs to print new logs when `follow=True`. + + Return: + A dictionary where the keys are log groups (e.g. gcs, raylet, worker), and + values are list of log filenames. + + Raises: + RayStateApiException: if the CLI failed to query the data, or ConnectionError if + failed to resolve the ray address. + """ # noqa: E501 + assert ( + node_ip is not None or node_id is not None + ), "At least one of node ip and node id is required" + + api_server_url = ray_address_to_api_server_url(address) + + if not glob_filter: + glob_filter = "*" + + options_dict = {} + if node_ip: + options_dict["node_ip"] = node_ip + if node_id: + options_dict["node_id"] = node_id + if glob_filter: + options_dict["glob"] = glob_filter + options_dict["timeout"] = timeout + + r = requests.get( + f"{api_server_url}/api/v0/logs?{urllib.parse.urlencode(options_dict)}" + ) + # TODO(rickyx): we could do better at error handling here. + r.raise_for_status() + + response = r.json() + if response["result"] is False: + raise RayStateApiException( + "API server internal error. See dashboard.log file for more details. " + f"Error: {response['msg']}" + ) + return response["data"]["result"] + + +""" +Summary APIs +""" + + +@DeveloperAPI +def summarize_tasks( + address: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> Dict: + """Summarize the tasks in cluster. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + timeout: Max timeout for requests made when getting the states. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Return: + Dictionarified + :class:`~ray.util.state.common.TaskSummaries` + + Raises: + RayStateApiException: if the CLI is failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).summary( + SummaryResource.TASKS, + options=SummaryApiOptions(timeout=timeout), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +@DeveloperAPI +def summarize_actors( + address: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> Dict: + """Summarize the actors in cluster. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + timeout: Max timeout for requests made when getting the states. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Return: + Dictionarified + :class:`~ray.util.state.common.ActorSummaries` + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).summary( + SummaryResource.ACTORS, + options=SummaryApiOptions(timeout=timeout), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) + + +@DeveloperAPI +def summarize_objects( + address: Optional[str] = None, + timeout: int = DEFAULT_RPC_TIMEOUT, + raise_on_missing_output: bool = True, + _explain: bool = False, +) -> Dict: + """Summarize the objects in cluster. + + Args: + address: Ray bootstrap address, could be `auto`, `localhost:6379`. + If None, it will be resolved automatically from an initialized ray. + timeout: Max timeout for requests made when getting the states. + raise_on_missing_output: When True, exceptions will be raised if + there is missing data due to truncation/data source unavailable. + _explain: Print the API information such as API latency or + failed query information. + + Return: + Dictionarified :class:`~ray.util.state.common.ObjectSummaries` + + Raises: + RayStateApiException: if the CLI failed to query the data. + """ # noqa: E501 + return StateApiClient(address=address).summary( + SummaryResource.OBJECTS, + options=SummaryApiOptions(timeout=timeout), + raise_on_missing_output=raise_on_missing_output, + _explain=_explain, + ) diff --git a/lib/python3.12/site-packages/ray/util/state/common.py b/lib/python3.12/site-packages/ray/util/state/common.py new file mode 100644 index 0000000000000000000000000000000000000000..9e4e7000eec3e198c964c9530a87f88a5831e810 --- /dev/null +++ b/lib/python3.12/site-packages/ray/util/state/common.py @@ -0,0 +1,1743 @@ +import datetime +import json +import logging +import sys +import warnings +from abc import ABC +from dataclasses import asdict, field, fields +from enum import Enum, unique +from typing import Any, Dict, List, Optional, Set, Tuple, Union + +import ray.dashboard.utils as dashboard_utils + +# TODO(aguo): Instead of a version check, modify the below models +# to use pydantic BaseModel instead of dataclass. +# In pydantic 2, dataclass no longer needs the `init=True` kwarg to +# generate an __init__ method. Additionally, it will raise an error if +# it detects `init=True` to be set. +from ray._common.pydantic_compat import IS_PYDANTIC_2 +from ray._private.custom_types import ( + TypeActorStatus, + TypeNodeStatus, + TypePlacementGroupStatus, + TypeReferenceType, + TypeTaskStatus, + TypeTaskType, + TypeWorkerExitType, + TypeWorkerType, +) +from ray._private.ray_constants import env_integer +from ray.core.generated.common_pb2 import TaskStatus, TaskType +from ray.core.generated.gcs_pb2 import TaskEvents +from ray.dashboard.modules.job.pydantic_models import JobDetails +from ray.util.state.exception import RayStateApiException + +try: + from pydantic.dataclasses import dataclass + + +except ImportError: + # pydantic is not available in the dashboard. + # We will use the dataclass from the standard library. + from dataclasses import dataclass + + +logger = logging.getLogger(__name__) + +DEFAULT_RPC_TIMEOUT = 30 +DEFAULT_LIMIT = 100 +DEFAULT_LOG_LIMIT = 1000 +DEFAULT_DOWNLOAD_FILENAME = "file.txt" + +# Max number of entries from API server to the client +RAY_MAX_LIMIT_FROM_API_SERVER = env_integer( + "RAY_MAX_LIMIT_FROM_API_SERVER", 10 * 1000 +) # 10k + +# Max number of entries from data sources (rest will be truncated at the +# data source, e.g. raylet) +RAY_MAX_LIMIT_FROM_DATA_SOURCE = env_integer( + "RAY_MAX_LIMIT_FROM_DATA_SOURCE", 10 * 1000 +) # 10k + + +@unique +class StateResource(Enum): + ACTORS = "actors" + JOBS = "jobs" + PLACEMENT_GROUPS = "placement_groups" + NODES = "nodes" + WORKERS = "workers" + TASKS = "tasks" + OBJECTS = "objects" + RUNTIME_ENVS = "runtime_envs" + CLUSTER_EVENTS = "cluster_events" + + +@unique +class SummaryResource(Enum): + ACTORS = "actors" + TASKS = "tasks" + OBJECTS = "objects" + + +SupportedFilterType = Union[str, bool, int, float] + + +PredicateType = str # Literal["=", "!="] + + +class Humanify: + """A class containing default methods to + convert units into a human readable string.""" + + def timestamp(x: float): + """Converts milliseconds to a datetime object.""" + return str(datetime.datetime.fromtimestamp(x / 1000)) + + def memory(x: int): + """Converts raw bytes to a human readable memory size.""" + if x >= 2**30: + return str(format(x / (2**30), ".3f")) + " GiB" + elif x >= 2**20: + return str(format(x / (2**20), ".3f")) + " MiB" + elif x >= 2**10: + return str(format(x / (2**10), ".3f")) + " KiB" + return str(format(x, ".3f")) + " B" + + def duration(x: int): + """Converts milliseconds to a human readable duration.""" + return str(datetime.timedelta(milliseconds=x)) + + def events(events: List[dict]): + """Converts a list of task events into a human readable format.""" + for event in events: + if "created_ms" in event: + event["created_ms"] = Humanify.timestamp(event["created_ms"]) + return events + + def node_resources(resources: dict): + """Converts a node's resources into a human readable format.""" + for resource in resources: + if "memory" in resource: + resources[resource] = Humanify.memory(resources[resource]) + return resources + + +@dataclass(init=not IS_PYDANTIC_2) +class ListApiOptions: + # Maximum number of entries to return + limit: int = DEFAULT_LIMIT + # The timeout for the API call. + timeout: int = DEFAULT_RPC_TIMEOUT + # If True, more detailed output will be printed. + # The API could query more sources than detail == False + # to get more data in detail. + detail: bool = False + # Filters. Each tuple pair (key, predicate, value) means key predicate value. + # If there's more than 1 filter, it means AND. + # E.g., [(key, "=", val), (key2, "!=" val2)] means (key=val) AND (key2!=val2) + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = field( + default_factory=list + ) + # [only tasks] If driver tasks should be excluded. + exclude_driver: bool = True + # When the request is processed on the server side, + # we should apply multiplier so that server side can finish + # processing a request within timeout. Otherwise, + # timeout will always lead Http timeout. + server_timeout_multiplier: float = 0.8 + + def __post_init__(self): + # To return the data to users, when there's a partial failure + # we need to have a timeout that's smaller than the users' timeout. + # 80% is configured arbitrarily. + self.timeout = max(1, int(self.timeout * self.server_timeout_multiplier)) + assert self.timeout != 0, "0 second timeout is not supported." + if self.filters is None: + self.filters = [] + + for filter in self.filters: + _, filter_predicate, _ = filter + if filter_predicate != "=" and filter_predicate != "!=": + raise ValueError( + f"Unsupported filter predicate {filter_predicate} is given. " + "Available predicates: =, !=." + ) + + def has_conflicting_filters(self) -> bool: + # Check the filters in the ListApiOptions conflicts. Specifically for: + # - multiple '=' filters with the same key but different values. + # TODO(myan): More conflicts situation can be added for further optimization. + # For exmaple, 2 filters with same key and same value but one with '=' predicate + # and ther other with '!=' predicate + equal_filters = {} + for filter in self.filters: + filter_key, filter_predicate, filter_value = filter + if filter_predicate == "=": + if ( + filter_key in equal_filters + and equal_filters[filter_key] != filter_value + ): + warnings.warn( + "There are multiple '=' filters with the same " + f"key '{filter_key}' but different values" + f"'{equal_filters[filter_key]}' & '{filter_value}'. " + "Empty result set will be returned", + UserWarning, + ) + return True + elif filter_key not in equal_filters: + equal_filters[filter_key] = filter_value + + return False + + +@dataclass(init=not IS_PYDANTIC_2) +class GetApiOptions: + # Timeout for the HTTP request + timeout: int = DEFAULT_RPC_TIMEOUT + # When the request is processed on the server side, + # we should apply multiplier so that server side can finish + # processing a request within timeout. Otherwise, + # timeout will always lead Http timeout. + server_timeout_multiplier: float = 0.8 + + def __post_init__(self): + # To return the data to users, when there's a partial failure + # we need to have a timeout that's smaller than the users' timeout. + # 80% is configured arbitrarily. + self.timeout = max(1, int(self.timeout * self.server_timeout_multiplier)) + assert self.timeout != 0, "0 second timeout is not supported." + + +@dataclass(init=not IS_PYDANTIC_2) +class SummaryApiOptions: + # Timeout for the HTTP request + timeout: int = DEFAULT_RPC_TIMEOUT + + # Filters. Each tuple pair (key, predicate, value) means key predicate value. + # If there's more than 1 filter, it means AND. + # E.g., [(key, "=", val), (key2, "!=" val2)] means (key=val) AND (key2!=val2) + # For summary endpoints that call list under the hood, we'll pass + # these filters directly into the list call. + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = field( + default_factory=list + ) + + # Change out to summarize the output. There is a summary_by value for each entity. + # Tasks: by func_name + # Actors: by class + # Objects: by callsite + summary_by: Optional[str] = None + + +def state_column(*, filterable: bool, detail: bool = False, format_fn=None, **kwargs): + """A wrapper around dataclass.field to add additional metadata. + + The metadata is used to define detail / filterable option of + each column. + + Args: + detail: If True, the column is used when detail == True + filterable: If True, the column can be used for filtering. + kwargs: The same kwargs for the `dataclasses.field` function. + """ + m = {"detail": detail, "filterable": filterable, "format_fn": format_fn} + # Default for detail field is None since it could be missing. + if detail and "default" not in kwargs: + kwargs["default"] = None + + if "metadata" in kwargs: + # Metadata explicitly specified, so add detail and filterable if missing. + kwargs["metadata"].update(m) + else: + # Metadata not explicitly specified, so add it. + kwargs["metadata"] = m + return field(**kwargs) + + +class StateSchema(ABC): + """Schema class for Ray resource abstraction. + + The child class must be dataclass. All child classes + - perform runtime type checking upon initialization. + - are supposed to use `state_column` instead of `field`. + It will allow the class to return filterable/detail columns. + If `state_column` is not specified, that column is not filterable + and for non-detail output. + + For example, + ``` + @dataclass + class State(StateSchema): + column_a: str + column_b: int = state_column(detail=True, filterable=True) + + s = State(column_a="abc", b=1) + # Returns {"column_b"} + s.filterable_columns() + # Returns {"column_a"} + s.base_columns() + # Returns {"column_a", "column_b"} + s.columns() + ``` + + In addition, the schema also provides a humanify abstract method to + convert the state object into something human readable, ready for printing. + + Subclasses should override this method, providing logic to convert its own fields + to something human readable, packaged and returned in a dict. + + Each field that wants to be humanified should include a 'format_fn' key in its + metadata dictionary. + """ + + @classmethod + def humanify(cls, state: dict) -> dict: + """Convert the given state object into something human readable.""" + for f in fields(cls): + if ( + f.metadata.get("format_fn") is not None + and f.name in state + and state[f.name] is not None + ): + try: + state[f.name] = f.metadata["format_fn"](state[f.name]) + except Exception as e: + logger.error(f"Failed to format {f.name}:{state[f.name]} with {e}") + return state + + @classmethod + def list_columns(cls, detail: bool = True) -> List[str]: + """Return a list of columns.""" + cols = [] + for f in fields(cls): + if detail: + cols.append(f.name) + elif not f.metadata.get("detail", False): + cols.append(f.name) + + return cols + + @classmethod + def columns(cls) -> Set[str]: + """Return a set of all columns.""" + return set(cls.list_columns(detail=True)) + + @classmethod + def filterable_columns(cls) -> Set[str]: + """Return a list of filterable columns""" + filterable = set() + for f in fields(cls): + if f.metadata.get("filterable", False): + filterable.add(f.name) + return filterable + + @classmethod + def base_columns(cls) -> Set[str]: + """Return a list of base columns. + + Base columns mean columns to return when detail == False. + """ + return set(cls.list_columns(detail=False)) + + @classmethod + def detail_columns(cls) -> Set[str]: + """Return a list of detail columns. + + Detail columns mean columns to return when detail == True. + """ + return set(cls.list_columns(detail=True)) + + def asdict(self): + return asdict(self) + + # Allow dict like access on the class directly for backward compatibility. + def __getitem__(self, key): + return getattr(self, key) + + def __setitem__(self, key, value): + setattr(self, key, value) + + def get(self, key, default=None): + return getattr(self, key, default) + + +def filter_fields(data: dict, state_dataclass: StateSchema, detail: bool) -> dict: + """Filter the given data's columns based on the given schema. + + Args: + data: A single data entry to filter columns. + state_dataclass: The schema to filter data. + detail: Whether or not it should include columns for detail output. + """ + filtered_data = {} + columns = state_dataclass.columns() if detail else state_dataclass.base_columns() + for col in columns: + if col in data: + filtered_data[col] = data[col] + else: + filtered_data[col] = None + return filtered_data + + +@dataclass(init=not IS_PYDANTIC_2) +class GetLogOptions: + timeout: int + node_id: Optional[str] = None + node_ip: Optional[str] = None + # One of {file, stream}. File means it will return the whole log. + # stream means it will keep the connection and streaming the log. + media_type: str = "file" + # The filename to match when finding the log to download from the Ray log directory. + # NOTE: This can be a nested path relative to the Ray log directory. + filename: Optional[str] = None + # The filename to download the log as on the client side. + # If not provided, the filename will be "file.txt". + download_filename: str = DEFAULT_DOWNLOAD_FILENAME + # The actor id of the log. It is used only for worker logs. + actor_id: Optional[str] = None + # The task id of the log. + task_id: Optional[str] = None + # The attempt number of the task. + attempt_number: int = 0 + # The pid of the log. It is used only for worker logs. + pid: Optional[int] = None + # Total log lines to return. + lines: int = 1000 + # The interval where new logs are streamed to. + # Should be used only when media_type == stream. + interval: Optional[float] = None + # The suffix of the log file if file resolution not through filename directly. + # Default to "out". + suffix: str = "out" + # The job submission id for submission job. This doesn't work for driver job + # since Ray doesn't log driver logs to file in the ray logs directory. + submission_id: Optional[str] = None + + def __post_init__(self): + if self.pid: + self.pid = int(self.pid) + if self.interval: + self.interval = float(self.interval) + self.lines = int(self.lines) + + if self.media_type == "file": + assert self.interval is None + if self.media_type not in ["file", "stream"]: + raise ValueError(f"Invalid media type: {self.media_type}") + if not (self.node_id or self.node_ip) and not (self.actor_id or self.task_id): + raise ValueError( + "node_id or node_ip must be provided as constructor arguments when no " + "actor or task_id is supplied as arguments." + ) + if self.node_id and self.node_ip: + raise ValueError( + "Both node_id and node_ip are given. Only one of them can be provided. " + f"Given node id: {self.node_id}, given node ip: {self.node_ip}" + ) + if not ( + self.actor_id + or self.task_id + or self.pid + or self.filename + or self.submission_id + ): + raise ValueError( + "None of actor_id, task_id, pid, submission_id or filename " + "is provided. At least one of them is required to fetch logs." + ) + + if self.suffix not in ["out", "err"]: + raise ValueError( + f"Invalid suffix: {self.suffix}. Must be one of 'out' or 'err'." + ) + + +# See the ActorTableData message in gcs.proto for all potential options that +# can be included in this class. +@dataclass(init=not IS_PYDANTIC_2) +class ActorState(StateSchema): + """Actor State""" + + #: The id of the actor. + actor_id: str = state_column(filterable=True) + #: The class name of the actor. + class_name: str = state_column(filterable=True) + #: The state of the actor. + #: + #: - DEPENDENCIES_UNREADY: Actor is waiting for dependency to be ready. + #: E.g., a new actor is waiting for object ref that's created from + #: other remote task. + #: - PENDING_CREATION: Actor's dependency is ready, but it is not created yet. + #: It could be because there are not enough resources, too many actor + #: entries in the scheduler queue, or the actor creation is slow + #: (e.g., slow runtime environment creation, + #: slow worker startup, or etc.). + #: - ALIVE: The actor is created, and it is alive. + #: - RESTARTING: The actor is dead, and it is restarting. + #: It is equivalent to `PENDING_CREATION`, + #: but means the actor was dead more than once. + #: - DEAD: The actor is permanatly dead. + state: TypeActorStatus = state_column(filterable=True) + #: The job id of this actor. + job_id: str = state_column(filterable=True) + #: The name of the actor given by the `name` argument. + name: Optional[str] = state_column(filterable=True) + #: The node id of this actor. + #: If the actor is restarting, it could be the node id + #: of the dead actor (and it will be re-updated when + #: the actor is successfully restarted). + node_id: Optional[str] = state_column(filterable=True) + #: The pid of the actor. 0 if it is not created yet. + pid: Optional[int] = state_column(filterable=True) + #: The namespace of the actor. + ray_namespace: Optional[str] = state_column(filterable=True) + #: The runtime environment information of the actor. + serialized_runtime_env: Optional[str] = state_column(filterable=False, detail=True) + #: The resource requirement of the actor. + required_resources: Optional[dict] = state_column(filterable=False, detail=True) + #: Actor's death information in detail. None if the actor is not dead yet. + death_cause: Optional[dict] = state_column(filterable=False, detail=True) + #: True if the actor is detached. False otherwise. + is_detached: Optional[bool] = state_column(filterable=False, detail=True) + #: The placement group id that's associated with this actor. + placement_group_id: Optional[str] = state_column(detail=True, filterable=True) + #: Actor's repr name if a customized __repr__ method exists, else empty string. + repr_name: Optional[str] = state_column(detail=True, filterable=True) + #: Number of restarts that has been tried on this actor. + num_restarts: int = state_column(filterable=False, detail=True) + #: Number of times this actor is restarted due to lineage reconstructions. + num_restarts_due_to_lineage_reconstruction: int = state_column( + filterable=False, detail=True + ) + #: Number of times this actor is restarted due to node preemption. + num_restarts_due_to_node_preemption: int = state_column( + filterable=False, detail=True + ) + #: The call site of the actor creation. + call_site: Optional[str] = state_column(detail=True, filterable=False) + #: The label selector for the actor. + label_selector: Optional[dict] = state_column(detail=True, filterable=False) + + +@dataclass(init=not IS_PYDANTIC_2) +class PlacementGroupState(StateSchema): + """PlacementGroup State""" + + #: The id of the placement group. + placement_group_id: str = state_column(filterable=True) + #: The name of the placement group if it is given by the name argument. + name: str = state_column(filterable=True) + #: The job id of the placement group. + creator_job_id: str = state_column(filterable=True) + #: The state of the placement group. + #: + #: - PENDING: The placement group creation is pending scheduling. + #: It could be because there's not enough resources, some of creation + #: stage has failed (e.g., failed to commit placement gropus because + #: the node is dead). + #: - CREATED: The placement group is created. + #: - REMOVED: The placement group is removed. + #: - RESCHEDULING: The placement group is rescheduling because some of + #: bundles are dead because they were on dead nodes. + state: TypePlacementGroupStatus = state_column(filterable=True) + #: The bundle specification of the placement group. + bundles: Optional[List[dict]] = state_column(filterable=False, detail=True) + #: True if the placement group is detached. False otherwise. + is_detached: Optional[bool] = state_column(filterable=True, detail=True) + #: The scheduling stats of the placement group. + stats: Optional[dict] = state_column(filterable=False, detail=True) + + +@dataclass(init=not IS_PYDANTIC_2) +class NodeState(StateSchema): + """Node State""" + + #: The id of the node. + node_id: str = state_column(filterable=True) + #: The ip address of the node. + node_ip: str = state_column(filterable=True) + #: If this is a head node. + is_head_node: bool = state_column(filterable=True) + #: The state of the node. + #: + #: ALIVE: The node is alive. + #: DEAD: The node is dead. + state: TypeNodeStatus = state_column(filterable=True) + #: The state message of the node. + #: This provides more detailed information about the node's state. + state_message: Optional[str] = state_column(filterable=False) + #: The name of the node if it is given by the name argument. + node_name: str = state_column(filterable=True) + #: The total resources of the node. + resources_total: dict = state_column( + filterable=False, format_fn=Humanify.node_resources + ) + #: The labels of the node. + labels: dict = state_column(filterable=False) + #: The time when the node (raylet) starts. + start_time_ms: Optional[int] = state_column( + filterable=False, detail=True, format_fn=Humanify.timestamp + ) + #: The time when the node exits. The timestamp could be delayed + #: if the node is dead unexpectedly (could be delayed + # up to 30 seconds). + end_time_ms: Optional[int] = state_column( + filterable=False, detail=True, format_fn=Humanify.timestamp + ) + + +# NOTE: Declaring this as dataclass would make __init__ not being called properly. +# NOTE: `JobDetails` will be `None` in the minimal install because Pydantic is not +# installed. Inheriting from `None` raises an exception. +class JobState(StateSchema, JobDetails if JobDetails is not None else object): + """The state of the job that's submitted by Ray's Job APIs or driver jobs""" + + def __init__(self, **kwargs): + JobDetails.__init__(self, **kwargs) + + @classmethod + def filterable_columns(cls) -> Set[str]: + # We are not doing any filtering since filtering is currently done + # at the backend. + return {"job_id", "type", "status", "submission_id"} + + @classmethod + def humanify(cls, state: dict) -> dict: + return state + + @classmethod + def list_columns(cls, detail: bool = True) -> List[str]: + if not detail: + return [ + "job_id", + "submission_id", + "entrypoint", + "type", + "status", + "message", + "error_type", + "driver_info", + ] + if JobDetails is None: + # We don't have pydantic in the dashboard. This is because + # we call this method at module import time, so we need to + # check if the class is a pydantic model. + return [] + + # TODO(aguo): Once we only support pydantic 2, we can remove this if check. + # In pydantic 2.0, `__fields__` has been renamed to `model_fields`. + return ( + list(JobDetails.model_fields.keys()) + if hasattr(JobDetails, "model_fields") + else list(JobDetails.__fields__.keys()) + ) + + def asdict(self): + return JobDetails.dict(self) + + @classmethod + def schema_dict(cls) -> Dict[str, Any]: + schema_types = cls.schema()["properties"] + # Get type name to actual type mapping. + return { + k: v["type"] for k, v in schema_types.items() if v.get("type") is not None + } + + +@dataclass(init=not IS_PYDANTIC_2) +class WorkerState(StateSchema): + """Worker State""" + + #: The id of the worker. + worker_id: str = state_column(filterable=True) + #: Whether or not if the worker is alive. + is_alive: bool = state_column(filterable=True) + #: The type of the worker. + #: + #: - WORKER: The regular Ray worker process that executes tasks or + # instantiates an actor. + #: - DRIVER: The driver (Python script that calls `ray.init`). + #: - SPILL_WORKER: The worker that spills objects. + #: - RESTORE_WORKER: The worker that restores objects. + worker_type: TypeWorkerType = state_column(filterable=True) + #: The exit type of the worker if the worker is dead. + #: + #: - SYSTEM_ERROR: Worker exit due to system level failures (i.e. worker crash). + #: - INTENDED_SYSTEM_EXIT: System-level exit that is intended. E.g., + #: Workers are killed because they are idle for a long time. + #: - USER_ERROR: Worker exits because of user error. + #: E.g., execptions from the actor initialization. + #: - INTENDED_USER_EXIT: Intended exit from users (e.g., users exit + #: workers with exit code 0 or exit initated by Ray API such as ray.kill). + exit_type: Optional[TypeWorkerExitType] = state_column(filterable=True) + #: The node id of the worker. + node_id: str = state_column(filterable=True) + #: The ip address of the worker. + ip: str = state_column(filterable=True) + #: The pid of the worker. + pid: int = state_column(filterable=True) + #: The exit detail of the worker if the worker is dead. + exit_detail: Optional[str] = state_column(detail=True, filterable=False) + #: The time worker is first launched. + #: -1 if the value doesn't exist. + #: The lifecycle of worker is as follow. + #: worker_launch_time_ms (process startup requested). + #: -> worker_launched_time_ms (process started). + #: -> start_time_ms (worker is ready to be used). + #: -> end_time_ms (worker is destroyed). + worker_launch_time_ms: Optional[int] = state_column( + filterable=False, + detail=True, + format_fn=lambda x: "" if x == -1 else Humanify.timestamp(x), + ) + #: The time worker is successfully launched + #: -1 if the value doesn't exist. + worker_launched_time_ms: Optional[int] = state_column( + filterable=False, + detail=True, + format_fn=lambda x: "" if x == -1 else Humanify.timestamp(x), + ) + #: The time when the worker is started and initialized. + #: 0 if the value doesn't exist. + start_time_ms: Optional[int] = state_column( + filterable=False, detail=True, format_fn=Humanify.timestamp + ) + #: The time when the worker exits. The timestamp could be delayed + #: if the worker is dead unexpectedly. + #: 0 if the value doesn't exist. + end_time_ms: Optional[int] = state_column( + filterable=False, detail=True, format_fn=Humanify.timestamp + ) + # the debugger port of the worker + debugger_port: Optional[int] = state_column(filterable=True, detail=True) + # the number of threads paused in this worker + num_paused_threads: Optional[int] = state_column(filterable=True, detail=True) + + +@dataclass(init=not IS_PYDANTIC_2) +class ClusterEventState(StateSchema): + severity: str = state_column(filterable=True) + time: str = state_column(filterable=False) + source_type: str = state_column(filterable=True) + message: str = state_column(filterable=False) + event_id: str = state_column(filterable=True) + custom_fields: Optional[dict] = state_column(filterable=False, detail=True) + + +@dataclass(init=not IS_PYDANTIC_2) +class TaskState(StateSchema): + """Task State""" + + #: The id of the task. + task_id: str = state_column(filterable=True) + #: The attempt (retry) number of the task. + attempt_number: int = state_column(filterable=True) + #: The name of the task if it is given by the name argument. + name: str = state_column(filterable=True) + #: The state of the task. + #: + #: Refer to src/ray/protobuf/common.proto for a detailed explanation of the state + #: breakdowns and typical state transition flow. + #: + state: TypeTaskStatus = state_column(filterable=True) + #: The job id of this task. + job_id: str = state_column(filterable=True) + #: The actor id that's associated with this task. + #: It is empty if there's no relevant actors. + actor_id: Optional[str] = state_column(filterable=True) + #: The type of the task. + #: + #: - NORMAL_TASK: Tasks created by `func.remote()`` + #: - ACTOR_CREATION_TASK: Actors created by `class.remote()` + #: - ACTOR_TASK: Actor tasks submitted by `actor.method.remote()` + #: - DRIVER_TASK: Driver (A script that calls `ray.init`). + type: TypeTaskType = state_column(filterable=True) + #: The name of the task. If is the name of the function + #: if the type is a task or an actor task. + #: It is the name of the class if it is a actor scheduling task. + func_or_class_name: str = state_column(filterable=True) + #: The parent task id. If the parent is a normal task, it will be the task's id. + #: If the parent runs in a concurrent actor (async actor or threaded actor), + #: it will be the actor's creation task id. + parent_task_id: str = state_column(filterable=True) + #: Id of the node that runs the task. If the task is retried, it could + #: contain the node id of the previous executed task. + #: If empty, it means the task hasn't been scheduled yet. + node_id: Optional[str] = state_column(filterable=True) + #: The worker id that's associated with this task. + worker_id: Optional[str] = state_column(filterable=True) + #: The worker's pid that's associated with this task. + worker_pid: Optional[int] = state_column(filterable=True) + #: Task error type. + error_type: Optional[str] = state_column(filterable=True) + #: The language of the task. E.g., Python, Java, or Cpp. + language: Optional[str] = state_column(detail=True, filterable=True) + #: The required resources to execute the task. + required_resources: Optional[dict] = state_column(detail=True, filterable=False) + #: The runtime environment information for the task. + runtime_env_info: Optional[dict] = state_column(detail=True, filterable=False) + #: The placement group id that's associated with this task. + placement_group_id: Optional[str] = state_column(detail=True, filterable=True) + #: The list of events of the given task. + #: Refer to src/ray/protobuf/common.proto for a detailed explanation of the state + #: breakdowns and typical state transition flow. + events: Optional[List[dict]] = state_column( + detail=True, filterable=False, format_fn=Humanify.events + ) + #: The list of profile events of the given task. + profiling_data: Optional[dict] = state_column(detail=True, filterable=False) + #: The time when the task is created. A Unix timestamp in ms. + creation_time_ms: Optional[int] = state_column( + detail=True, + filterable=False, + format_fn=Humanify.timestamp, + ) + #: The time when the task starts to run. A Unix timestamp in ms. + start_time_ms: Optional[int] = state_column( + detail=True, + filterable=False, + format_fn=Humanify.timestamp, + ) + #: The time when the task is finished or failed. A Unix timestamp in ms. + end_time_ms: Optional[int] = state_column( + detail=True, filterable=False, format_fn=Humanify.timestamp + ) + #: The task logs info, e.g. offset into the worker log file when the task + #: starts/finishes. + #: None if the task is from a concurrent actor (e.g. async actor or threaded actor) + task_log_info: Optional[dict] = state_column(detail=True, filterable=False) + #: Task error detail info. + error_message: Optional[str] = state_column(detail=True, filterable=False) + # Is task paused by the debugger + is_debugger_paused: Optional[bool] = state_column(detail=True, filterable=True) + #: The call site of the task. + call_site: Optional[str] = state_column(detail=True, filterable=False) + #: The label selector for the task. + label_selector: Optional[dict] = state_column(detail=True, filterable=False) + + +@dataclass(init=not IS_PYDANTIC_2) +class ObjectState(StateSchema): + """Object State""" + + #: The id of the object. + object_id: str = state_column(filterable=True) + #: The size of the object in mb. + object_size: int = state_column(filterable=True, format_fn=Humanify.memory) + #: The status of the task that creates the object. + #: + #: - NIL: We don't have a status for this task because we are not the owner or the + #: task metadata has already been deleted. + #: - WAITING_FOR_DEPENDENCIES: The task is waiting for its dependencies + #: to be created. + #: - SCHEDULED: All dependencies have been created and the task is + #: scheduled to execute. + #: It could be because the task is waiting for resources, + #: runtime environmenet creation, fetching dependencies to the + #: local node, and etc.. + #: - FINISHED: The task finished successfully. + #: - WAITING_FOR_EXECUTION: The task is scheduled properly and + #: waiting for execution. It includes time to deliver the task + #: to the remote worker + queueing time from the execution side. + #: - RUNNING: The task that is running. + task_status: TypeTaskStatus = state_column(filterable=True) + #: The number of times the task has been executed (including the current execution) + attempt_number: int = state_column(filterable=True) + #: The reference type of the object. + #: See :ref:`Debugging with Ray Memory ` for more details. + #: + #: - ACTOR_HANDLE: The reference is an actor handle. + #: - PINNED_IN_MEMORY: The object is pinned in memory, meaning there's + #: in-flight `ray.get` on this reference. + #: - LOCAL_REFERENCE: There's a local reference (e.g., Python reference) + #: to this object reference. The object won't be GC'ed until all of them is gone. + #: - USED_BY_PENDING_TASK: The object reference is passed to other tasks. E.g., + #: `a = ray.put()` -> `task.remote(a)`. In this case, a is used by a + #: pending task `task`. + #: - CAPTURED_IN_OBJECT: The object is serialized by other objects. E.g., + #: `a = ray.put(1)` -> `b = ray.put([a])`. a is serialized within a list. + #: - UNKNOWN_STATUS: The object ref status is unkonwn. + reference_type: TypeReferenceType = state_column(filterable=True) + #: The callsite of the object. + call_site: str = state_column(filterable=True) + #: The worker type that creates the object. + #: + #: - WORKER: The regular Ray worker process that executes tasks or + #: instantiates an actor. + #: - DRIVER: The driver (Python script that calls `ray.init`). + #: - SPILL_WORKER: The worker that spills objects. + #: - RESTORE_WORKER: The worker that restores objects. + type: TypeWorkerType = state_column(filterable=True) + #: The pid of the owner. + pid: int = state_column(filterable=True) + #: The ip address of the owner. + ip: str = state_column(filterable=True) + + +@dataclass(init=not IS_PYDANTIC_2) +class RuntimeEnvState(StateSchema): + """Runtime Environment State""" + + #: The runtime environment spec. + runtime_env: dict = state_column(filterable=True) + #: Whether or not the runtime env creation has succeeded. + success: bool = state_column(filterable=True) + #: The latency of creating the runtime environment. + #: Available if the runtime env is successfully created. + creation_time_ms: Optional[float] = state_column( + filterable=False, format_fn=Humanify.timestamp + ) + #: The node id of this runtime environment. + node_id: str = state_column(filterable=True) + #: The number of actors and tasks that use this runtime environment. + ref_cnt: Optional[int] = state_column(detail=True, filterable=False) + #: The error message if the runtime environment creation has failed. + #: Available if the runtime env is failed to be created. + error: Optional[str] = state_column(detail=True, filterable=True) + + +AVAILABLE_STATES = [ + ActorState, + PlacementGroupState, + NodeState, + WorkerState, + JobState, + TaskState, + ObjectState, + RuntimeEnvState, +] + + +for state in AVAILABLE_STATES: + if len(state.filterable_columns()) > 0: + filterable_cols = "\n\n ".join(state.filterable_columns()) + state.__doc__ += f""" +\nBelow columns can be used for the `--filter` option. +\n + {filterable_cols} +\n +""" + + if len(state.detail_columns()) > 0: + detail_cols = "\n\n ".join(state.detail_columns()) + state.__doc__ += f""" +\nBelow columns are available only when `get` API is used, +\n`--detail` is specified through CLI, or `detail=True` is given to Python APIs. +\n +\n + {detail_cols} +\n +""" + + +@dataclass(init=not IS_PYDANTIC_2) +class ListApiResponse: + # NOTE(rickyyx): We currently perform hard truncation when querying + # resources which could have a large number (e.g. asking raylets for + # the number of all objects). + # The returned of resources seen by the user will go through from the + # below funnel: + # - total + # | With truncation at the data source if the number of returned + # | resource exceeds `RAY_MAX_LIMIT_FROM_DATA_SOURCE` + # v + # - num_after_truncation + # | With filtering at the state API server + # v + # - num_filtered + # | With limiting, + # | set by min(`RAY_MAX_LIMIT_FROM_API_SERER`, ) + # v + # - len(result) + + # Total number of the available resource from the cluster. + total: int + # Number of resources returned by data sources after truncation + num_after_truncation: int + # Number of resources after filtering + num_filtered: int + # Returned data. None if no data is returned. + result: List[Dict] + # List API can have a partial failure if queries to + # all sources fail. For example, getting object states + # require to ping all raylets, and it is possible some of + # them fails. Note that it is impossible to guarantee high + # availability of data because ray's state information is + # not replicated. + partial_failure_warning: Optional[str] = "" + # A list of warnings to print. + warnings: Optional[List[str]] = None + + +""" +Summary API schema +""" + +DRIVER_TASK_ID_PREFIX = "ffffffffffffffffffffffffffffffffffffffff" + + +@dataclass(init=not IS_PYDANTIC_2) +class TaskSummaryPerFuncOrClassName: + #: The function or class name of this task. + func_or_class_name: str + #: The type of the class. Equivalent to protobuf TaskType. + type: str + #: State name to the count dict. State name is equivalent to + #: the protobuf TaskStatus. + state_counts: Dict[TypeTaskStatus, int] = field(default_factory=dict) + + +@dataclass +class Link: + #: The type of entity to link to + type: str + #: The id of the entity to link to + id: str + + +@dataclass(init=not IS_PYDANTIC_2) +class NestedTaskSummary: + #: The name of this task group + name: str + #: A unique identifier for this group + key: str + #: The type of the class. Equivalent to protobuf TaskType, + #: "ACTOR" if it represents an Actor, or "GROUP" if it's a grouping of tasks. + type: str + #: Unix timestamp to use to sort the task group. + timestamp: Optional[int] = None + #: State name to the count dict. State name is equivalent to + #: the protobuf TaskStatus. + state_counts: Dict[TypeTaskStatus, int] = field(default_factory=dict) + #: The child + children: List["NestedTaskSummary"] = field(default_factory=list) + #: A link to more details about this summary. + link: Optional[Link] = None + + +@dataclass +class TaskSummaries: + #: Group key -> summary. + #: Right now, we only have func_class_name as a key. + # TODO(sang): Support the task group abstraction. + summary: Union[Dict[str, TaskSummaryPerFuncOrClassName], List[NestedTaskSummary]] + #: Total Ray tasks. + total_tasks: int + #: Total actor tasks. + total_actor_tasks: int + #: Total scheduled actors. + total_actor_scheduled: int + summary_by: str = "func_name" + + @classmethod + def to_summary_by_func_name(cls, *, tasks: List[Dict]) -> "TaskSummaries": + # NOTE: The argument tasks contains a list of dictionary + # that have the same k/v as TaskState. + summary = {} + total_tasks = 0 + total_actor_tasks = 0 + total_actor_scheduled = 0 + + for task in tasks: + key = task["func_or_class_name"] + if key not in summary: + summary[key] = TaskSummaryPerFuncOrClassName( + func_or_class_name=task["func_or_class_name"], + type=task["type"], + ) + task_summary = summary[key] + + state = task["state"] + if state not in task_summary.state_counts: + task_summary.state_counts[state] = 0 + task_summary.state_counts[state] += 1 + + type_enum = TaskType.DESCRIPTOR.values_by_name[task["type"]].number + if type_enum == TaskType.NORMAL_TASK: + total_tasks += 1 + elif type_enum == TaskType.ACTOR_CREATION_TASK: + total_actor_scheduled += 1 + elif type_enum == TaskType.ACTOR_TASK: + total_actor_tasks += 1 + + return TaskSummaries( + summary=summary, + total_tasks=total_tasks, + total_actor_tasks=total_actor_tasks, + total_actor_scheduled=total_actor_scheduled, + summary_by="func_name", + ) + + @classmethod + def to_summary_by_lineage( + cls, *, tasks: List[Dict], actors: List[Dict] + ) -> "TaskSummaries": + """ + This summarizes tasks by lineage. + i.e. A task will be grouped with another task if they have the + same parent. + + This does things in 4 steps. + Step 1: Iterate through all tasks and keep track of them by id and ownership + Step 2: Put the tasks in a tree structure based on ownership + Step 3: Merge together siblings in the tree if there are more + than one with the same name. + Step 4: Sort by running and then errored and then successful tasks + Step 5: Total the children + + This can probably be more efficient if we merge together some steps to + reduce the amount of iterations but this algorithm produces very easy to + understand code. We can optimize in the future. + """ + # NOTE: The argument tasks contains a list of dictionary + # that have the same k/v as TaskState. + + tasks_by_id = {} + task_group_by_id = {} + actor_creation_task_id_for_actor_id = {} + summary = [] + total_tasks = 0 + total_actor_tasks = 0 + total_actor_scheduled = 0 + + # Step 1 + # We cannot assume that a parent task always comes before the child task + # So we need to keep track of all tasks by ids so we can quickly find the + # parent. + # We also track the actor creation tasks so we can quickly figure out the + # ownership of actors. + for task in tasks: + tasks_by_id[task["task_id"]] = task + type_enum = TaskType.DESCRIPTOR.values_by_name[task["type"]].number + if type_enum == TaskType.ACTOR_CREATION_TASK: + actor_creation_task_id_for_actor_id[task["actor_id"]] = task["task_id"] + + actor_dict = {actor["actor_id"]: actor for actor in actors} + + def get_or_create_task_group(task_id: str) -> Optional[NestedTaskSummary]: + """ + Gets an already created task_group + OR + Creates a task group and puts it in the right place under its parent. + For actor tasks, the parent is the Actor that owns it. For all other + tasks, the owner is the driver or task that created it. + + Returns None if there is missing data about the task or one of its parents. + + For task groups that represents actors, the id is in the + format actor:{actor_id} + """ + if task_id in task_group_by_id: + return task_group_by_id[task_id] + + task = tasks_by_id.get(task_id) + if not task: + logger.debug(f"We're missing data about {task_id}") + # We're missing data about this parent. So we're dropping the whole + # tree at that node. + return None + + # Use name first which allows users to customize the name of + # their remote function call using the name option. + func_name = task["name"] or task["func_or_class_name"] + task_id = task["task_id"] + type_enum = TaskType.DESCRIPTOR.values_by_name[task["type"]].number + + task_group_by_id[task_id] = NestedTaskSummary( + name=func_name, + key=task_id, + type=task["type"], + timestamp=task["creation_time_ms"], + link=Link(type="task", id=task_id), + ) + + # Set summary in right place under parent + if ( + type_enum == TaskType.ACTOR_TASK + or type_enum == TaskType.ACTOR_CREATION_TASK + ): + # For actor tasks, the parent is the actor and not the parent task. + parent_task_group = get_or_create_actor_task_group(task["actor_id"]) + if parent_task_group: + parent_task_group.children.append(task_group_by_id[task_id]) + else: + parent_task_id = task["parent_task_id"] + if not parent_task_id or parent_task_id.startswith( + DRIVER_TASK_ID_PREFIX + ): + summary.append(task_group_by_id[task_id]) + else: + parent_task_group = get_or_create_task_group(parent_task_id) + if parent_task_group: + parent_task_group.children.append(task_group_by_id[task_id]) + + return task_group_by_id[task_id] + + def get_or_create_actor_task_group( + actor_id: str, + ) -> Optional[NestedTaskSummary]: + """ + Gets an existing task group that represents an actor. + OR + Creates a task group that represents an actor. The owner of the actor is + the parent of the creation_task that created that actor. + + Returns None if there is missing data about the actor or one of its parents. + """ + key = f"actor:{actor_id}" + actor = actor_dict.get(actor_id) + if key not in task_group_by_id: + creation_task_id = actor_creation_task_id_for_actor_id.get(actor_id) + creation_task = tasks_by_id.get(creation_task_id) + + if not creation_task: + logger.debug(f"We're missing data about actor {actor_id}") + # We're missing data about the parent. So we're dropping the whole + # tree at that node. + return None + + # TODO(rickyx) + # We are using repr name for grouping actors if exists, + # else use class name. We should be using some group_name in the future. + if actor is None: + logger.debug( + f"We are missing actor info for actor {actor_id}, " + f"even though creation task exists: {creation_task}" + ) + [actor_name, *rest] = creation_task["func_or_class_name"].split(".") + else: + actor_name = ( + actor["repr_name"] + if actor["repr_name"] + else actor["class_name"] + ) + + task_group_by_id[key] = NestedTaskSummary( + name=actor_name, + key=key, + type="ACTOR", + timestamp=task["creation_time_ms"], + link=Link(type="actor", id=actor_id), + ) + + parent_task_id = creation_task["parent_task_id"] + if not parent_task_id or parent_task_id.startswith( + DRIVER_TASK_ID_PREFIX + ): + summary.append(task_group_by_id[key]) + else: + parent_task_group = get_or_create_task_group(parent_task_id) + if parent_task_group: + parent_task_group.children.append(task_group_by_id[key]) + + return task_group_by_id[key] + + # Step 2: Create the tree structure based on ownership + for task in tasks: + task_id = task["task_id"] + + task_group = get_or_create_task_group(task_id) + + if not task_group: + # We are probably missing data about this task or one of its parents. + continue + + state = task["state"] + if state not in task_group.state_counts: + task_group.state_counts[state] = 0 + task_group.state_counts[state] += 1 + + type_enum = TaskType.DESCRIPTOR.values_by_name[task["type"]].number + if type_enum == TaskType.NORMAL_TASK: + total_tasks += 1 + elif type_enum == TaskType.ACTOR_CREATION_TASK: + total_actor_scheduled += 1 + elif type_enum == TaskType.ACTOR_TASK: + total_actor_tasks += 1 + + def merge_sibings_for_task_group( + siblings: List[NestedTaskSummary], + ) -> Tuple[List[NestedTaskSummary], Optional[int]]: + """ + Merges task summaries with the same name into a group if there are more than + one child with that name. + + Args: + siblings: A list of NestedTaskSummary's to merge together + + Returns + Index 0: A list of NestedTaskSummary's which have been merged + Index 1: The smallest timestamp amongst the siblings + """ + if not len(siblings): + return siblings, None + + # Group by name + groups = {} + min_timestamp = None + + for child in siblings: + child.children, child_min_timestamp = merge_sibings_for_task_group( + child.children + ) + if child_min_timestamp and child_min_timestamp < ( + child.timestamp or sys.maxsize + ): + child.timestamp = child_min_timestamp + + if child.name not in groups: + groups[child.name] = NestedTaskSummary( + name=child.name, + key=child.name, + type="GROUP", + ) + groups[child.name].children.append(child) + if child.timestamp and child.timestamp < ( + groups[child.name].timestamp or sys.maxsize + ): + groups[child.name].timestamp = child.timestamp + if child.timestamp < (min_timestamp or sys.maxsize): + min_timestamp = child.timestamp + + # Take the groups that have more than one children and return it. + # For groups with just one child, return the child itself instead of + # creating a group. + return [ + group if len(group.children) > 1 else group.children[0] + for group in groups.values() + ], min_timestamp + + # Step 3 + summary, _ = merge_sibings_for_task_group(summary) + + def get_running_tasks_count(task_group: NestedTaskSummary) -> int: + return ( + task_group.state_counts.get("RUNNING", 0) + + task_group.state_counts.get("RUNNING_IN_RAY_GET", 0) + + task_group.state_counts.get("RUNNING_IN_RAY_WAIT", 0) + ) + + def get_pending_tasks_count(task_group: NestedTaskSummary) -> int: + return ( + task_group.state_counts.get("PENDING_ARGS_AVAIL", 0) + + task_group.state_counts.get("PENDING_NODE_ASSIGNMENT", 0) + + task_group.state_counts.get("PENDING_OBJ_STORE_MEM_AVAIL", 0) + + task_group.state_counts.get("PENDING_ARGS_FETCH", 0) + ) + + def sort_task_groups(task_groups: List[NestedTaskSummary]) -> None: + # Sort by running tasks, pending tasks, failed tasks, timestamp, + # and actor_creation_task + # Put actor creation tasks above other tasks with the same timestamp + task_groups.sort(key=lambda x: 0 if x.type == "ACTOR_CREATION_TASK" else 1) + task_groups.sort(key=lambda x: x.timestamp or sys.maxsize) + task_groups.sort( + key=lambda x: x.state_counts.get("FAIELD", 0), reverse=True + ) + task_groups.sort(key=get_pending_tasks_count, reverse=True) + task_groups.sort(key=get_running_tasks_count, reverse=True) + + def calc_total_for_task_group( + task_group: NestedTaskSummary, + ) -> NestedTaskSummary: + """ + Calculates the total of a group as the sum of all children. + Sorts children by timestamp + """ + if not len(task_group.children): + return task_group + + for child in task_group.children: + totaled = calc_total_for_task_group(child) + + for state, count in totaled.state_counts.items(): + task_group.state_counts[state] = ( + task_group.state_counts.get(state, 0) + count + ) + + sort_task_groups(task_group.children) + + return task_group + + # Step 4 + summary = [calc_total_for_task_group(task_group) for task_group in summary] + sort_task_groups(summary) + + return TaskSummaries( + summary=summary, + total_tasks=total_tasks, + total_actor_tasks=total_actor_tasks, + total_actor_scheduled=total_actor_scheduled, + summary_by="lineage", + ) + + +@dataclass(init=not IS_PYDANTIC_2) +class ActorSummaryPerClass: + #: The class name of the actor. + class_name: str + #: State name to the count dict. State name is equivalent to + #: the protobuf ActorState. + state_counts: Dict[TypeActorStatus, int] = field(default_factory=dict) + + +@dataclass +class ActorSummaries: + #: Group key (actor class name) -> summary + summary: Dict[str, ActorSummaryPerClass] + #: Total number of actors + total_actors: int + summary_by: str = "class" + + @classmethod + def to_summary(cls, *, actors: List[Dict]): + # NOTE: The argument tasks contains a list of dictionary + # that have the same k/v as ActorState. + summary = {} + total_actors = 0 + + for actor in actors: + key = actor["class_name"] + if key not in summary: + summary[key] = ActorSummaryPerClass( + class_name=actor["class_name"], + ) + actor_summary = summary[key] + + state = actor["state"] + if state not in actor_summary.state_counts: + actor_summary.state_counts[state] = 0 + actor_summary.state_counts[state] += 1 + + total_actors += 1 + + return ActorSummaries( + summary=summary, + total_actors=total_actors, + ) + + +@dataclass(init=not IS_PYDANTIC_2) +class ObjectSummaryPerKey: + #: Total number of objects of the type. + total_objects: int + #: Total size in mb. + total_size_mb: float + #: Total number of workers that reference the type of objects. + total_num_workers: int + #: Total number of nodes that reference the type of objects. + total_num_nodes: int + #: State name to the count dict. State name is equivalent to + #: ObjectState. + task_state_counts: Dict[TypeTaskStatus, int] = field(default_factory=dict) + #: Attempt number to the count dict. The attempt number include the current + #: execution + task_attempt_number_counts: Dict[str, int] = field(default_factory=dict) + #: Ref count type to the count dict. State name is equivalent to + #: ObjectState. + ref_type_counts: Dict[TypeReferenceType, int] = field(default_factory=dict) + + +@dataclass +class ObjectSummaries: + #: Group key (actor class name) -> summary + summary: Dict[str, ObjectSummaryPerKey] + #: Total number of referenced objects in the cluster. + total_objects: int + #: Total size of referenced objects in the cluster in MB. + total_size_mb: float + #: Whether or not the callsite collection is enabled. + callsite_enabled: bool + summary_by: str = "callsite" + + @classmethod + def to_summary(cls, *, objects: List[Dict]): + # NOTE: The argument tasks contains a list of dictionary + # that have the same k/v as ObjectState. + summary = {} + total_objects = 0 + total_size_mb = 0 + key_to_workers = {} + key_to_nodes = {} + callsite_enabled = True + + for object in objects: + key = object["call_site"] + if key == "disabled": + callsite_enabled = False + if key not in summary: + summary[key] = ObjectSummaryPerKey( + total_objects=0, + total_size_mb=0, + total_num_workers=0, + total_num_nodes=0, + ) + key_to_workers[key] = set() + key_to_nodes[key] = set() + + object_summary = summary[key] + + task_state = object["task_status"] + if task_state not in object_summary.task_state_counts: + object_summary.task_state_counts[task_state] = 0 + object_summary.task_state_counts[task_state] += 1 + + attempt_number = str(object["attempt_number"]) + if attempt_number not in object_summary.task_attempt_number_counts: + object_summary.task_attempt_number_counts[attempt_number] = 0 + object_summary.task_attempt_number_counts[attempt_number] += 1 + + ref_type = object["reference_type"] + if ref_type not in object_summary.ref_type_counts: + object_summary.ref_type_counts[ref_type] = 0 + object_summary.ref_type_counts[ref_type] += 1 + object_summary.total_objects += 1 + total_objects += 1 + + size_bytes = object["object_size"] + # object_size's unit is byte by default. It is -1, if the size is + # unknown. + if size_bytes != -1: + object_summary.total_size_mb += size_bytes / 1024**2 + total_size_mb += size_bytes / 1024**2 + + key_to_workers[key].add(object["pid"]) + key_to_nodes[key].add(object["ip"]) + + # Convert set of pid & node ips to length. + for key, workers in key_to_workers.items(): + summary[key].total_num_workers = len(workers) + for key, nodes in key_to_nodes.items(): + summary[key].total_num_nodes = len(nodes) + + return ObjectSummaries( + summary=summary, + total_objects=total_objects, + total_size_mb=total_size_mb, + callsite_enabled=callsite_enabled, + ) + + +@dataclass(init=not IS_PYDANTIC_2) +class StateSummary: + #: Node ID -> summary per node + #: If the data is not required to be orgnized per node, it will contain + #: a single key, "cluster". + node_id_to_summary: Dict[str, Union[TaskSummaries, ActorSummaries, ObjectSummaries]] + + +@dataclass(init=not IS_PYDANTIC_2) +class SummaryApiResponse: + # Carried over from ListApiResponse + # We currently use list API for listing the resources + total: int + # Carried over from ListApiResponse + # Number of resources returned by data sources after truncation + num_after_truncation: int + # Number of resources after filtering + num_filtered: int + result: StateSummary = None + partial_failure_warning: Optional[str] = "" + # A list of warnings to print. + warnings: Optional[List[str]] = None + + +def resource_to_schema(resource: StateResource) -> StateSchema: + if resource == StateResource.ACTORS: + return ActorState + elif resource == StateResource.JOBS: + return JobState + elif resource == StateResource.NODES: + return NodeState + elif resource == StateResource.OBJECTS: + return ObjectState + elif resource == StateResource.PLACEMENT_GROUPS: + return PlacementGroupState + elif resource == StateResource.RUNTIME_ENVS: + return RuntimeEnvState + elif resource == StateResource.TASKS: + return TaskState + elif resource == StateResource.WORKERS: + return WorkerState + elif resource == StateResource.CLUSTER_EVENTS: + return ClusterEventState + else: + assert False, "Unreachable" + + +def protobuf_message_to_dict( + message, + fields_to_decode: List[str], + preserving_proto_field_name: bool = True, +) -> dict: + """Convert a protobuf message to dict + + Args: + fields_to_decode: field names which will be decoded from binary to hex. + preserving_proto_field_name: a pass-through option for protobuf message + method. See google.protobuf MessageToDict + + Return: + Dictionary of the converted rpc protobuf. + """ + return dashboard_utils.message_to_dict( + message, + fields_to_decode, + always_print_fields_with_no_presence=True, + preserving_proto_field_name=preserving_proto_field_name, + ) + + +def protobuf_to_task_state_dict(message: TaskEvents) -> dict: + """ + Convert a TaskEvents to a dic repr of `TaskState` + """ + task_attempt = protobuf_message_to_dict( + message=message, + fields_to_decode=[ + "task_id", + "job_id", + "node_id", + "actor_id", + "parent_task_id", + "worker_id", + "placement_group_id", + "component_id", + ], + ) + + task_state = {} + task_info = task_attempt.get("task_info", {}) + state_updates = task_attempt.get("state_updates", {}) + profiling_data = task_attempt.get("profile_events", {}) + if profiling_data: + for event in profiling_data["events"]: + # End/start times are recorded in ns. We convert them to ms. + event["end_time"] = int(event["end_time"]) / 1e6 + event["start_time"] = int(event["start_time"]) / 1e6 + event["extra_data"] = json.loads(event["extra_data"]) + task_state["profiling_data"] = profiling_data + + # Convert those settable fields + mappings = [ + ( + task_info, + [ + "task_id", + "name", + "actor_id", + "type", + "func_or_class_name", + "language", + "required_resources", + "runtime_env_info", + "parent_task_id", + "placement_group_id", + "call_site", + "label_selector", + ], + ), + (task_attempt, ["task_id", "attempt_number", "job_id"]), + ( + state_updates, + [ + "node_id", + "worker_id", + "task_log_info", + "actor_repr_name", + "worker_pid", + "is_debugger_paused", + ], + ), + ] + for src, keys in mappings: + for key in keys: + task_state[key] = src.get(key) + + task_state["creation_time_ms"] = None + task_state["start_time_ms"] = None + task_state["end_time_ms"] = None + events = [] + + if "state_ts_ns" in state_updates: + state_ts_ns = state_updates["state_ts_ns"] + for state_name, state in TaskStatus.items(): + # state_ts_ns is Map[str, str] after protobuf MessageToDict + key = str(state) + if key in state_ts_ns: + # timestamp is recorded as nanosecond from the backend. + # We need to convert it to the second. + ts_ms = int(state_ts_ns[key]) // 1e6 + events.append( + { + "state": state_name, + "created_ms": ts_ms, + } + ) + if state == TaskStatus.PENDING_ARGS_AVAIL: + task_state["creation_time_ms"] = ts_ms + if state == TaskStatus.RUNNING: + task_state["start_time_ms"] = ts_ms + if state == TaskStatus.FINISHED or state == TaskStatus.FAILED: + task_state["end_time_ms"] = ts_ms + + task_state["events"] = events + if len(events) > 0: + latest_state = events[-1]["state"] + else: + latest_state = "NIL" + task_state["state"] = latest_state + + # Parse error info + if latest_state == "FAILED": + error_info = state_updates.get("error_info", None) + if error_info: + # We captured colored error message printed to console, e.g. + # "\x1b[31mTraceback (most recent call last):\x1b[0m", + # this is to remove the ANSI escape codes. + task_state["error_message"] = remove_ansi_escape_codes( + error_info.get("error_message", "") + ) + task_state["error_type"] = error_info.get("error_type", "") + + # Parse actor task name for actor with repr name. + if ( + state_updates.get("actor_repr_name") + and task_state["type"] == "ACTOR_TASK" + and task_state["name"] + == task_state["func_or_class_name"] # no name option provided. + ): + # If it's an actor task with no name override, and has repr name defined + # for the actor, we override the name. + method_name = task_state["name"].split(".")[-1] + actor_repr_task_name = f"{state_updates['actor_repr_name']}.{method_name}" + task_state["name"] = actor_repr_task_name + + return task_state + + +def remove_ansi_escape_codes(text: str) -> str: + """Remove ANSI escape codes from a string.""" + import re + + return re.sub(r"\x1b[^m]*m", "", text) + + +def dict_to_state(d: Dict, state_resource: StateResource) -> StateSchema: + """Convert a dict to a state schema. + + Args: + d: a dict to convert. + state_resource: the state resource to convert to. + + Returns: + A state schema. + """ + try: + return resource_to_schema(state_resource)(**d) + + except Exception as e: + raise RayStateApiException(f"Failed to convert {d} to StateSchema: {e}") from e diff --git a/lib/python3.12/site-packages/ray/util/state/exception.py b/lib/python3.12/site-packages/ray/util/state/exception.py new file mode 100644 index 0000000000000000000000000000000000000000..8d8a180c2c3233d56f26c32383d7d3b470e8b20b --- /dev/null +++ b/lib/python3.12/site-packages/ray/util/state/exception.py @@ -0,0 +1,18 @@ +"""Internal Error""" + + +class DataSourceUnavailable(Exception): + pass + + +"""User-facing Error""" + + +class RayStateApiException(Exception): + pass + + +class ServerUnavailable(RayStateApiException): + """Thrown when failing to connect to dashboard server""" + + pass diff --git a/lib/python3.12/site-packages/ray/util/state/state_cli.py b/lib/python3.12/site-packages/ray/util/state/state_cli.py new file mode 100644 index 0000000000000000000000000000000000000000..16ab4f34e2ea2b78a3ccae9716034f0ccbbdc3cf --- /dev/null +++ b/lib/python3.12/site-packages/ray/util/state/state_cli.py @@ -0,0 +1,1328 @@ +import json +import logging +from datetime import datetime +from enum import Enum, unique +from typing import Dict, List, Optional, Tuple + +import click +import yaml + +import ray._private.services as services +from ray._common.network_utils import parse_address +from ray._private.thirdparty.tabulate.tabulate import tabulate +from ray.util.annotations import PublicAPI +from ray.util.state import ( + StateApiClient, + get_log, + list_logs, + summarize_actors, + summarize_objects, + summarize_tasks, +) +from ray.util.state.common import ( + DEFAULT_LIMIT, + DEFAULT_LOG_LIMIT, + DEFAULT_RPC_TIMEOUT, + GetApiOptions, + ListApiOptions, + PredicateType, + StateResource, + StateSchema, + SupportedFilterType, + resource_to_schema, +) +from ray.util.state.exception import RayStateApiException + +logger = logging.getLogger(__name__) + + +@unique +class AvailableFormat(Enum): + DEFAULT = "default" + JSON = "json" + YAML = "yaml" + TABLE = "table" + + +def _parse_filter(filter: str) -> Tuple[str, PredicateType, SupportedFilterType]: + """Parse the filter string to a tuple of key, preciate, and value.""" + # The function assumes there's going to be no key that includes "="" or "!=". + # Since key is controlled by us, it should be trivial to keep the invariant. + predicate = None + # Tuple of [predicate_start, predicate_end). + predicate_index = None + + # Find the first predicate match. This logic works because we assume the + # key doesn't contain = or !=. + for i in range(len(filter)): + char = filter[i] + if char == "=": + predicate = "=" + predicate_index = (i, i + 1) + break + elif char == "!": + if len(filter) <= i + 1: + continue + + next_char = filter[i + 1] + if next_char == "=": + predicate = "!=" + predicate_index = (i, i + 2) + break + + if not predicate or not predicate_index: + raise ValueError( + f"The format of a given filter {filter} is invalid: " + "Cannot find the predicate. " + "Please provide key=val or key!=val format string." + ) + + key, predicate, value = ( + filter[: predicate_index[0]], + filter[predicate_index[0] : predicate_index[1]], + filter[predicate_index[1] :], + ) + assert predicate == "=" or predicate == "!=" + if len(key) == 0 or len(value) == 0: + raise ValueError( + f"The format of a given filter {filter} is invalid: " + f"Cannot identify key {key} or value, {value}. " + "Please provide key=val or key!=val format string." + ) + + return (key, predicate, value) + + +def _get_available_formats() -> List[str]: + """Return the available formats in a list of string""" + return [format_enum.value for format_enum in AvailableFormat] + + +def _get_available_resources( + excluded: Optional[List[StateResource]] = None, +) -> List[str]: + """Return the available resources in a list of string + + Args: + excluded: List of resources that should be excluded + """ + # All resource names use '_' rather than '-'. But users options have '-' + return [ + e.value.replace("_", "-") + for e in StateResource + if excluded is None or e not in excluded + ] + + +def get_table_output(state_data: List, schema: StateSchema, detail: bool) -> str: + """Display the table output. + + The table headers are ordered as the order defined in the dataclass of + `StateSchema`. For example, + + @dataclass + class A(StateSchema): + a: str + b: str + c: str + + will create headers + A B C + ----- + + Args: + state_data: A list of state data. + schema: The schema for the corresponding resource. + + Returns: + The table formatted string. + """ + time = datetime.now() + header = "=" * 8 + f" List: {time} " + "=" * 8 + headers = [] + table = [] + cols = schema.list_columns(detail=detail) + for data in state_data: + for key, val in data.items(): + if isinstance(val, dict): + data[key] = yaml.dump(val, indent=2) + keys = set(data.keys()) + headers = [] + for col in cols: + if col in keys: + headers.append(col.upper()) + table.append([data[header.lower()] for header in headers]) + return f""" +{header} +Stats: +------------------------------ +Total: {len(state_data)} + +Table: +------------------------------ +{tabulate(table, headers=headers, showindex=True, tablefmt="plain", floatfmt=".3f")} +""" + + +def output_with_format( + state_data: List[Dict], + *, + schema: Optional[StateSchema], + format: AvailableFormat = AvailableFormat.DEFAULT, + detail: bool = False, +) -> str: + # humanify all input state data + if schema: + state_data = [schema.humanify(state) for state in state_data] + if format == AvailableFormat.DEFAULT: + return get_table_output(state_data, schema, detail) + if format == AvailableFormat.YAML: + return yaml.dump( + state_data, + indent=4, + explicit_start=True, + # We want to keep the defined ordering of the states, thus sort_keys=False + sort_keys=False, + explicit_end=True, + ) + elif format == AvailableFormat.JSON: + return json.dumps(state_data) + elif format == AvailableFormat.TABLE: + return get_table_output(state_data, schema, detail) + else: + raise ValueError( + f"Unexpected format: {format}. " + f"Supported formatting: {_get_available_formats()}" + ) + + +def format_summary_output(state_data: Dict, *, resource: StateResource) -> str: + if len(state_data) == 0: + return "No resource in the cluster" + + # Parse the data. + cluster_data = state_data["cluster"] + summaries = cluster_data["summary"] + summary_by = cluster_data["summary_by"] + del cluster_data["summary_by"] + del cluster_data["summary"] + + cluster_info_table = yaml.dump(cluster_data, indent=2) + + # Create a table. + table = [] + headers = [] + for summary in summaries.values(): + # Convert dict to yaml for better formatting. + for key, val in summary.items(): + if isinstance(val, dict): + summary[key] = yaml.dump(val, indent=2) + + headers = sorted([key.upper() for key in summary.keys()]) + table.append([summary[header.lower()] for header in headers]) + + summary_table = tabulate( + table, headers=headers, showindex=True, tablefmt="plain", numalign="left" + ) + + time = datetime.now() + header = "=" * 8 + f" {resource.value.capitalize()} Summary: {time} " + "=" * 8 + return f""" +{header} +Stats: +------------------------------------ +{cluster_info_table} + +Table (group by {summary_by}): +------------------------------------ +{summary_table} +""" + + +def format_object_summary_output(state_data: Dict) -> str: + if len(state_data) == 0: + return "No resource in the cluster" + + # Parse the data. + cluster_data = state_data["cluster"] + summaries = cluster_data["summary"] + summary_by = cluster_data["summary_by"] + del cluster_data["summary_by"] + del cluster_data["summary"] + + cluster_info_table = yaml.dump(cluster_data, indent=2) + + # Create a table per callsite. + tables = [] + for callsite, summary in summaries.items(): + # Convert dict to yaml for better formatting. + for key, val in summary.items(): + if isinstance(val, dict): + summary[key] = yaml.dump(val, indent=2) + + table = [] + headers = sorted([key.upper() for key in summary.keys()]) + table.append([summary[header.lower()] for header in headers]) + table_for_callsite = tabulate( + table, headers=headers, showindex=True, numalign="left" + ) + + # Format callsite. | is a separator for ray callsite. + formatted_callsite = callsite.replace("|", "\n|") + tables.append(f"{formatted_callsite}\n{table_for_callsite}") + + time = datetime.now() + header = "=" * 8 + f" Object Summary: {time} " + "=" * 8 + table_string = "\n\n\n\n".join(tables) + return f""" +{header} +Stats: +------------------------------------ +{cluster_info_table} + +Table (group by {summary_by}) +------------------------------------ +{table_string} +""" + + +def format_get_api_output( + state_data: Optional[StateSchema], + id: str, + *, + schema: StateSchema, + format: AvailableFormat = AvailableFormat.YAML, +) -> str: + if not state_data or isinstance(state_data, list) and len(state_data) == 0: + return f"Resource with id={id} not found in the cluster." + + if not isinstance(state_data, list): + state_data = [state_data] + state_data = [state.asdict() for state in state_data] + + return output_with_format(state_data, schema=schema, format=format, detail=True) + + +def format_list_api_output( + state_data: List[StateSchema], + *, + schema: StateSchema, + format: AvailableFormat = AvailableFormat.DEFAULT, + detail: bool = False, +) -> str: + if len(state_data) == 0: + return "No resource in the cluster" + state_data = [state.asdict() for state in state_data] + return output_with_format(state_data, schema=schema, format=format, detail=detail) + + +def _should_explain(format: AvailableFormat) -> bool: + # If the format is json or yaml, it should not print stats because + # users don't want additional strings. + return format == AvailableFormat.DEFAULT or format == AvailableFormat.TABLE + + +""" +Common Options for State API commands +""" +timeout_option = click.option( + "--timeout", + default=DEFAULT_RPC_TIMEOUT, + help=f"Timeout in seconds for the API requests. Default is {DEFAULT_RPC_TIMEOUT}", +) +address_option = click.option( + "--address", + default=None, + help=( + "The address of Ray API server. If not provided, it will be configured " + "automatically from querying the GCS server." + ), +) + + +@click.command() +@click.argument( + "resource", + # NOTE(rickyyx): We are not allowing query job with id, and runtime envs + type=click.Choice( + _get_available_resources( + excluded=[StateResource.JOBS, StateResource.RUNTIME_ENVS] + ) + ), +) +@click.argument( + "id", + type=str, + required=False, +) +@address_option +@timeout_option +@PublicAPI(stability="stable") +def ray_get( + resource: str, + id: str, + address: Optional[str], + timeout: float, +): + """Get a state of a given resource by ID. + + We currently DO NOT support get by id for jobs and runtime-envs + + The output schema is defined at :ref:`State API Schema section. ` + + For example, the output schema of `ray get tasks ` is + :class:`~ray.util.state.common.TaskState`. + + Usage: + + Get an actor with actor id + + ``` + ray get actors + ``` + + Get a placement group information with + + ``` + ray get placement-groups + ``` + + The API queries one or more components from the cluster to obtain the data. + The returned state snapshot could be stale, and it is not guaranteed to return + the live data. + + Args: + resource: The type of the resource to query. + id: The id of the resource. + + Raises: + :class:`RayStateApiException ` + if the CLI is failed to query the data. + """ # noqa: E501 + if not id: + raise click.BadParameter( + f"Missing argument 'ID'. Do you mean 'ray list {resource}'?" + ) + + # All resource names use '_' rather than '-'. But users options have '-' + resource = StateResource(resource.replace("-", "_")) + + # Create the State API server and put it into context + logger.debug(f"Create StateApiClient to ray instance at: {address}...") + client = StateApiClient(address=address) + options = GetApiOptions(timeout=timeout) + + # If errors occur, exceptions will be thrown. + try: + data = client.get( + resource=resource, + id=id, + options=options, + _explain=_should_explain(AvailableFormat.YAML), + ) + except RayStateApiException as e: + raise click.UsageError(str(e)) + + # Print data to console. + print( + format_get_api_output( + state_data=data, + id=id, + schema=resource_to_schema(resource), + format=AvailableFormat.YAML, + ) + ) + + +@click.command() +@click.argument( + "resource", + type=click.Choice(_get_available_resources()), +) +@click.option( + "--format", default="default", type=click.Choice(_get_available_formats()) +) +@click.option( + "-f", + "--filter", + help=( + "A key, predicate, and value to filter the result. " + "E.g., --filter 'key=value' or --filter 'key!=value'. " + "You can specify multiple --filter options. In this case all predicates " + "are concatenated as AND. For example, --filter key=value --filter key2=value " + "means (key==val) AND (key2==val2), " + "String filter values are case-insensitive." + ), + multiple=True, +) +@click.option( + "--limit", + default=DEFAULT_LIMIT, + type=int, + help=("Maximum number of entries to return. 100 by default."), +) +@click.option( + "--detail", + help=( + "If the flag is set, the output will contain data in more details. " + "Note that the API could query more sources " + "to obtain information in a greater detail." + ), + is_flag=True, + default=False, +) +@timeout_option +@address_option +@PublicAPI(stability="stable") +def ray_list( + resource: str, + format: str, + filter: List[str], + limit: int, + detail: bool, + timeout: float, + address: str, +): + """List all states of a given resource. + + Normally, summary APIs are recommended before listing all resources. + + The output schema is defined at :ref:`State API Schema section. ` + + For example, the output schema of `ray list tasks` is + :class:`~ray.util.state.common.TaskState`. + + Usage: + + List all actor information from the cluster. + + ``` + ray list actors + ``` + + List 50 actors from the cluster. The sorting order cannot be controlled. + + ``` + ray list actors --limit 50 + ``` + + List 10 actors with state PENDING. + + ``` + ray list actors --limit 10 --filter "state=PENDING" + ``` + + List actors with yaml format. + + ``` + ray list actors --format yaml + ``` + + List actors with details. When --detail is specified, it might query + more data sources to obtain data in details. + + ``` + ray list actors --detail + ``` + + The API queries one or more components from the cluster to obtain the data. + The returned state snapshot could be stale, and it is not guaranteed to return + the live data. + + The API can return partial or missing output upon the following scenarios. + + - When the API queries more than 1 component, if some of them fail, + the API will return the partial result (with a suppressible warning). + - When the API returns too many entries, the API + will truncate the output. Currently, truncated data cannot be + selected by users. + + Args: + resource: The type of the resource to query. + + Raises: + :class:`RayStateApiException ` + if the CLI is failed to query the data. + + Changes: + - changed in version 2.7: --filter values are case-insensitive. + + """ # noqa: E501 + # All resource names use '_' rather than '-'. But users options have '-' + resource = StateResource(resource.replace("-", "_")) + format = AvailableFormat(format) + + # Create the State API server and put it into context + client = StateApiClient(address=address) + + filter = [_parse_filter(f) for f in filter] + + options = ListApiOptions( + limit=limit, + timeout=timeout, + filters=filter, + detail=detail, + ) + + # If errors occur, exceptions will be thrown. Empty data indicate successful query. + try: + data = client.list( + resource, + options=options, + raise_on_missing_output=False, + _explain=_should_explain(format), + ) + except RayStateApiException as e: + raise click.UsageError(str(e)) + + # If --detail is given, the default formatting is yaml. + if detail and format == AvailableFormat.DEFAULT: + format = AvailableFormat.YAML + + # Print data to console. + print( + format_list_api_output( + state_data=data, + schema=resource_to_schema(resource), + format=format, + detail=detail, + ) + ) + + +@click.group("summary") +@click.pass_context +@PublicAPI(stability="stable") +def summary_state_cli_group(ctx): + """Return the summarized information of a given resource.""" + pass + + +@summary_state_cli_group.command(name="tasks") +@timeout_option +@address_option +@click.pass_context +@PublicAPI(stability="stable") +def task_summary(ctx, timeout: float, address: str): + """Summarize the task state of the cluster. + + By default, the output contains the information grouped by + task function names. + + The output schema is + :class:`~ray.util.state.common.TaskSummaries`. + + Raises: + :class:`RayStateApiException ` + if the CLI is failed to query the data. + """ # noqa: E501 + print( + format_summary_output( + summarize_tasks( + address=address, + timeout=timeout, + raise_on_missing_output=False, + _explain=True, + ), + resource=StateResource.TASKS, + ) + ) + + +@summary_state_cli_group.command(name="actors") +@timeout_option +@address_option +@click.pass_context +@PublicAPI(stability="stable") +def actor_summary(ctx, timeout: float, address: str): + """Summarize the actor state of the cluster. + + By default, the output contains the information grouped by + actor class names. + + The output schema is + :class:`ray.util.state.common.ActorSummaries + `. + + Raises: + :class:`RayStateApiException ` + if the CLI is failed to query the data. + """ # noqa: E501 + print( + format_summary_output( + summarize_actors( + address=address, + timeout=timeout, + raise_on_missing_output=False, + _explain=True, + ), + resource=StateResource.ACTORS, + ) + ) + + +@summary_state_cli_group.command(name="objects") +@timeout_option +@address_option +@click.pass_context +@PublicAPI(stability="stable") +def object_summary(ctx, timeout: float, address: str): + """Summarize the object state of the cluster. + + The API is recommended when debugging memory leaks. + See :ref:`Debugging with Ray Memory ` for more details. + (Note that this command is almost equivalent to `ray memory`, but it returns + easier-to-understand output). + + By default, the output contains the information grouped by + object callsite. Note that the callsite is not collected and + all data will be aggregated as "disable" callsite if the env var + `RAY_record_ref_creation_sites` is not configured. To enable the + callsite collection, set the following environment variable when + starting Ray. + + Example: + + ``` + RAY_record_ref_creation_sites=1 ray start --head + ``` + + ``` + RAY_record_ref_creation_sites=1 ray_script.py + ``` + + The output schema is + :class:`ray.util.state.common.ObjectSummaries + `. + + Raises: + :class:`RayStateApiException ` + if the CLI is failed to query the data. + """ # noqa: E501 + print( + format_object_summary_output( + summarize_objects( + address=address, + timeout=timeout, + raise_on_missing_output=False, + _explain=True, + ), + ) + ) + + +log_follow_option = click.option( + "--follow", + "-f", + required=False, + type=bool, + is_flag=True, + help="Streams the log file as it is updated instead of just tailing.", +) + +log_tail_option = click.option( + "--tail", + required=False, + type=int, + default=DEFAULT_LOG_LIMIT, + help="Number of lines to tail from log. Use -1 to fetch the whole file.", +) + +log_interval_option = click.option( + "--interval", + required=False, + type=float, + default=None, + help="The interval in secs to print new logs when `--follow` is specified.", + hidden=True, +) + +log_timeout_option = click.option( + "--timeout", + default=DEFAULT_RPC_TIMEOUT, + help=( + "Timeout in seconds for the API requests. " + f"Default is {DEFAULT_RPC_TIMEOUT}. If --follow is specified, " + "this option will be ignored." + ), +) + +log_node_ip_option = click.option( + "-ip", + "--node-ip", + required=False, + type=str, + default=None, + help="Filters the logs by this ip address", +) + +log_node_id_option = click.option( + "--node-id", + "-id", + required=False, + type=str, + default=None, + help="Filters the logs by this NodeID", +) + +log_suffix_option = click.option( + "--err", + is_flag=True, + default=False, + help=( + "If supplied, querying stderr files for workers/actors, " + "else defaults to stdout files." + ), +) + +log_encoding_option = click.option( + "--encoding", + required=False, + default="utf-8", + help=( + "The encoding use to decode the log file. Accepts any encoding " + "supported by Python's `codecs` module. Defaults to utf-8." + ), +) + +log_encoding_errors_option = click.option( + "--encoding-errors", + required=False, + default="strict", + help=( + "The error handling scheme to use for decoding errors. " + "Accepts any error handling scheme supported by Python's `codecs`" + "module. Defaults to strict." + ), +) + + +def _get_head_node_ip(address: Optional[str] = None): + """Get the head node ip from the ray address if possible + + Args: + address: ray cluster address, e.g. "auto", "localhost:6379" + + Raises: + click.UsageError: if node ip could not be resolved + """ + try: + address = services.canonicalize_bootstrap_address_or_die(address) + return parse_address(address)[0] + except (ConnectionError, ValueError) as e: + # Hide all the stack trace + raise click.UsageError(str(e)) + + +def _print_log( + address: Optional[str] = None, + node_id: Optional[str] = None, + node_ip: Optional[str] = None, + filename: Optional[str] = None, + actor_id: Optional[str] = None, + pid: Optional[int] = None, + follow: bool = False, + tail: int = DEFAULT_LOG_LIMIT, + timeout: int = DEFAULT_RPC_TIMEOUT, + interval: Optional[float] = None, + suffix: str = "out", + encoding: str = "utf-8", + encoding_errors: str = "strict", + task_id: Optional[str] = None, + attempt_number: int = 0, + submission_id: Optional[str] = None, +): + """Wrapper around `get_log()` that prints the preamble and the log lines""" + if tail > 0: + print( + f"--- Log has been truncated to last {tail} lines." + " Use `--tail` flag to toggle. Set to -1 for getting the entire file. ---\n" + ) + + if node_id is None and node_ip is None: + # Auto detect node ip from the ray address when address neither is given + node_ip = _get_head_node_ip(address) + + for chunk in get_log( + address=address, + node_id=node_id, + node_ip=node_ip, + filename=filename, + actor_id=actor_id, + tail=tail, + pid=pid, + follow=follow, + _interval=interval, + timeout=timeout, + suffix=suffix, + encoding=encoding, + errors=encoding_errors, + task_id=task_id, + attempt_number=attempt_number, + submission_id=submission_id, + ): + print(chunk, end="", flush=True) + + +LOG_CLI_HELP_MSG = """ +Get logs based on filename (cluster) or resource identifiers (actor) + +Example: + + Get all the log files available on a node (ray address could be + obtained from `ray start --head` or `ray.init()`). + + ``` + ray logs cluster + ``` + + [ray logs cluster] Print the last 500 lines of raylet.out on a head node. + + ``` + ray logs cluster raylet.out --tail 500 + ``` + + Or simply, using `ray logs` as an alias for `ray logs cluster`: + + ``` + ray logs raylet.out --tail 500 + ``` + + Print the last 500 lines of raylet.out on a worker node id A. + + ``` + ray logs raylet.out --tail 500 —-node-id A + ``` + + [ray logs actor] Follow the log file with an actor id ABC. + + ``` + ray logs actor --id ABC --follow + ``` + + [ray logs task] Get the std err generated by a task. + + Note: If a task is from a concurrent actor (i.e. an async actor or + a threaded actor), the log of the tasks are expected to be interleaved. + Please use `ray logs actor --id ` for the entire actor log. + + ``` + ray logs task --id --err + ``` +""" + + +class LogCommandGroup(click.Group): + def resolve_command(self, ctx, args): + """Try resolve the command line args assuming users omitted the subcommand. + + This overrides the default `resolve_command` for the parent class. + This will allow command alias of `ray ` to `ray cluster `. + """ + ctx.resilient_parsing = True + res = super().resolve_command(ctx, args) + cmd_name, cmd, parsed_args = res + if cmd is None: + # It could have been `ray logs ...`, forward to `ray logs cluster ...` + return super().resolve_command(ctx, ["cluster"] + args) + return cmd_name, cmd, parsed_args + + +logs_state_cli_group = LogCommandGroup(help=LOG_CLI_HELP_MSG) + + +@logs_state_cli_group.command(name="cluster") +@click.argument( + "glob_filter", + required=False, + default="*", +) +@address_option +@log_node_id_option +@log_node_ip_option +@log_follow_option +@log_tail_option +@log_interval_option +@log_timeout_option +@log_encoding_option +@log_encoding_errors_option +@click.pass_context +@PublicAPI(stability="stable") +def log_cluster( + ctx, + glob_filter: str, + address: Optional[str], + node_id: Optional[str], + node_ip: Optional[str], + follow: bool, + tail: int, + interval: float, + timeout: int, + encoding: str, + encoding_errors: str, +): + """Get/List logs that matches the GLOB_FILTER in the cluster. + By default, it prints a list of log files that match the filter. + By default, it prints the head node logs. + If there's only 1 match, it will print the log file. + + Example: + + Print the last 500 lines of raylet.out on a head node. + + ``` + ray logs [cluster] raylet.out --tail 500 + ``` + + Print the last 500 lines of raylet.out on a worker node id A. + + ``` + ray logs [cluster] raylet.out --tail 500 —-node-id A + ``` + + Download the gcs_server.txt file to the local machine. + + ``` + ray logs [cluster] gcs_server.out --tail -1 > gcs_server.txt + ``` + + Follow the log files from the last 100 lines. + + ``` + ray logs [cluster] raylet.out --tail 100 -f + ``` + + Raises: + :class:`RayStateApiException ` if the CLI + is failed to query the data. + """ # noqa: E501 + + if node_id is None and node_ip is None: + node_ip = _get_head_node_ip(address) + + logs = list_logs( + address=address, + node_id=node_id, + node_ip=node_ip, + glob_filter=glob_filter, + timeout=timeout, + ) + + log_files_found = [] + for _, log_files in logs.items(): + for log_file in log_files: + log_files_found.append(log_file) + + if len(log_files_found) != 1: + # Print the list of log files found if no unique log found + if node_id: + print(f"Node ID: {node_id}") + elif node_ip: + print(f"Node IP: {node_ip}") + print(output_with_format(logs, schema=None, format=AvailableFormat.YAML)) + return + + # If there's only 1 file, that means there's a unique match. + filename = log_files_found[0] + + _print_log( + address=address, + node_id=node_id, + node_ip=node_ip, + filename=filename, + tail=tail, + follow=follow, + interval=interval, + timeout=timeout, + encoding=encoding, + encoding_errors=encoding_errors, + ) + + +@logs_state_cli_group.command(name="actor") +@click.option( + "--id", + "-a", + required=False, + type=str, + default=None, + help="Retrieves the logs corresponding to this ActorID.", +) +@click.option( + "--pid", + "-pid", + required=False, + type=str, + default=None, + help="Retrieves the logs from the actor with this pid.", +) +@address_option +@log_node_id_option +@log_node_ip_option +@log_follow_option +@log_tail_option +@log_interval_option +@log_timeout_option +@log_suffix_option +@click.pass_context +@PublicAPI(stability="stable") +def log_actor( + ctx, + id: Optional[str], + pid: Optional[str], + address: Optional[str], + node_id: Optional[str], + node_ip: Optional[str], + follow: bool, + tail: int, + interval: float, + timeout: int, + err: bool, +): + """Get/List logs associated with an actor. + + Example: + + Follow the log file with an actor id ABCDEFG. + + ``` + ray logs actor --id ABCDEFG --follow + ``` + + Get the actor log from pid 123, ip x.x.x.x + Note that this goes well with the driver log of Ray which prints + (ip=x.x.x.x, pid=123, class_name) logs. + + ``` + ray logs actor --pid=123 —ip=x.x.x.x + ``` + + Get the actor err log file. + + ``` + ray logs actor --id ABCDEFG --err + ``` + + Raises: + :class:`RayStateApiException ` + if the CLI is failed to query the data. + MissingParameter: if inputs are missing. + """ # noqa: E501 + + if pid is None and id is None: + raise click.MissingParameter( + message="At least one of `--pid` and `--id` has to be set", + param_type="option", + ) + + _print_log( + address=address, + node_id=node_id, + node_ip=node_ip, + pid=pid, + actor_id=id, + tail=tail, + follow=follow, + interval=interval, + timeout=timeout, + suffix="err" if err else "out", + ) + + +@logs_state_cli_group.command(name="worker") +@click.option( + "--pid", + "-pid", + # The only identifier supported for now, TODO(rickyx): add worker id support + required=True, + type=str, + help="Retrieves the logs from the worker with this pid.", +) +@address_option +@log_node_id_option +@log_node_ip_option +@log_follow_option +@log_tail_option +@log_interval_option +@log_timeout_option +@log_suffix_option +@click.pass_context +@PublicAPI(stability="stable") +def log_worker( + ctx, + pid: Optional[str], + address: Optional[str], + node_id: Optional[str], + node_ip: Optional[str], + follow: bool, + tail: int, + interval: float, + timeout: int, + err: bool, +): + """Get logs associated with a worker process. + + Example: + + Follow the log file from a worker process with pid=123 + + ``` + ray logs worker --pid 123 --follow + ``` + + Get the stderr logs from a worker process. + + ``` + ray logs worker --pid 123 --err + ``` + + Raises: + :class:`RayStateApiException ` + if the CLI is failed to query the data. + MissingParameter: if inputs are missing. + """ # noqa: E501 + + _print_log( + address=address, + node_id=node_id, + node_ip=node_ip, + pid=pid, + tail=tail, + follow=follow, + interval=interval, + timeout=timeout, + suffix="err" if err else "out", + ) + + +@logs_state_cli_group.command(name="job") +@click.option( + "--id", + "submission_id", + required=True, + type=str, + help=( + "Retrieves the logs from a submission job with submission id," + "i.e. raysubmit_XXX" + ), +) +@address_option +@log_follow_option +@log_tail_option +@log_interval_option +@log_timeout_option +@click.pass_context +@PublicAPI(stability="stable") +def log_job( + ctx, + submission_id: Optional[str], + address: Optional[str], + follow: bool, + tail: int, + interval: float, + timeout: int, +): + """Get logs associated with a submission job. + + Example: + + Follow the log file from a submission job with submission id raysumbit_xxx. + + ``` + ray logs job --id raysubmit_xxx + ``` + + Follow the submission job log. + + ``` + ray logs jobs --id raysubmit_xxx --follow + + ``` + + Raises: + :class:`RayStateApiException ` + if the CLI is failed to query the data. + MissingParameter: if inputs are missing. + """ # noqa: E501 + + _print_log( + address=address, + tail=tail, + follow=follow, + interval=interval, + timeout=timeout, + submission_id=submission_id, + ) + + +@logs_state_cli_group.command(name="task") +@click.option( + "--id", + "task_id", + required=True, + type=str, + help="Retrieves the logs from the task with this task id.", +) +@click.option( + "--attempt-number", + "-a", + required=False, + type=int, + default=0, + help="Retrieves the logs from the attempt, default to 0", +) +@address_option +@log_follow_option +@log_interval_option +@log_tail_option +@log_timeout_option +@log_suffix_option +@click.pass_context +@PublicAPI(stability="stable") +def log_task( + ctx, + task_id: Optional[str], + attempt_number: int, + address: Optional[str], + follow: bool, + interval: float, + tail: int, + timeout: int, + err: bool, +): + """Get logs associated with a task. + + Example: + + Follow the log file from a task with task id = ABCDEFG + + ``` + ray logs tasks --id ABCDEFG --follow + ``` + + Get the log from a retry attempt 1 from a task. + + ``` + ray logs tasks --id ABCDEFG -a 1 + ``` + + Note: If a task is from a concurrent actor (i.e. an async actor or + a threaded actor), the log of the tasks are expected to be interleaved. + Please use `ray logs actor --id ` for the entire actor log. + + Raises: + :class:`RayStateApiException ` + if the CLI is failed to query the data. + MissingParameter: if inputs are missing. + """ # noqa: E501 + + _print_log( + address=address, + task_id=task_id, + attempt_number=attempt_number, + follow=follow, + tail=tail, + interval=interval, + timeout=timeout, + suffix="err" if err else "out", + ) diff --git a/lib/python3.12/site-packages/ray/util/state/state_manager.py b/lib/python3.12/site-packages/ray/util/state/state_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..d693eea0a78062b12f9166dd48a28ce4e9be0570 --- /dev/null +++ b/lib/python3.12/site-packages/ray/util/state/state_manager.py @@ -0,0 +1,491 @@ +import dataclasses +import inspect +import json +import logging +from functools import wraps +from typing import List, Optional, Tuple + +import aiohttp +import grpc +from grpc.aio._call import UnaryStreamCall + +import ray.dashboard.consts as dashboard_consts +import ray.dashboard.modules.log.log_consts as log_consts +from ray._common.network_utils import build_address +from ray._common.utils import hex_to_binary +from ray._private import ray_constants +from ray._raylet import ActorID, GcsClient, JobID, NodeID, TaskID +from ray.core.generated import gcs_service_pb2_grpc +from ray.core.generated.gcs_pb2 import ActorTableData, GcsNodeInfo +from ray.core.generated.gcs_service_pb2 import ( + FilterPredicate, + GetAllActorInfoReply, + GetAllActorInfoRequest, + GetAllNodeInfoReply, + GetAllNodeInfoRequest, + GetAllPlacementGroupReply, + GetAllPlacementGroupRequest, + GetAllWorkerInfoReply, + GetAllWorkerInfoRequest, + GetTaskEventsReply, + GetTaskEventsRequest, +) +from ray.core.generated.node_manager_pb2 import ( + GetObjectsInfoReply, + GetObjectsInfoRequest, +) +from ray.core.generated.node_manager_pb2_grpc import NodeManagerServiceStub +from ray.core.generated.reporter_pb2 import ( + ListLogsReply, + ListLogsRequest, + StreamLogRequest, +) +from ray.core.generated.reporter_pb2_grpc import LogServiceStub +from ray.core.generated.runtime_env_agent_pb2 import ( + GetRuntimeEnvsInfoReply, + GetRuntimeEnvsInfoRequest, +) +from ray.dashboard.modules.job.common import JobInfoStorageClient +from ray.dashboard.modules.job.pydantic_models import JobDetails, JobType +from ray.dashboard.modules.job.utils import get_driver_jobs +from ray.util.state.common import ( + RAY_MAX_LIMIT_FROM_DATA_SOURCE, + PredicateType, + SupportedFilterType, +) +from ray.util.state.exception import DataSourceUnavailable + +logger = logging.getLogger(__name__) + +_STATE_MANAGER_GRPC_OPTIONS = [ + *ray_constants.GLOBAL_GRPC_OPTIONS, + ("grpc.max_send_message_length", ray_constants.GRPC_CPP_MAX_MESSAGE_SIZE), + ("grpc.max_receive_message_length", ray_constants.GRPC_CPP_MAX_MESSAGE_SIZE), +] + + +def handle_grpc_network_errors(func): + """Decorator to add a network handling logic. + + It is a helper method for `StateDataSourceClient`. + The method can only be used for async methods. + """ + assert inspect.iscoroutinefunction(func) + + @wraps(func) + async def api_with_network_error_handler(*args, **kwargs): + """Apply the network error handling logic to each APIs, + such as retry or exception policies. + + Returns: + If RPC succeeds, it returns what the original function returns. + If RPC fails, it raises exceptions. + + Raises: + DataSourceUnavailable: if the source is unavailable because it is down + or there's a slow network issue causing timeout. + + Exception: Otherwise, the raw network exceptions (e.g., gRPC) will be + raised. + """ + try: + return await func(*args, **kwargs) + except grpc.aio.AioRpcError as e: + if ( + e.code() == grpc.StatusCode.DEADLINE_EXCEEDED + or e.code() == grpc.StatusCode.UNAVAILABLE + ): + raise DataSourceUnavailable( + "Failed to query the data source. " + "It is either there's a network issue, or the source is down." + ) from e + else: + logger.exception(e) + raise e + + return api_with_network_error_handler + + +class StateDataSourceClient: + """The client to query states from various data sources such as Raylet, GCS, Agents. + + Note that it doesn't directly query core workers. They are proxied through raylets. + + The module is not in charge of service discovery. The caller is responsible for + finding services and register stubs through `register*` APIs. + + Non `register*` APIs + - Return the protobuf directly if it succeeds to query the source. + - Raises an exception if there's any network issue. + - throw a ValueError if it cannot find the source. + """ + + def __init__(self, gcs_channel: grpc.aio.Channel, gcs_client: GcsClient): + self.register_gcs_client(gcs_channel) + self._job_client = JobInfoStorageClient(gcs_client) + self._gcs_client = gcs_client + self._client_session = aiohttp.ClientSession() + + def register_gcs_client(self, gcs_channel: grpc.aio.Channel): + self._gcs_actor_info_stub = gcs_service_pb2_grpc.ActorInfoGcsServiceStub( + gcs_channel + ) + self._gcs_pg_info_stub = gcs_service_pb2_grpc.PlacementGroupInfoGcsServiceStub( + gcs_channel + ) + self._gcs_node_info_stub = gcs_service_pb2_grpc.NodeInfoGcsServiceStub( + gcs_channel + ) + self._gcs_worker_info_stub = gcs_service_pb2_grpc.WorkerInfoGcsServiceStub( + gcs_channel + ) + self._gcs_task_info_stub = gcs_service_pb2_grpc.TaskInfoGcsServiceStub( + gcs_channel + ) + + def get_raylet_stub(self, ip: str, port: int): + from ray._private.grpc_utils import init_grpc_channel + + options = _STATE_MANAGER_GRPC_OPTIONS + channel = init_grpc_channel(build_address(ip, port), options, asynchronous=True) + return NodeManagerServiceStub(channel) + + async def get_log_service_stub(self, node_id: NodeID) -> LogServiceStub: + """Returns None if the agent on the node is not registered in Internal KV.""" + from ray._private.grpc_utils import init_grpc_channel + + agent_addr = await self._gcs_client.async_internal_kv_get( + f"{dashboard_consts.DASHBOARD_AGENT_ADDR_NODE_ID_PREFIX}{node_id.hex()}".encode(), + namespace=ray_constants.KV_NAMESPACE_DASHBOARD, + timeout=dashboard_consts.GCS_RPC_TIMEOUT_SECONDS, + ) + if not agent_addr: + return None + ip, http_port, grpc_port = json.loads(agent_addr) + options = ray_constants.GLOBAL_GRPC_OPTIONS + channel = init_grpc_channel( + build_address(ip, grpc_port), options=options, asynchronous=True + ) + return LogServiceStub(channel) + + async def ip_to_node_id(self, ip: Optional[str]) -> Optional[str]: + """Return the node id in hex that corresponds to the given ip. + + Args: + ip: The ip address. + + Returns: + None if the corresponding id doesn't exist. + Node id otherwise. If None node_ip is given, + it will also return None. + """ + if not ip: + return None + # Uses the dashboard agent keys to find ip -> id mapping. + agent_addr = await self._gcs_client.async_internal_kv_get( + f"{dashboard_consts.DASHBOARD_AGENT_ADDR_IP_PREFIX}{ip}".encode(), + namespace=ray_constants.KV_NAMESPACE_DASHBOARD, + timeout=dashboard_consts.GCS_RPC_TIMEOUT_SECONDS, + ) + if not agent_addr: + return None + node_id, http_port, grpc_port = json.loads(agent_addr) + return node_id + + @handle_grpc_network_errors + async def get_all_actor_info( + self, + timeout: int = None, + limit: int = RAY_MAX_LIMIT_FROM_DATA_SOURCE, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + ) -> Optional[GetAllActorInfoReply]: + if filters is None: + filters = [] + + req_filters = GetAllActorInfoRequest.Filters() + for filter in filters: + key, predicate, value = filter + if predicate != "=": + # We only support EQUAL predicate for source side filtering. + continue + if key == "actor_id": + req_filters.actor_id = ActorID(hex_to_binary(value)).binary() + elif key == "state": + # Convert to uppercase. + value = value.upper() + if value not in ActorTableData.ActorState.keys(): + raise ValueError(f"Invalid actor state for filtering: {value}") + req_filters.state = ActorTableData.ActorState.Value(value) + elif key == "job_id": + req_filters.job_id = JobID(hex_to_binary(value)).binary() + + request = GetAllActorInfoRequest(limit=limit, filters=req_filters) + reply = await self._gcs_actor_info_stub.GetAllActorInfo( + request, timeout=timeout + ) + return reply + + @handle_grpc_network_errors + async def get_all_task_info( + self, + timeout: int = None, + limit: int = RAY_MAX_LIMIT_FROM_DATA_SOURCE, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + exclude_driver: bool = False, + ) -> Optional[GetTaskEventsReply]: + + if filters is None: + filters = [] + + req_filters = GetTaskEventsRequest.Filters() + for filter in filters: + key, predicate, value = filter + filter_predicate = None + if predicate == "=": + filter_predicate = FilterPredicate.EQUAL + elif predicate == "!=": + filter_predicate = FilterPredicate.NOT_EQUAL + else: + # We only support EQUAL and NOT_EQUAL predicate for source side + # filtering. If invalid predicates were specified, it should already be + # raised when the filters arguments are parsed + assert False, "Invalid predicate: " + predicate + + if key == "actor_id": + actor_filter = GetTaskEventsRequest.Filters.ActorIdFilter() + actor_filter.actor_id = ActorID(hex_to_binary(value)).binary() + actor_filter.predicate = filter_predicate + req_filters.actor_filters.append(actor_filter) + + elif key == "job_id": + job_filter = GetTaskEventsRequest.Filters.JobIdFilter() + job_filter.job_id = JobID(hex_to_binary(value)).binary() + job_filter.predicate = filter_predicate + req_filters.job_filters.append(job_filter) + + elif key == "task_id": + task_filter = GetTaskEventsRequest.Filters.TaskIdFilter() + task_filter.task_id = TaskID(hex_to_binary(value)).binary() + task_filter.predicate = filter_predicate + req_filters.task_filters.append(task_filter) + + elif key == "name": + task_name_filter = GetTaskEventsRequest.Filters.TaskNameFilter() + task_name_filter.task_name = value + task_name_filter.predicate = filter_predicate + req_filters.task_name_filters.append(task_name_filter) + + elif key == "state": + state_filter = GetTaskEventsRequest.Filters.StateFilter() + state_filter.state = value + state_filter.predicate = filter_predicate + req_filters.state_filters.append(state_filter) + + else: + continue + + req_filters.exclude_driver = exclude_driver + + request = GetTaskEventsRequest(limit=limit, filters=req_filters) + reply = await self._gcs_task_info_stub.GetTaskEvents(request, timeout=timeout) + return reply + + @handle_grpc_network_errors + async def get_all_placement_group_info( + self, timeout: int = None, limit: int = RAY_MAX_LIMIT_FROM_DATA_SOURCE + ) -> Optional[GetAllPlacementGroupReply]: + + request = GetAllPlacementGroupRequest(limit=limit) + reply = await self._gcs_pg_info_stub.GetAllPlacementGroup( + request, timeout=timeout + ) + return reply + + @handle_grpc_network_errors + async def get_all_node_info( + self, + timeout: int = None, + limit: int = RAY_MAX_LIMIT_FROM_DATA_SOURCE, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + ) -> Optional[GetAllNodeInfoReply]: + # TODO(ryw): move this to GcsClient.async_get_all_node_info, i.e. + # InnerGcsClient.async_get_all_node_info + + if filters is None: + filters = [] + + node_selectors = [] + state_filter = None + for filter in filters: + key, predicate, value = filter + if predicate != "=": + # We only support EQUAL predicate for source side filtering. + continue + + if key == "node_id": + node_selector = GetAllNodeInfoRequest.NodeSelector() + node_selector.node_id = NodeID(hex_to_binary(value)).binary() + node_selectors.append(node_selector) + elif key == "state": + value = value.upper() + if value not in GcsNodeInfo.GcsNodeState.keys(): + raise ValueError(f"Invalid node state for filtering: {value}") + state_filter = GcsNodeInfo.GcsNodeState.Value(value) + elif key == "node_name": + node_selector = GetAllNodeInfoRequest.NodeSelector() + node_selector.node_name = value + node_selectors.append(node_selector) + else: + continue + + request = GetAllNodeInfoRequest( + limit=limit, node_selectors=node_selectors, state_filter=state_filter + ) + reply = await self._gcs_node_info_stub.GetAllNodeInfo(request, timeout=timeout) + return reply + + @handle_grpc_network_errors + async def get_all_worker_info( + self, + timeout: int = None, + limit: int = RAY_MAX_LIMIT_FROM_DATA_SOURCE, + filters: Optional[List[Tuple[str, PredicateType, SupportedFilterType]]] = None, + ) -> Optional[GetAllWorkerInfoReply]: + + if filters is None: + filters = [] + + req_filters = GetAllWorkerInfoRequest.Filters() + for filter in filters: + key, predicate, value = filter + # Special treatments for the Ray Debugger. + if ( + key == "num_paused_threads" + and predicate in ("!=", ">") + and value == "0" + ): + req_filters.exist_paused_threads = True + continue + if key == "is_alive" and predicate == "=" and value == "True": + req_filters.is_alive = True + continue + else: + continue + + request = GetAllWorkerInfoRequest(limit=limit, filters=req_filters) + reply = await self._gcs_worker_info_stub.GetAllWorkerInfo( + request, timeout=timeout + ) + return reply + + # TODO(rickyx): + # This is currently mirroring dashboard/modules/job/job_head.py::list_jobs + # We should eventually unify the logic. + async def get_job_info(self, timeout: int = None) -> List[JobDetails]: + # Cannot use @handle_grpc_network_errors because async def is not supported yet. + + driver_jobs, submission_job_drivers = await get_driver_jobs( + self._gcs_client, timeout=timeout + ) + submission_jobs = await self._job_client.get_all_jobs(timeout=timeout) + submission_jobs = [ + JobDetails( + **dataclasses.asdict(job), + submission_id=submission_id, + job_id=submission_job_drivers.get(submission_id).id + if submission_id in submission_job_drivers + else None, + driver_info=submission_job_drivers.get(submission_id), + type=JobType.SUBMISSION, + ) + for submission_id, job in submission_jobs.items() + ] + + return list(driver_jobs.values()) + submission_jobs + + @handle_grpc_network_errors + async def get_object_info( + self, + node_manager_ip: str, + node_manager_port: int, + timeout: int = None, + limit: int = RAY_MAX_LIMIT_FROM_DATA_SOURCE, + ) -> Optional[GetObjectsInfoReply]: + stub = self.get_raylet_stub(node_manager_ip, node_manager_port) + + reply = await stub.GetObjectsInfo( + GetObjectsInfoRequest(limit=limit), + timeout=timeout, + ) + return reply + + async def get_runtime_envs_info( + self, + node_ip: str, + runtime_env_agent_port: int, + timeout: int = None, + limit: int = RAY_MAX_LIMIT_FROM_DATA_SOURCE, + ) -> Optional[GetRuntimeEnvsInfoReply]: + if not node_ip or not runtime_env_agent_port: + raise ValueError( + f"Expected non empty node ip and runtime env agent port, got {node_ip} and {runtime_env_agent_port}." + ) + timeout = aiohttp.ClientTimeout(total=timeout) + url = f"http://{build_address(node_ip, runtime_env_agent_port)}/get_runtime_envs_info" + request = GetRuntimeEnvsInfoRequest(limit=limit) + data = request.SerializeToString() + async with self._client_session.post(url, data=data, timeout=timeout) as resp: + if resp.status >= 200 and resp.status < 300: + response_data = await resp.read() + reply = GetRuntimeEnvsInfoReply() + reply.ParseFromString(response_data) + return reply + else: + raise DataSourceUnavailable( + "Failed to query the runtime env agent for get_runtime_envs_info. " + "Either there's a network issue, or the source is down. " + f"Response is {resp.status}, reason {resp.reason}" + ) + + @handle_grpc_network_errors + async def list_logs( + self, node_id: str, glob_filter: str, timeout: int = None + ) -> ListLogsReply: + stub = await self.get_log_service_stub(NodeID.from_hex(node_id)) + if not stub: + raise ValueError(f"Agent for node id: {node_id} doesn't exist.") + return await stub.ListLogs( + ListLogsRequest(glob_filter=glob_filter), timeout=timeout + ) + + @handle_grpc_network_errors + async def stream_log( + self, + node_id: str, + log_file_name: str, + keep_alive: bool, + lines: int, + interval: Optional[float], + timeout: int, + start_offset: Optional[int] = None, + end_offset: Optional[int] = None, + ) -> UnaryStreamCall: + stub = await self.get_log_service_stub(NodeID.from_hex(node_id)) + if not stub: + raise ValueError(f"Agent for node id: {node_id} doesn't exist.") + + stream = stub.StreamLog( + StreamLogRequest( + keep_alive=keep_alive, + log_file_name=log_file_name, + lines=lines, + interval=interval, + start_offset=start_offset, + end_offset=end_offset, + ), + timeout=timeout, + ) + metadata = await stream.initial_metadata() + if metadata.get(log_consts.LOG_GRPC_ERROR) is not None: + raise ValueError(metadata.get(log_consts.LOG_GRPC_ERROR)) + return stream diff --git a/lib/python3.12/site-packages/ray/util/state/util.py b/lib/python3.12/site-packages/ray/util/state/util.py new file mode 100644 index 0000000000000000000000000000000000000000..77894289d9fb2bc2574d0b27b3eb89bac3efbb3b --- /dev/null +++ b/lib/python3.12/site-packages/ray/util/state/util.py @@ -0,0 +1,62 @@ +from typing import Optional, Union + + +def convert_string_to_type( + val: Optional[Union[str, int, float, bool]], convert_type: Union[int, float, bool] +) -> Union[int, float, bool]: + """Convert the given value to a convert type. + + If the given val is None, it will just return None without the conversion. + + It supports, + str -> int/float/bool + int -> int + bool -> bool + float -> float + """ + if val is None: + return None + elif type(val) is convert_type: + return val + elif convert_type is int: + try: + val = int(val) + except ValueError: + raise ValueError( + f"Failed to convert a value {val} of type {type(val)} to {convert_type}" + ) + elif convert_type is float: + try: + val = float(val) + except ValueError: + raise ValueError( + f"Failed to convert a value {val} of type {type(val)} to {convert_type}" + ) + elif convert_type is bool: + # Without this, "False" will become True. + if val == "False" or val == "false" or val == "0": + val = False + elif val == "True" or val == "true" or val == "1": + val = True + else: + raise ValueError( + f"Failed to convert a value {val} of type {type(val)} to {convert_type}" + ) + else: + assert False, f"Unsupported convert type {convert_type}" + return val + + +def record_deprecated_state_api_import(): + import warnings + + from ray._common.usage.usage_lib import TagKey, record_extra_usage_tag + + warnings.warn( + "Ray state API is no longer experimental. Please import from `ray.util.state`. " + "instead. Importing from `ray.experimental` will be deprecated in " + "future releases. ", + DeprecationWarning, + ) + + record_extra_usage_tag(TagKey.EXPERIMENTAL_STATE_API_IMPORT, "1") diff --git a/lib/python3.12/site-packages/ray/util/tracing/__init__.py b/lib/python3.12/site-packages/ray/util/tracing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/util/tracing/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/tracing/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31ca8c8039b1c301b8e2d0237d69ea1a50110a61 Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/tracing/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/tracing/__pycache__/setup_local_tmp_tracing.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/tracing/__pycache__/setup_local_tmp_tracing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c6707237f2b1b1354167ff159f9f9ff2747acb3 Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/tracing/__pycache__/setup_local_tmp_tracing.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/tracing/__pycache__/setup_tempo_tracing.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/tracing/__pycache__/setup_tempo_tracing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38fbb62087fe33257604c4bb471ce4810b727534 Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/tracing/__pycache__/setup_tempo_tracing.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/tracing/__pycache__/tracing_helper.cpython-312.pyc b/lib/python3.12/site-packages/ray/util/tracing/__pycache__/tracing_helper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..066b95b929e5e53db9f84ff8ad71f7085b07be42 Binary files /dev/null and b/lib/python3.12/site-packages/ray/util/tracing/__pycache__/tracing_helper.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/util/tracing/setup_local_tmp_tracing.py b/lib/python3.12/site-packages/ray/util/tracing/setup_local_tmp_tracing.py new file mode 100644 index 0000000000000000000000000000000000000000..94523ea8bff7fd20d710add053580a6f2db50e9b --- /dev/null +++ b/lib/python3.12/site-packages/ray/util/tracing/setup_local_tmp_tracing.py @@ -0,0 +1,26 @@ +import os + +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleSpanProcessor, +) + +spans_dir = "/tmp/spans/" + + +def setup_tracing() -> None: + """Stand-in for a user-provided `setup_tracing` hook.""" + os.makedirs("/tmp/spans", exist_ok=True) + # Sets the tracer_provider. This is only allowed once per execution + # context and will log a warning if attempted multiple times. + trace.set_tracer_provider(TracerProvider()) + trace.get_tracer_provider().add_span_processor( + SimpleSpanProcessor( + ConsoleSpanExporter( + out=open(f"{spans_dir}{os.getpid()}.txt", "w"), + formatter=lambda span: span.to_json(indent=None) + os.linesep, + ) + ) + ) diff --git a/lib/python3.12/site-packages/ray/util/tracing/setup_tempo_tracing.py b/lib/python3.12/site-packages/ray/util/tracing/setup_tempo_tracing.py new file mode 100644 index 0000000000000000000000000000000000000000..e2bb3102b09d563fb2427fb961f85d39e6a185b2 --- /dev/null +++ b/lib/python3.12/site-packages/ray/util/tracing/setup_tempo_tracing.py @@ -0,0 +1,24 @@ +# This file is intended for examples exporting traces to a local OTLP listener +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) # noqa +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleSpanProcessor, +) + + +def setup_tracing() -> None: + # Sets the tracer_provider. This is only allowed once per execution + # context and will log a warning if attempted multiple times. + trace.set_tracer_provider(TracerProvider()) + trace.get_tracer_provider().add_span_processor( + SimpleSpanProcessor( + OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) + ) + ) + trace.get_tracer_provider().add_span_processor( + SimpleSpanProcessor(ConsoleSpanExporter()) + ) diff --git a/lib/python3.12/site-packages/ray/util/tracing/tracing_helper.py b/lib/python3.12/site-packages/ray/util/tracing/tracing_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..e0231da692968941f3c5b2f0f8b3da1bf55cafaa --- /dev/null +++ b/lib/python3.12/site-packages/ray/util/tracing/tracing_helper.py @@ -0,0 +1,553 @@ +import importlib +import inspect +import logging +import os +from contextlib import contextmanager +from functools import wraps +from inspect import Parameter +from types import ModuleType +from typing import ( + Any, + Callable, + Dict, + Generator, + List, + MutableMapping, + Optional, + Sequence, + Union, + cast, +) + +import ray +import ray._private.worker +from ray._private.inspect_util import ( + is_class_method, + is_function_or_method, + is_static_method, +) +from ray.runtime_context import get_runtime_context + +logger = logging.getLogger(__name__) + + +class _OpenTelemetryProxy: + """ + This proxy makes it possible for tracing to be disabled when opentelemetry + is not installed on the cluster, but is installed locally. + + The check for `opentelemetry`'s existence must happen where the functions + are executed because `opentelemetry` may be present where the functions + are pickled. This can happen when `ray[full]` is installed locally by `ray` + (no extra dependencies) is installed on the cluster. + """ + + allowed_functions = {"trace", "context", "propagate", "Context"} + + def __getattr__(self, name): + if name in _OpenTelemetryProxy.allowed_functions: + return getattr(self, f"_{name}")() + else: + raise AttributeError(f"Attribute does not exist: {name}") + + def _trace(self): + return self._try_import("opentelemetry.trace") + + def _context(self): + return self._try_import("opentelemetry.context") + + def _propagate(self): + return self._try_import("opentelemetry.propagate") + + def _Context(self): + context = self._context() + if context: + return context.context.Context + else: + return None + + def try_all(self): + self._trace() + self._context() + self._propagate() + self._Context() + + def _try_import(self, module): + try: + return importlib.import_module(module) + except ImportError: + if _is_tracing_enabled(): + raise ImportError( + "Install OpenTelemetry with " + "'pip install opentelemetry-api==1.34.1 opentelemetry-sdk==1.34.1 opentelemetry-exporter-otlp==1.34.1' " + "to enable tracing. See the Ray documentation for details: " + "https://docs.ray.io/en/latest/ray-observability/user-guides/ray-tracing.html#installation" + ) + + +_global_is_tracing_enabled = False +_opentelemetry = None + + +def _is_tracing_enabled() -> bool: + """Checks environment variable feature flag to see if tracing is turned on. + Tracing is off by default.""" + return _global_is_tracing_enabled + + +def _enable_tracing(): + global _global_is_tracing_enabled, _opentelemetry + _global_is_tracing_enabled = True + _opentelemetry = _OpenTelemetryProxy() + _opentelemetry.try_all() + + +def _sort_params_list(params_list: List[Parameter]): + """Given a list of Parameters, if a kwargs Parameter exists, + move it to the end of the list.""" + for i, param in enumerate(params_list): + if param.kind == Parameter.VAR_KEYWORD: + params_list.append(params_list.pop(i)) + break + return params_list + + +def _add_param_to_signature(function: Callable, new_param: Parameter): + """Add additional Parameter to function signature.""" + old_sig = inspect.signature(function) + old_sig_list_repr = list(old_sig.parameters.values()) + # If new_param is already in signature, do not add it again. + if any(param.name == new_param.name for param in old_sig_list_repr): + return old_sig + new_params = _sort_params_list(old_sig_list_repr + [new_param]) + new_sig = old_sig.replace(parameters=new_params) + return new_sig + + +class _ImportFromStringError(Exception): + pass + + +def _import_from_string(import_str: Union[ModuleType, str]) -> ModuleType: + """Given a string that is in format ":", + import the attribute.""" + if not isinstance(import_str, str): + return import_str + + module_str, _, attrs_str = import_str.partition(":") + if not module_str or not attrs_str: + message = ( + 'Import string "{import_str}" must be in format' '":".' + ) + raise _ImportFromStringError(message.format(import_str=import_str)) + + try: + module = importlib.import_module(module_str) + except ImportError as exc: + if exc.name != module_str: + raise exc from None + message = 'Could not import module "{module_str}".' + raise _ImportFromStringError(message.format(module_str=module_str)) + + instance = module + try: + for attr_str in attrs_str.split("."): + instance = getattr(instance, attr_str) + except AttributeError: + message = 'Attribute "{attrs_str}" not found in module "{module_str}".' + raise _ImportFromStringError( + message.format(attrs_str=attrs_str, module_str=module_str) + ) + + return instance + + +class _DictPropagator: + def inject_current_context() -> Dict[Any, Any]: + """Inject trace context into otel propagator.""" + context_dict: Dict[Any, Any] = {} + _opentelemetry.propagate.inject(context_dict) + return context_dict + + def extract(context_dict: Dict[Any, Any]) -> "_opentelemetry.Context": + """Given a trace context, extract as a Context.""" + return cast( + _opentelemetry.Context, _opentelemetry.propagate.extract(context_dict) + ) + + +@contextmanager +def _use_context( + parent_context: "_opentelemetry.Context", +) -> Generator[None, None, None]: + """Uses the Ray trace context for the span.""" + if parent_context is not None: + new_context = parent_context + else: + new_context = _opentelemetry.Context() + token = _opentelemetry.context.attach(new_context) + try: + yield + finally: + _opentelemetry.context.detach(token) + + +def _function_hydrate_span_args(function_name: str): + """Get the Attributes of the function that will be reported as attributes + in the trace.""" + runtime_context = get_runtime_context() + + span_args = { + "ray.remote": "function", + "ray.function": function_name, + "ray.pid": str(os.getpid()), + "ray.job_id": runtime_context.get_job_id(), + "ray.node_id": runtime_context.get_node_id(), + } + + # We only get task ID for workers + if ray._private.worker.global_worker.mode == ray._private.worker.WORKER_MODE: + task_id = runtime_context.get_task_id() + if task_id: + span_args["ray.task_id"] = task_id + + worker_id = getattr(ray._private.worker.global_worker, "worker_id", None) + if worker_id: + span_args["ray.worker_id"] = worker_id.hex() + + return span_args + + +def _function_span_producer_name(func: Callable[..., Any]) -> str: + """Returns the function span name that has span kind of producer.""" + return f"{func} ray.remote" + + +def _function_span_consumer_name(func: Callable[..., Any]) -> str: + """Returns the function span name that has span kind of consumer.""" + return f"{func} ray.remote_worker" + + +def _actor_hydrate_span_args( + class_: Union[str, Callable[..., Any]], + method: Union[str, Callable[..., Any]], +): + """Get the Attributes of the actor that will be reported as attributes + in the trace.""" + if callable(class_): + class_ = class_.__name__ + if callable(method): + method = method.__name__ + + runtime_context = get_runtime_context() + span_args = { + "ray.remote": "actor", + "ray.actor_class": class_, + "ray.actor_method": method, + "ray.function": f"{class_}.{method}", + "ray.pid": str(os.getpid()), + "ray.job_id": runtime_context.get_job_id(), + "ray.node_id": runtime_context.get_node_id(), + } + + # We only get actor ID for workers + if ray._private.worker.global_worker.mode == ray._private.worker.WORKER_MODE: + actor_id = runtime_context.get_actor_id() + + if actor_id: + span_args["ray.actor_id"] = actor_id + + worker_id = getattr(ray._private.worker.global_worker, "worker_id", None) + if worker_id: + span_args["ray.worker_id"] = worker_id.hex() + + return span_args + + +def _actor_span_producer_name( + class_: Union[str, Callable[..., Any]], + method: Union[str, Callable[..., Any]], +) -> str: + """Returns the actor span name that has span kind of producer.""" + if not isinstance(class_, str): + class_ = class_.__name__ + if not isinstance(method, str): + method = method.__name__ + + return f"{class_}.{method} ray.remote" + + +def _actor_span_consumer_name( + class_: Union[str, Callable[..., Any]], + method: Union[str, Callable[..., Any]], +) -> str: + """Returns the actor span name that has span kind of consumer.""" + if not isinstance(class_, str): + class_ = class_.__name__ + if not isinstance(method, str): + method = method.__name__ + + return f"{class_}.{method} ray.remote_worker" + + +def _tracing_task_invocation(method): + """Trace the execution of a remote task. Inject + the current span context into kwargs for propagation.""" + + @wraps(method) + def _invocation_remote_span( + self, + args: Any = None, # from tracing + kwargs: MutableMapping[Any, Any] = None, # from tracing + *_args: Any, # from Ray + **_kwargs: Any, # from Ray + ) -> Any: + # If tracing feature flag is not on, perform a no-op. + # Tracing doesn't work for cross lang yet. + if not _is_tracing_enabled() or self._is_cross_language: + if kwargs is not None: + assert "_ray_trace_ctx" not in kwargs + return method(self, args, kwargs, *_args, **_kwargs) + + assert "_ray_trace_ctx" not in kwargs + tracer = _opentelemetry.trace.get_tracer(__name__) + with tracer.start_as_current_span( + _function_span_producer_name(self._function_name), + kind=_opentelemetry.trace.SpanKind.PRODUCER, + attributes=_function_hydrate_span_args(self._function_name), + ): + # Inject a _ray_trace_ctx as a dictionary + kwargs["_ray_trace_ctx"] = _DictPropagator.inject_current_context() + return method(self, args, kwargs, *_args, **_kwargs) + + return _invocation_remote_span + + +def _inject_tracing_into_function(function): + """Wrap the function argument passed to RemoteFunction's __init__ so that + future execution of that function will include tracing. + Use the provided trace context from kwargs. + """ + if not _is_tracing_enabled(): + return function + + function.__signature__ = _add_param_to_signature( + function, + inspect.Parameter( + "_ray_trace_ctx", inspect.Parameter.KEYWORD_ONLY, default=None + ), + ) + + @wraps(function) + def _function_with_tracing( + *args: Any, + _ray_trace_ctx: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> Any: + if _ray_trace_ctx is None: + return function(*args, **kwargs) + + tracer = _opentelemetry.trace.get_tracer(__name__) + function_name = function.__module__ + "." + function.__name__ + + # Retrieves the context from the _ray_trace_ctx dictionary we injected + with _use_context( + _DictPropagator.extract(_ray_trace_ctx) + ), tracer.start_as_current_span( + _function_span_consumer_name(function_name), + kind=_opentelemetry.trace.SpanKind.CONSUMER, + attributes=_function_hydrate_span_args(function_name), + ): + return function(*args, **kwargs) + + return _function_with_tracing + + +def _tracing_actor_creation(method): + """Trace the creation of an actor. Inject + the current span context into kwargs for propagation.""" + + @wraps(method) + def _invocation_actor_class_remote_span( + self, + args: Any = tuple(), # from tracing + kwargs: MutableMapping[Any, Any] = None, # from tracing + *_args: Any, # from Ray + **_kwargs: Any, # from Ray + ): + if kwargs is None: + kwargs = {} + + # If tracing feature flag is not on, perform a no-op + if not _is_tracing_enabled(): + assert "_ray_trace_ctx" not in kwargs + return method(self, args, kwargs, *_args, **_kwargs) + + class_name = self.__ray_metadata__.class_name + method_name = "__init__" + assert "_ray_trace_ctx" not in _kwargs + tracer = _opentelemetry.trace.get_tracer(__name__) + with tracer.start_as_current_span( + name=_actor_span_producer_name(class_name, method_name), + kind=_opentelemetry.trace.SpanKind.PRODUCER, + attributes=_actor_hydrate_span_args(class_name, method_name), + ) as span: + # Inject a _ray_trace_ctx as a dictionary + kwargs["_ray_trace_ctx"] = _DictPropagator.inject_current_context() + + result = method(self, args, kwargs, *_args, **_kwargs) + + span.set_attribute("ray.actor_id", result._ray_actor_id.hex()) + + return result + + return _invocation_actor_class_remote_span + + +def _tracing_actor_method_invocation(method): + """Trace the invocation of an actor method.""" + + @wraps(method) + def _start_span( + self, + args: Sequence[Any] = None, + kwargs: MutableMapping[Any, Any] = None, + *_args: Any, + **_kwargs: Any, + ) -> Any: + # If tracing feature flag is not on, perform a no-op + if not _is_tracing_enabled() or self._actor._ray_is_cross_language: + if kwargs is not None: + assert "_ray_trace_ctx" not in kwargs + return method(self, args, kwargs, *_args, **_kwargs) + + class_name = self._actor._ray_actor_creation_function_descriptor.class_name + method_name = self._method_name + assert "_ray_trace_ctx" not in _kwargs + + tracer = _opentelemetry.trace.get_tracer(__name__) + with tracer.start_as_current_span( + name=_actor_span_producer_name(class_name, method_name), + kind=_opentelemetry.trace.SpanKind.PRODUCER, + attributes=_actor_hydrate_span_args(class_name, method_name), + ) as span: + # Inject a _ray_trace_ctx as a dictionary + kwargs["_ray_trace_ctx"] = _DictPropagator.inject_current_context() + + span.set_attribute("ray.actor_id", self._actor._ray_actor_id.hex()) + + return method(self, args, kwargs, *_args, **_kwargs) + + return _start_span + + +def _inject_tracing_into_class(_cls): + """Given a class that will be made into an actor, + inject tracing into all of the methods.""" + + def span_wrapper(method: Callable[..., Any]) -> Any: + def _resume_span( + self: Any, + *_args: Any, + _ray_trace_ctx: Optional[Dict[str, Any]] = None, + **_kwargs: Any, + ) -> Any: + """ + Wrap the user's function with a function that + will extract the trace context + """ + # If tracing feature flag is not on, perform a no-op + if not _is_tracing_enabled() or _ray_trace_ctx is None: + return method(self, *_args, **_kwargs) + + tracer: _opentelemetry.trace.Tracer = _opentelemetry.trace.get_tracer( + __name__ + ) + + # Retrieves the context from the _ray_trace_ctx dictionary we + # injected. + with _use_context( + _DictPropagator.extract(_ray_trace_ctx) + ), tracer.start_as_current_span( + _actor_span_consumer_name(self.__class__.__name__, method), + kind=_opentelemetry.trace.SpanKind.CONSUMER, + attributes=_actor_hydrate_span_args(self.__class__.__name__, method), + ): + return method(self, *_args, **_kwargs) + + return _resume_span + + def async_span_wrapper(method: Callable[..., Any]) -> Any: + async def _resume_span( + self: Any, + *_args: Any, + _ray_trace_ctx: Optional[Dict[str, Any]] = None, + **_kwargs: Any, + ) -> Any: + """ + Wrap the user's function with a function that + will extract the trace context + """ + # If tracing feature flag is not on, perform a no-op + if not _is_tracing_enabled() or _ray_trace_ctx is None: + return await method(self, *_args, **_kwargs) + + tracer = _opentelemetry.trace.get_tracer(__name__) + + # Retrieves the context from the _ray_trace_ctx dictionary we + # injected, or starts a new context + with _use_context( + _DictPropagator.extract(_ray_trace_ctx) + ), tracer.start_as_current_span( + _actor_span_consumer_name(self.__class__.__name__, method.__name__), + kind=_opentelemetry.trace.SpanKind.CONSUMER, + attributes=_actor_hydrate_span_args( + self.__class__.__name__, method.__name__ + ), + ): + return await method(self, *_args, **_kwargs) + + return _resume_span + + methods = inspect.getmembers(_cls, is_function_or_method) + for name, method in methods: + # Skip tracing for staticmethod or classmethod, because these method + # might not be called directly by remote calls. Additionally, they are + # tricky to get wrapped and unwrapped. + if is_static_method(_cls, name) or is_class_method(method): + continue + + if inspect.isgeneratorfunction(method) or inspect.isasyncgenfunction(method): + # Right now, this method somehow changes the signature of the method + # when they are generator. + # TODO(sang): Fix it. + continue + + # Don't decorate the __del__ magic method. + # It's because the __del__ can be called after Python + # modules are garbage colleted, which means the modules + # used for the decorator (e.g., `span_wrapper`) may not be + # available. For example, it is not guranteed that + # `_is_tracing_enabled` is available when `__del__` is called. + # Tracing `__del__` is also not very useful. + # https://joekuan.wordpress.com/2015/06/30/python-3-__del__-method-and-imported-modules/ # noqa + if name == "__del__": + continue + + # Add _ray_trace_ctx to method signature + method.__signature__ = _add_param_to_signature( + method, + inspect.Parameter( + "_ray_trace_ctx", inspect.Parameter.KEYWORD_ONLY, default=None + ), + ) + + if inspect.iscoroutinefunction(method): + # If the method was async, swap out sync wrapper into async + wrapped_method = wraps(method)(async_span_wrapper(method)) + else: + wrapped_method = wraps(method)(span_wrapper(method)) + + setattr(_cls, name, wrapped_method) + + return _cls